diff --git a/.changeset/16094-liveness-dead-warns.md b/.changeset/16094-liveness-dead-warns.md deleted file mode 100644 index 9662ac3e0d6..00000000000 --- a/.changeset/16094-liveness-dead-warns.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -Authoring a key whose liveness-ledger verdict is `dead` now draws a `liveness-dead-property` warning, and a `live-elsewhere` key a `liveness-live-elsewhere-property` warning, with no per-row `authorWarn` opt-in: the verdict itself is the warning. Before this, both rule ids were exported and never produced, because no shipped `dead` or `live-elsewhere` row opted in. - -Clause-②: no - -**Which keys warn.** A ledger row warns when its status is `dead`, `live-elsewhere` or `experimental`, or when it sets `authorWarn: true` (still the only way a `planned` row warns). Among authorable keys in the metadata types the rule walks, four are `dead` today: a view container's own `name` and `label` (the `defineView` container, not `list.label`), and a permission set's `rowLevelSecurity[].label` and `rowLevelSecurity[].description`. No walk visits `manifest`, `connectors` or realtime subscriptions, so their rows still warn nobody. That includes `manifest.runtime`, the one `live-elsewhere` row. - -**The hint.** A row that warns only because of its `dead` or `live-elsewhere` verdict shows its `authorHint`, else the verdict's default hint: "Remove it — it is declared in the spec but not consumed at runtime." for `dead`. It never shows the ledger's internal `note`. Rows that opt in with `authorWarn`, and `experimental` rows, show exactly the hint they showed before. - -**Retired keys.** A `retiredKey` tombstone keeps its `dead` row. Every command that parses (`os validate`, `os build`, the runtime publish gate) still refuses the key first, so it gets no second report. `os lint` does not parse: a config it accepts without `defineStack` that carries a retired key now gets a `liveness-dead-property` warning where it got nothing. - -**What changes for a project.** Nothing is refused, and nothing changes without `--strict`. `os lint --strict` and `os validate --strict` now exit 1 instead of 0 on a stack that was otherwise warning-clean and authors a view container `label` or `name`, or a row-level-security policy `label` or `description`. A view container that carries its own `name` and `label` beside its `object` binding is one such shape: it draws two warnings per container, and under `--strict` that flips the exit. Across this repository's example apps, only `app-showcase` gains warnings: two, on one permission set's policy `label` and `description`, and no example's exit code changes. To clear the warning, delete the key: nothing reads it. diff --git a/.changeset/18386-export-import-template.md b/.changeset/18386-export-import-template.md deleted file mode 100644 index 542d0899944..00000000000 --- a/.changeset/18386-export-import-template.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/rest': minor ---- - -feat(rest): `GET /api/v1/data/:object/export?template=true` answers an xlsx import template for the object (#18386) - -Clause-②: yes (widening) - -The export door takes one more query parameter, `template`. `template=true` -answers an `.xlsx` workbook with no data rows; `template=false` answers the export. -Without a `template` parameter the export is exactly as before, byte for byte. - -- **Columns.** Every field of the object except - those marked `system` or `readonly`, and `formula`, `summary` and - `autonumber` fields, in the order the object declares them. A `hidden` field - that can be written is a column. The seven columns the platform adds to every - object (`organization_id`, `created_at`, `created_by`, `updated_at`, - `updated_by`, `owner_id`, `owning_business_unit_id`) are never template - columns. A field the caller's field-level security does not let them edit is - left out. If the security service cannot say which fields the caller can edit, - the columns are narrowed by the fields the caller can read instead. The - instructions sheet then says so, and the `X-Export-Template-Projection` - response header reads `readable` instead of `writable` (`none` when no - field-level security applies). An explicit `?fields=` list is used as sent. -- **First sheet.** The header row, with ` *` after each field that is required - and has no default value, and one example row to replace or delete. Select, - radio and boolean columns carry a dropdown. -- **Second sheet.** One row per column: the field's API name, its type, whether - it is required, and the values the import accepts for it. -- **Language.** The sheets are in Chinese for a `zh` request locale - (`?locale=` or `Accept-Language`) and in English otherwise. - -The same two permission checks as the export apply: an object that does not -expose export answers `405`, and a caller without the export permission answers -`403`. `template` with a value other than `true` or `false`, a `format` other -than `xlsx`, or any of `limit`, `page`, `filter`, `search`, `searchFields`, -`orderby` or `header` beside `template=true`, answers `400 VALIDATION_ERROR`. diff --git a/.changeset/18386-plugin-security-writable-fields.md b/.changeset/18386-plugin-security-writable-fields.md deleted file mode 100644 index 14df141e00d..00000000000 --- a/.changeset/18386-plugin-security-writable-fields.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -The `security` service implements `getWritableFields(object, context)` (#18386). It uses the same permission sets, field grants, `requiredPermissions` check and on-behalf-of delegator intersection as the write gate. A field is in the answer exactly when a write naming it passes the field-level-security check. `getReadableFields` now shares that derivation, and its answers are unchanged. diff --git a/.changeset/18386-security-service-writable-fields.md b/.changeset/18386-security-service-writable-fields.md deleted file mode 100644 index 9e2e5b23c48..00000000000 --- a/.changeset/18386-security-service-writable-fields.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -`ISecurityService` (`@objectstack/spec/contracts`) gains an optional `getWritableFields(object, context)`: the field names field-level security lets the caller write on the object, the write-side twin of `getReadableFields` (#18386). - -Clause-②: yes (widening) - -- It is the exact complement of the fields the write path's field-level-security gate refuses when a payload names them. Neither the object permission nor a field's own rules (`readonly`, `system`, a `formula`, `summary` or `autonumber` type) are part of the answer. -- It fails soft like `getReadableFields`: `undefined` means no answer, `[]` means no field is writable. A system context gets every field. -- It is optional. A consumer checks `typeof svc.getWritableFields === 'function'`. When the method is missing, the consumer may narrow by `getReadableFields` instead, and must say in its response that it did. diff --git a/.changeset/18386-template-required-option-default.md b/.changeset/18386-template-required-option-default.md deleted file mode 100644 index 44b1f12965f..00000000000 --- a/.changeset/18386-template-required-option-default.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the import template (`GET /api/v1/data/:object/export?template=true`) puts ` *` on a column only when the import refuses a row that leaves it blank - -Clause-②: no - -- A required field whose option list marks an option `default: true` no longer gets ` *` in the header, and the instructions sheet lists it as not required. A blank cell in that column is imported as the marked option. -- A required field that declares `defaultValue: null` now gets ` *`, unless one of its options is marked `default: true`: the import treats `null` as no default and refuses the blank. A required field whose default is `''`, or `[]` on a multi-valued field, now gets ` *` too: the required check refuses that default. -- A required `system`, `readonly` or `autonumber` field named in `?fields=` no longer gets ` *`: the import does not refuse a blank in it. -- Each dropdown's error title, which is the column header, is cut to 32 characters, the longest title Excel's data-validation dialog takes. diff --git a/.changeset/19518-picklist-kind.md b/.changeset/19518-picklist-kind.md deleted file mode 100644 index d5990040929..00000000000 --- a/.changeset/19518-picklist-kind.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/platform-objects': patch -'@objectstack/cli': patch -'@objectstack/driver-sql': patch ---- - -feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518) - -Clause-②: yes (widening) - -- **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused. -- **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source. -- **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. The runtime resolves the reference onto that served field; see the picklist runtime entry of this release. -- **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package. -- **Translation.** `TranslationData` gains `picklists..{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them. -- **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES). -- **Extraction.** `os i18n extract` walks `picklists.NAME.{label, options.VALUE}`, including an extension's options under the list it extends, and `os lint` reports an untranslated option under its own rule, `i18n/missing-picklist`. -- **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column. diff --git a/.changeset/19518-picklist-wording.md b/.changeset/19518-picklist-wording.md deleted file mode 100644 index 00eab7a60d4..00000000000 --- a/.changeset/19518-picklist-wording.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the `picklist` field key's description, two doc comments and the refusal of `picklist` with `options` no longer say that the reference is resolved and its options served (#19518) - -Clause-②: no diff --git a/.changeset/19519-picklist-runtime.md b/.changeset/19519-picklist-runtime.md deleted file mode 100644 index 2cb5af66447..00000000000 --- a/.changeset/19519-picklist-runtime.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/objectql': minor -'@objectstack/metadata': patch -'@objectstack/spec': patch ---- - -feat(objectql): the runtime resolves a field's `picklist` onto its served options, validates writes against the resolved list, and merges `picklistExtensions` additively - -Clause-②: no - -- **Load.** `defineStack({ picklists })` and `defineStack({ picklistExtensions })` now register, from a manifest and from a nested plugin, through the same registration seam as every other collection. The compiled-artifact door registers `picklists` as `picklist` items, so `GET /meta/picklist` serves them on an artifact boot. -- **Merge.** A picklist's options are its own, followed by the options every `picklistExtensions` entry adds. A value the list already carries is refused with `422 INVALID_METADATA`, which names both declarations, whichever of the two registered first. The later declaration never replaces the earlier one. A package that registers again replaces its own extension. Uninstalling a package removes the values it added. -- **Serve.** A field with `picklist: 'NAME'` is served with the resolved options written onto it and `picklist` kept (`PicklistServedFieldSchema`), on every object read, including objects stored in `sys_metadata`. The list's translations (`picklists.NAME.options.VALUE`) relabel those options per request locale. An option marked `default: true` in the list fills an omitted field on insert, as an inline option does, and the import template reads it the same way. -- **Unknown name.** A packaged field that names a picklist no loaded package declares fails the boot at `kernel:ready` with `INVALID_METADATA`, and so does a `picklistExtensions` entry that extends such a list. The error names every such field or extension and the package that declared it. After boot, an artifact registered through the `manifest` service is checked before any of it registers. A field whose list does not resolve is served with no options and accepts no value. -- **Write validation.** The write door judges a picklist-bound field against the resolved options, and its refusal names the picklist. The wire code stays `invalid_option`. The validation message catalog gains three message keys for this (`invalid_option_picklist`, `invalid_option_value_picklist`, `invalid_option_picklist_unresolved`) in en, zh-CN, ja-JP and es-ES. They change the message text only, never the wire. -- **Writing the served body back.** The served body carries `picklist` and `options` together. Writing it back through the metadata door is still refused, with the prescription to drop `options`, as `FieldSchema` declares. Nothing strips it on the write side. -- **Ledger.** `field.picklist`, the `picklist` kind's rows and `translation.picklists` are `live`. `field.picklist` no longer carries `authorWarn`, so `os lint` / `os validate` stop warning an author who writes it. diff --git a/.changeset/19922-console-manifest-fallback.md b/.changeset/19922-console-manifest-fallback.md deleted file mode 100644 index 3e5132d0005..00000000000 --- a/.changeset/19922-console-manifest-fallback.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -fix(cli)!: a project with no `sdui.manifest.json` of its own has its `kind: 'html'` pages checked against the manifest `@objectstack/console` ships, so `div` and every other undeclared tag or prop is refused (#19922) - -Clause-②: no (narrowing) - - - -**BREAKING for `kind: 'html'` pages in projects without their own manifest.** - -**What changed.** `objectstack validate`, `objectstack compile` / `build` and -`objectstack lint` check the `source` of a `kind: 'html'` page against an SDUI -component manifest. (`dev` and `start` run `compile` before they boot when -`dist/objectstack.json` is missing or `--compile` is passed, and `dev`'s default -watch mode reruns it when a watched file changes.) They look first for the -`sdui.manifest.json` in the directory the command runs in, then for the copy -`@objectstack/console` ships as `dist/sdui.manifest.json`. The second lookup asked for that file by a subpath -the console package does not export, so it always failed, and a project with no -manifest of its own had its html pages checked at parse level only: syntax and -structure, never which components and props they use. The lookup now reaches the -shipped copy, and those pages get full component and prop validation. A tag or -prop the manifest does not declare is refused (`jsx-forbidden-tag`, -`jsx-unknown-component`, `jsx-unknown-prop`), naming the page and the tag, and -the command exits 1. - -**What was refused before, and what is new.** The console has refused a `div` on -a `kind: 'html'` page since `@objectstack/console` 17.5.0: when the page renders, -its in-browser html compile answers `forbidden-tag`, naming `box`. These commands -now give that answer while you author. What they refuse that nothing refused -before is every other tag the manifest does not declare. The console's html -compile accepts every component its registry knows that is not deprecated there, -while the published manifest declares only the public component contract and the -html tier's intrinsic tags. So a page using, for example, `avatar` or `checkbox` -renders in the console and is refused here. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `
` … `
` in a `kind: 'html'` page | `` … ``, which takes the same `className` and children | -| any other tag or prop the command names | a component and prop the manifest declares | - -**What is not affected.** A project that keeps its own `sdui.manifest.json` is -checked against that file, as before. `kind: 'react'` pages and pages authored -as regions are not read by this gate. With no manifest reachable at all, the -pages are still checked at parse level, and the notice that says so is -unchanged. - -**A damaged install is refused, not skipped.** A shipped copy that is present -but cannot be read or parsed stops the command with exit 1, naming the file, -with the remedy: reinstall `@objectstack/console`. - -**A correction to the 17.5.0 note on this manifest.** The `@objectstack/console` -17.5.0 patch entry `28ce612`, the one that says the prebuilt Console dist now -ships `dist/sdui.manifest.json`, ends with a paragraph that this release changes, -sentence by sentence: - -- "For now the file is only present in the tarball." No longer true: the CLI - reads it, as described above. -- "This package's `exports` map exposes `./package.json` and nothing else, so - resolving `@objectstack/console/dist/sdui.manifest.json` through `exports` - fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`." Still true: the `exports` map is - unchanged. -- "Anything that resolves through `exports` cannot read the file yet." Still - true. To read the file, resolve `@objectstack/console/package.json` and join - `dist/sdui.manifest.json` to its directory. -- "That includes the CLI's JSX-page manifest fallback, which catches the error - and keeps parse-level validation, as before." True of the 17.5.0 CLI, false - from this release: the fallback now reads the file that way, so a project - without its own manifest is checked against the shipped copy. diff --git a/.changeset/20051-persist-parsed-view-body.md b/.changeset/20051-persist-parsed-view-body.md deleted file mode 100644 index 3a91e7b95f0..00000000000 --- a/.changeset/20051-persist-parsed-view-body.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/spec': minor ---- - -fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level `options` bag is refused by name (#20051) - -**BREAKING** — two narrowings on the `view` write door (`PUT /api/v1/meta/view/:name`, the Studio and MCP save). They ship as `minor` under the repo's launch-window convention for breaking changes. This is stage (iv), the last stage, of ruling 甲 on #20051. The storage half follows the maintainer's ruling on its Q2, letter B (「同意 批次 #256」). - -Clause-②: no (narrowing) - -## What changes - -**1. What a saved view stores.** `saveMetaItem` used to validate a `view` body and then store the request body as sent, with two normalizations grafted back (filter operator spellings, and form `groups` → `sections`). It now stores the parsed value of every key the body carried, and nothing else: - -- **An undeclared key is dropped.** The members' top-level `.strip()` used to drop it from the parse only, so it lived in the store and nowhere in the contract. Every key the console reads back off a stored row is declared (`VIEW_CONSOLE_ROUND_TRIP_KEYS`), so nothing the console relies on is lost. The keys the console writes that are dropped are the ones mapped rather than declared in that record: a sort row's `id` (the builders mint a fresh one), a top-level `id` (read only when a row has no `name`), and `objectName` (every reader falls back to `object`, which both writers stamp). -- **A declared key keeps its normalized value.** Examples: `notEquals` → `not_equals`, `exportOptions: ['csv']` → `{ formats: ['csv'] }`, and a CEL string → its expression object. -- **A moved key is stored under its canonical spelling.** Examples: `groups` → `sections`, and `visibleOn` → `visibleWhen`. The second was never grafted before, so a form stored with `visibleOn` kept the legacy spelling. -- **A schema default the author did not write is NOT stored.** This is ADR-0087's `storable` rule, the one flows already follow: a stored row never pins the day's default. A console toolbar save (sort, density, hidden fields, column widths, inline edit) stores no `type: 'grid'`. A form stores no `sharing.enabled` and no section `collapsible` / `collapsed` / `columns` it did not write. Storing the whole parse output instead would also have minted rows that fail their own re-save: a column-less toolbar patch carrying the `grid` default is refused as "sets `type` but lists no `columns`". - -The stored row re-parses to exactly what the save accepted, so a GET → PUT of it is judged the same. Every other metadata type keeps its request body, with the two grafts, as before. - -**2. A ViewItem record's top-level `options` bag is refused.** On a record (`{ name, object, viewKind, config }`), the member's top-level strip used to drop `options` from the parse unread while the save stored it. The interface page then rendered it and the object page did not. It is now refused by name with `422 INVALID_METADATA` at `options`, and the message prescribes `config.KIND`. No console write puts the bag on a record. The flattened list overlay's legacy `options` bag is unchanged: it is judged key by key, and objectui pins it. - -## FROM → TO - -| you wrote | the stored row / the door now | -|:--|:--| -| a view body with a key its member does not declare (`objectName`, a form-only `layout` on a list view, `isPinned` on a form) | the key is not stored: write only declared keys (`object`, not `objectName`) | -| a view body relying on a schema default being written into the row | the row carries only what you wrote; the parse applies the default on every read | -| `sort: [{ id, field, order }]` | stored as `sort: [{ field, order }]` | -| `groups: [...]` / `visibleOn: '…'` on a form | stored as `sections: [...]` / `visibleWhen: { dialect: 'cel', source: '…' }` | -| a ViewItem record with `options: { kanban: {...} }` | `422` at `options`: move it to `config: { kanban: {...} }` (`config.KIND`), or remove it | - -**The one-line fix:** write the declared spelling. A stored view you read back is what the contract accepts, and a record's per-kind blocks live under `config`. - -## Existing rows - -Stored rows are not migrated and not re-read differently. The maintainer's word on this card is 「20051 不考虑现有的数据」. A row keeps its bytes until its next save, and that save stores it as described above. A record carrying a top-level `options` is refused on its next save and served as stored until then. - - diff --git a/.changeset/20112-sdui-parser-html-tier-stamp.md b/.changeset/20112-sdui-parser-html-tier-stamp.md deleted file mode 100644 index 10b4a113947..00000000000 --- a/.changeset/20112-sdui-parser-html-tier-stamp.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/sdui-parser': minor -'@objectstack/console': minor ---- - -The SDUI manifest now marks the html tier's intrinsic tags `tier: 'html'`, and this copy of the parser carries that marker the way the renderer's copy does. - -objectui's copy of `packages/sdui-parser` gained the marker when its registry started declaring the intrinsic HTML tags a `kind:'html'` page may author (`h1`–`h6`, `p`, `a`, `code`, `span`, `table`, the sectioning tags and the rest of the roster; never `div`). This copy still declared only `'public' | 'internal'` and dropped the key. The repo-root `sdui.manifest.json` is serialised through this copy, and `@objectstack/console` ships it in its `dist`, so the published manifest listed those tags with no marker. A reader could not tell them from curated blocks. The port is byte-faithful to objectui at the console pin `dd3f7e1be356`: - -- `RegistryConfigLike.tier` accepts `'html'`, the stamp objectui's `getPublicConfigs()` puts on the roster in its projection. -- `ManifestComponent.tier?: 'html'` is new. `manifestFromConfigs` writes exactly `'html'` or omits the key, so every other entry serialises byte-identically to before. -- `generateBlockList` counts only curated blocks in its title and lists the html tier in a section of its own. - -**What moved in the published manifest:** `"tier": "html"` on 48 entries, and nothing else. It still has the same 107 components in the same order, and no other field on any entry changed. - -**What did not move:** `compile()` and `validateTree()` read the manifest as a whitelist of keys and never read `tier`. So no page's verdict changes. Measured on the three shipped `kind:'html'` pages of `examples/app-showcase`: the full `compile()` result is identical against the old and the new manifest. diff --git a/.changeset/20142-console-page-nav-entries.md b/.changeset/20142-console-page-nav-entries.md deleted file mode 100644 index b7535988f7c..00000000000 --- a/.changeset/20142-console-page-nav-entries.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/platform-objects': patch -'@objectstack/plugin-audit': patch ---- - -fix(platform-objects,plugin-audit): Setup and Studio navigation entries for the console's Audit Log and Integrations & APIs pages (#20142) - -The console retired its System Hub card wall and its Developer Hub, which had been the only in-app links to several pages, and registered each page under a component-registry key instead. Framework navigation reaches a console page only through a `type: 'component'` item that names such a key, and no item named them, so each page was reachable only by a typed URL. Two entries now name the pages whose capability ships in the open framework: - -| Entry | App / group | `componentRef` | Contributed by | Gate | -| --- | --- | --- | --- | --- | -| `nav_audit_log_browser` ("Audit Log Browser") | Setup / Diagnostics, directly under Audit Logs | `audit:log` | `@objectstack/plugin-audit` | none: it lives and dies with the plugin that owns `sys_audit_log` | -| `nav_integrations` ("Integrations & APIs") | Studio / Developer, after Public Forms | `developer:integrations` | `@objectstack/platform-objects` | none beyond Studio's own `studio.access` | - -**Two audit entries, on purpose.** The existing Audit Logs entry (the `sys_audit_log` object view) stays. It carries the named list views, search, and the actor and tenant rendered as resolved lookups. The new page adds one filterable table whose detail drawer pretty-prints a change's before and after JSON, where the record page shows `old_value` / `new_value` as raw text. Neither surface replaces the other. - -**No entry for the console's AI Approvals page (`ai:approvals`) here.** Under ADR-0029 D7, each capability plugin contributes its own navigation entries into a Setup slot, and the Setup shell does not enumerate capability objects. The AI pending-action queue belongs to the AI capability, whose provider (`@objectstack/service-ai`) ships in Cloud/Enterprise, not in the open framework. Its entry is therefore that capability's to contribute. - -The keys are the ones the console registers at the objectui commit this release's console is built from. Labels ship in all four locales (en, zh-CN, ja-JP, es-ES), with their source hashes recorded. Nothing is removed or renamed, and there is nothing to migrate. diff --git a/.changeset/20166-jsx-gate-manifest-beside-config.md b/.changeset/20166-jsx-gate-manifest-beside-config.md deleted file mode 100644 index bf0d990aa45..00000000000 --- a/.changeset/20166-jsx-gate-manifest-beside-config.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -fix(cli)!: `objectstack validate`, `objectstack build` and `objectstack lint` read the project's `sdui.manifest.json` beside the config they were given, not in the directory they were run from (#20166) - -Clause-②: no (narrowing) - - - -**BREAKING for runs given a config path in another directory.** - -**What changed.** These commands check the `source` of each `kind: 'html'` page -against an SDUI component manifest: the project's own `sdui.manifest.json` first, -then the copy `@objectstack/console` ships. They located everything else about a -project from the directory of its config, but looked for the project's own -manifest in the directory the command was run from. So -`objectstack validate path/to/app/objectstack.config.ts`, run from anywhere else, -never read `path/to/app/sdui.manifest.json`, and a manifest that happened to sit in -the directory it was run from judged a project it does not belong to. They now read -the manifest beside the config. - -## Which manifest each run reads - -| the run | the project manifest it read | the project manifest it reads now | -|:--|:--|:--| -| `objectstack validate` / `build` / `lint` with no config path, in the project's directory | `./sdui.manifest.json` | `./sdui.manifest.json` (unchanged) | -| the same commands given `path/to/app/objectstack.config.ts`, run from another directory | that other directory's `sdui.manifest.json` | `path/to/app/sdui.manifest.json` | - -When the project carries no manifest of its own, both rows then fall back to the -copy `@objectstack/console` ships, as before. - -**Which runs change, and which way.** Only runs whose config path names a directory -other than the one they run in. For those, the verdict can move in both directions: - -- A page the project's own manifest does not declare is now refused - (`jsx-forbidden-tag`, `jsx-unknown-component`, `jsx-unknown-prop`, exit 1), where - the other directory's manifest, or the console's copy, used to admit it. -- A project manifest that is present but not usable is now refused (exit 1), naming - that file, where the run used to read some other file. -- A project with no manifest of its own is now checked against the console's copy, - where the other directory's manifest used to decide. -- In the other direction, a page the other directory's manifest refused, and that - the project's own manifest (or the console's copy) declares, is now admitted. - -If such a run now fails, the manifest that belongs to the project is the one to -keep beside its config. - -**What is not affected.** A run in the project's own directory, with or without a -config path, reads the same file as before. `objectstack init`'s check of a freshly -generated scaffold keeps reading the directory it was run from. - -**A correction to this release's console-fallback entry.** That entry says these -commands "look first for the `sdui.manifest.json` in the directory the command runs -in". From this release they look first beside the config the command was given, -which is the same directory whenever the command runs in the project. diff --git a/.changeset/20233-actor-hot-external-query-delete-etl-storage-apimethod-dashboard-notification-record-runtime-rls-scim-migration-guidance-tracker-free.md b/.changeset/20233-actor-hot-external-query-delete-etl-storage-apimethod-dashboard-notification-record-runtime-rls-scim-migration-guidance-tracker-free.md deleted file mode 100644 index 98067568a9a..00000000000 --- a/.changeset/20233-actor-hot-external-query-delete-etl-storage-apimethod-dashboard-notification-record-runtime-rls-scim-migration-guidance-tracker-free.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `os migrate meta` guidance for the `actor-*`, `hot-*`, `external-*`, `query-*`, `delete-*`, `etl-*`, `storage-*`, `apimethod-*`, `dashboard-*`, `notification-*`, `record-*`, `runtime-*`, `rls-*` and `scim-*` migration entries states each lesson in words instead of citing tracker numbers - -Clause-②: no - -The ADR-0087 semantic entries of the `actor-*` family (the retired `ctx.user.roles` alias), -the `hot-*` family (the inert `'disk'` / `'distributed'` state strategies and the file-watch -placeholder), the `external-*` family (the retired external-lookup and message-queue -schemas), the `query-*` family (the retired `QueryAST` request members and aggregation -functions), the `delete-*` family (the retired by-id repoint in a `beforeDelete` hook), the -`etl-*` family (the retired ETL pipeline layer), the `storage-*` family (the retired -single-argument `IStorageService.list`), the `apimethod-*` family (the `apiMethods` enum -shrunk to six primitives), the `dashboard-*` family (the `compareTo` offset, the page-only -modal target, the chart-config structure refusal, the single-measure metric tile and the -funnel-only `stageOrder`), the `notification-*` family (the retired inbox cursor), the -`record-*` family (the object-form detail sections and the converged chatter position), the -`runtime-*` family (the retired `HttpServer` wrapper), the `rls-*` family (the refused array -comparand, cross-class field comparison and stored-list ordering in row-level predicates) -and the `scim-*` family (the retired `sys_scim_provider` object) are printed by -`os migrate meta` as the header, `why:` and `verify:` lines of a manual change. Their text -sent the reader to issue-tracker, pull-request and decision-batch numbers — some of which no -longer resolve, and some in another repository — for what a ruling, measurement or fix had -decided; it now says what was decided, in the sentence being read. ADR ids are kept. One -entry of another family is corrected in the same way: `rest-api-endpoint-handler-status-retired` -now names the API skill, whose factual sweep corrected the `handlerStatus` sentence, instead -of the automation skill. - -Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain -rewrites exactly what it rewrote before. No `surface` changes. The generated migration -registry, `spec-changes.json` and the protocol upgrade guide carry the same text. diff --git a/.changeset/20233-stage-10-migration-guidance-last-references.md b/.changeset/20233-stage-10-migration-guidance-last-references.md deleted file mode 100644 index e81e6d3875d..00000000000 --- a/.changeset/20233-stage-10-migration-guidance-last-references.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `os migrate meta` guidance for the two padded list-view field-name entries states the contract-first rule in words, and the notification/embed retirement drops a sweep batch ordinal - -Clause-②: no - -The ADR-0087 semantic entries are printed by `os migrate meta` as the header, `why:` and -`verify:` lines of a manual change. Two of them — -`ui-list-view-grouping-field-padded-refused` and `ui-list-view-groupbyfield-padded-refused` — -explained why a padded field name is refused rather than trimmed by pointing at a rule number -in a contributor guide, a number that names nothing in this repository's guide. Their `why:` -text now states the rule itself: fix the metadata, not the renderer — off-spec metadata is -refused where it is authored, never coerced into working. - -`ui-notification-action-embed-config-retired` named the batch of the v17 unknown-key -strictness sweep that measured the two retired shapes by its ordinal. The sentence already -says what that batch measured and decided, so the ordinal is dropped. - -Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes, and the -chain rewrites exactly what it rewrote before. The generated migration registry, -`spec-changes.json` and the protocol upgrade guide carry the same text. diff --git a/.changeset/20233-stage-8-migration-guidance-tracker-free.md b/.changeset/20233-stage-8-migration-guidance-tracker-free.md deleted file mode 100644 index 4ad41c993bc..00000000000 --- a/.changeset/20233-stage-8-migration-guidance-tracker-free.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `os migrate meta` guidance for twenty-four more migration-entry families — `stack-*`, `evaluated-*`, `aggregation-*`, `authoring-*`, `automation-*`, `cache-*`, `tenant-*`, `client-*`, `spec-*`, `cli-*`, `identity-*`, `import-*`, `tool-*`, `advanced-*`, `cloud-*`, `startup-*`, `sys-*`, `declarative-*`, `sort-*`, `address-*`, `packages-*`, `platform-*`, `session-*` and `strategy-*` — states each lesson in words instead of citing tracker numbers - -Clause-②: no - -The ADR-0087 semantic entries of these twenty-four families are printed by `os migrate meta` -as the header, `why:` and `verify:` lines of a manual change. Their text sent the reader to -issue-tracker, pull-request, decision-batch and summon numbers — some of which no longer -resolve, and some in another repository or a vendor's tracker — for what a ruling, -measurement or fix had decided; it now says what was decided, in the sentence being read. -ADR ids are kept, and so are the rule numbers of this repository's own contributor guide. - -Two entries also carried a tracker number in `surface`, the header line itself: -`authoring-schemas-strict-unknown-keys` now names the unknown-key strictness wave, and -`evaluated-expression-slots-source-required` names the census of engine-evaluated slots. -One sentence is corrected while being rewritten: `cli-command-contribution-retired` said the -`manifest.contributes.commands` tombstone was protocol 17; it is registered under protocol 18. - -Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain -rewrites exactly what it rewrote before. The generated migration registry, -`spec-changes.json` and the protocol upgrade guide carry the same text. diff --git a/.changeset/20233-stage-9-migration-guidance-tracker-free.md b/.changeset/20233-stage-9-migration-guidance-tracker-free.md deleted file mode 100644 index 4ba8d21d292..00000000000 --- a/.changeset/20233-stage-9-migration-guidance-tracker-free.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `os migrate meta` guidance for the remaining migration-entry families states each lesson in words instead of citing tracker numbers - -Clause-②: no - -The ADR-0087 semantic entries are printed by `os migrate meta` as the header, `why:` and -`verify:` lines of a manual change. In the families not yet brought to this line — among them -`turso-*`, `auth-*`, `admin-*`, `ai-*`, `assembled-*`, `change-*`, `device-*`, `epoch-*`, -`incident-*`, `logging-*`, `memory-*`, `send-*`, `standard-*`, `training-*`, `websocket-*`, -`structured-*` and `translation-*` — that text sent the reader to issue-tracker, pull-request, -decision-batch and cross-repository numbers, some of which no longer resolve, for what a -ruling, measurement or fix had decided; it now says what was decided, in the sentence being -read. Verbatim rulings that carried a card or batch number keep only their operative words. -ADR ids are kept, and so are the rule numbers of this repository's own contributor guide. With -this change no semantic entry's printed guidance carries a `#`-numbered tracker id. - -One replacement also named a contributor-guide rule by a number that no longer exists: -`address-location-value-unknown-keys-refused` now states the rule itself — a consumer never -carries an alias for an off-spec key; the metadata is fixed where it is written. - -Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes, and the -chain rewrites exactly what it rewrote before. The generated migration registry, -`spec-changes.json` and the protocol upgrade guide carry the same text. diff --git a/.changeset/20234-liveness-ledger-provenance-anchors-2.md b/.changeset/20234-liveness-ledger-provenance-anchors-2.md deleted file mode 100644 index 581104bb5a7..00000000000 --- a/.changeset/20234-liveness-ledger-provenance-anchors-2.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Notes in seven more liveness ledgers cite the commit that decided them, or say the decision in words, instead of a tracker number that no longer resolves - -Clause-②: no - -Notes in the `datasource`, `api`, `query`, `object`, `email_template`, `mapping` and -`webhook` ledgers named GitHub issues that no longer exist, so a reader could not tell why -a row carries its verdict. Each such note now either names the commit that made the -decision or, where the number alone carried the meaning, says what was decided. The -`manifest` ledger's `permissions` note also names the commit that recorded its structured -arm's zero apart. The `liveness/` ledgers ship in this package's tarball, which is why -this is a release note at all. Note text only: no row's status, evidence, proof or date -changes, and no schema, export or runtime behaviour changes. diff --git a/.changeset/20234-liveness-ledger-provenance-anchors-3.md b/.changeset/20234-liveness-ledger-provenance-anchors-3.md deleted file mode 100644 index 0f44127c7bc..00000000000 --- a/.changeset/20234-liveness-ledger-provenance-anchors-3.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Notes in twenty-one more liveness ledgers, and one `datasource` evidence string, cite the commit that decided them, or say the decision in words, instead of a tracker number that no longer resolves - -Clause-②: no - -Notes in the `book`, `doc`, `job`, `validation`, `translation`, `hook`, `seed`, `flow`, -`capability`, `qa`, `dashboard`, `action`, `agent`, `skill`, `tool`, `rest_api`, -`route_generation`, `crud_endpoints`, `metadata_endpoints`, `batch_endpoints` and -`analytics_cube` ledgers cited tracker numbers that no longer resolve on GitHub, so a reader -could not tell why a row carries its verdict. Each such note now either names the commit that -made the decision or, where the number alone carried the meaning, says what was decided. The -`datasource` ledger's `ssl.rejectUnauthorized` evidence string cited one such number in its -prose; it now names the commit that made the fix, and its code anchors are unchanged. The -`liveness/` ledgers ship in this package's tarball, which is why this is a release note at all. -Note text and that one evidence parenthesis only: no row's status, proof or date changes, and -no schema, export or runtime behaviour changes. diff --git a/.changeset/20234-liveness-ledger-provenance-anchors-4a.md b/.changeset/20234-liveness-ledger-provenance-anchors-4a.md deleted file mode 100644 index 752400504c1..00000000000 --- a/.changeset/20234-liveness-ledger-provenance-anchors-4a.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Notes in the `app` and `view` liveness ledgers that cited a tracker number which no longer resolves now either cite the commit that decided them or say the decision in words - -Clause-②: no - -Sixteen notes in the `app` and `view` ledgers cited a GitHub issue that no longer exists, so a -reader could not tell why a row carries its verdict. Each such note now either names the commit -that made the decision or, where the number alone carried the meaning, says what was decided. -The `liveness/` ledgers ship in this package's tarball, which is why this is a release note at -all. Note text only: no row's status, evidence, proof, producer or date changes, and no schema, -export or runtime behaviour changes. diff --git a/.changeset/20234-liveness-ledger-provenance-anchors-4b.md b/.changeset/20234-liveness-ledger-provenance-anchors-4b.md deleted file mode 100644 index 59116a0aeb5..00000000000 --- a/.changeset/20234-liveness-ledger-provenance-anchors-4b.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Notes in the `field` liveness ledger and sentences in the ledger README that cited a tracker number which no longer resolves now either cite a commit in this repository or say in words what the number stood for - -Clause-②: no - -Eleven citations in the `field` ledger's notes and twenty-nine in `liveness/README.md` pointed at -a GitHub issue or pull request that no longer exists, so they led nowhere. Each one now either -names the commit that did or recorded what the number pointed at or, where the number alone -carried the meaning, says that meaning in words. The `liveness/` ledgers and their README ship in -this package's tarball, which is why this is a release note at all. Prose only: no row's status, -evidence, proof, producer or date changes, no README table row or heading is added, removed or -renamed, and no schema, export or runtime behaviour changes. diff --git a/.changeset/20234-liveness-ledger-provenance-anchors.md b/.changeset/20234-liveness-ledger-provenance-anchors.md deleted file mode 100644 index 749843e5c02..00000000000 --- a/.changeset/20234-liveness-ledger-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The `manifest`, `dataset` and `permission` liveness ledgers cite the commit that decided each note, or say the decision in words, instead of a tracker number that no longer resolves - -Clause-②: no - -Notes in these three ledgers named GitHub issues that no longer exist, so a reader could -not tell why a row carries its verdict. Each such note now either names the commit that -made the decision or, where the number alone carried the meaning, says what was decided. The -`liveness/` ledgers ship in this package's tarball, which is why this is a release note at -all. Note text only: no row's status, evidence, proof or date changes, and no schema, -export or runtime behaviour changes. diff --git a/.changeset/20280-datetime-year-floor.md b/.changeset/20280-datetime-year-floor.md deleted file mode 100644 index 05eb6e8ff24..00000000000 --- a/.changeset/20280-datetime-year-floor.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/objectql': minor ---- - -fix(core,objectql)!: a `datetime` value names a year from 1000 to 9999 at both engine doors, so one before year 1000 is refused as a written value and as a filter comparand; a `date` keeps 0001 to 9999 - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the engine accepts as a `datetime`. The one range function both doors ask, `isOutsideTemporalYearRange` in `@objectstack/core`, now takes a lower bound per kind: a `datetime` starts at year 1000 (its UTC year), a `date` stays at 0001, and both still end at 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What is refused now.** A `datetime` whose UTC year falls in 0001..0999, which both doors accepted before: - -- **The write door** (the record validator), in every spelling it took: an ISO instant string, a bare `YYYY-MM-DD` (midnight UTC), a zone-naive `YYYY-MM-DD HH:MM`, and a `Date`. It answers `VALIDATION_FAILED` with the field's `invalid_date` code, on `engine.insert`, `engine.update` (one row or many) and the dry-run `engine.validate`, so on `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and each row of `POST /api/v1/data/:object/import` too. A number was already refused there, because a `datetime` is stored as text. -- **The comparand door** (the temporal-comparand door), in every spelling: an ISO string, a `Date` and epoch milliseconds as a number. It answers `INVALID_FILTER` / 400 at `where`, at a per-aggregation `filter` and at `having`, on the engine and on `POST /api/v1/data/:object/query`. The analytics native-SQL strategy asks the same predicate, so it declines such a comparand and the query reaches this door. -- The year is the instant's UTC year: `1000-01-01T00:00:00+08:00` is year 999 in UTC and is refused, and `0999-12-31T23:00:00-02:00` is year 1000 in UTC and is read. - -**What an author sees.** The comparand refusal names the field, the value, its position and the range: "an instant whose UTC year falls outside the years 1000 to 9999, the years a datetime value may name". It then says why the floor sits at 1000, instead of the misorder words that a year past 9999 still gets: MySQL documents its `DATETIME` from year 1000 only, and reads one stored in the years 0001 to 0099 back a century late. A comparand in those years that was already refused for its spelling or its day (a non-ISO spelling, a day that does not exist) is now named by its year first, as one past 9999 already was. The write refusal is the existing `invalid_date` sentence for a `datetime` field. - -**Why.** MySQL documents `DATETIME` from year 1000, and it reads a stored `DATETIME` in 0001..0099 back a century late through its client's instant parser (`0009-03-04 10:00` comes back as `2004-09-03T10:00Z`), which ADR-0053 D-F2 keeps. The range is the contract on every backend, so SQLite, PostgreSQL and the in-memory driver, which held these years, refuse them too. No writer or query of a `datetime` before year 1000 was found. - -**A `datetime` already stored before year 1000.** Nothing rewrites it, and nothing shifts it into the range. It reads back as before, and on MySQL a year in 0001..0099 still presents a century late. To find such rows, filter the field with `$lt` on `1000-01-01T00:00:00.000Z`, the floor's first instant, which both doors admit; the comparison runs on the stored value, so it finds them on MySQL as well. An update that leaves the field out is accepted. A write that carries a year below 1000 is refused, so the field can be written again with an instant from year 1000 on, or with `null`, and the author decides which. - -**Unchanged.** A `date` keeps 0001..9999, padded to four digits as before. A `time` column still reads the time of day of an instant in 0001..0999, and a `time` comparand refused for another reason keeps that reason's words. Every year from 1000 to 9999 on a `datetime`, and every refusal outside 0001..9999 on either kind, answers as before, apart from the range the words name. diff --git a/.changeset/20281-connector-sync-moved-to-mapping.md b/.changeset/20281-connector-sync-moved-to-mapping.md deleted file mode 100644 index ec19a77fc7b..00000000000 --- a/.changeset/20281-connector-sync-moved-to-mapping.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: connector-attached sync leaves the connector — `syncConfig` and `fieldMappings` are retired, and a `mapping` gains the `connectorSource` pull binding (#20281) - -**BREAKING** — `connector.syncConfig` (the `DataSyncConfig` block: `strategy`, -`direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, -`deleteMode`, `filters`) and `connector.fieldMappings` (the `ConnectorFieldMapping` -list: `source`, `target`, `defaultValue`, `dataType`, `required`, `syncMode`) are -removed from `ConnectorSchema` and `DeclarativeConnectorEntrySchema` — so from -`defineConnector`, `stack.connectors[]`, the `PUT /api/v1/meta/connector/:name` door -and `AutomationEngine.registerConnector`. The `DataSyncConfigSchema`, -`SyncStrategySchema`, `ConnectorConflictResolutionSchema` and -`ConnectorFieldMappingSchema` exports (and their `DataSyncConfig`, `SyncStrategy`, -`ConnectorConflictResolution`, `ConnectorFieldMapping` types and `…Parsed` aliases) -leave `@objectstack/spec/integration` with them. ADR-0049, ruled ENFORCE on the -maintainer's criterion for a declared-but-unenforced family, with the definition -MOVED: every mainstream platform binds a sync to its TARGET, not to the connection. - -Measured before removal: no engine ever ran a connector-attached sync or moved a -value through a connector field mapping — outside the spec package `syncConfig` -appeared only in two comments and `fieldMappings` nowhere, the automation service's -declared-connector item carried neither key, and the def a provider registers is its -own. The `latest_wins` and `soft_delete` defaults read as configured policy and did -nothing; the platform has no soft delete. - -**Added:** `mapping.connectorSource` — the pull -binding on the target side, beside the mapping's existing `targetObject`, -`fieldMapping`, `mode` and `upsertKey`: `connector` (a `rest` or `openapi` -connector instance), `action` (the action that reads the records), optional -`input`, optional `recordsPath`, and an optional `watermark` (`field` on the -record, `param` on the request) for a timestamp-incremental pull. Version 1 is a -one-way pull. It carries no cadence (a `job` sets that), no credential (the -connector instance holds it) and no delete or conflict policy. The connector sync -executor, `@objectstack/service-automation`'s `pullConnectorSource` (#20919), reads -it; nothing schedules a pull until the `job` stage lands. - -### FROM → TO - -| removed | what to write instead | -| --- | --- | -| `connector.syncConfig` | delete the key. A sync you still want is a `mapping` on its target object, with `connectorSource` naming the connector and its read action, `watermark` for an incremental pull, and a `job` for the cadence. `direction`, `conflictResolution` and `deleteMode` have no counterpart: the pull is one-way and writes through `mode` / `upsertKey`. | -| `connector.fieldMappings` | delete the key, and carry its `source` → `target` pairs into that mapping's `fieldMapping` (a `defaultValue` becomes a `constant` transform). | -| `DataSyncConfigSchema`, `SyncStrategySchema`, `ConnectorConflictResolutionSchema`, `ConnectorFieldMappingSchema` and their types | no replacement — nothing parsed a sync or a connector field mapping into anything that ran. | - -**The one-line fix: delete `syncConfig:` and `fieldMappings:` from every connector.** -`os migrate meta --from 17` lists the mechanical edits for existing sources. - -⚠️ Runtime behaviour is deliberately **unchanged**: no connector sync ever ran. -What changes is the answer an author gets — both keys are refused at parse with a -prescription naming the target-side binding, and in `tsc` (their input type is -`never`), instead of being saved with no effect. - -### The retirement kit - -- **Tombstones.** `syncConfig` and `fieldMappings` are `retiredKey()` tombstones on - the private `ConnectorBaseSchema` both published carriers wrap (the schema is not - `.strict()`, so a bare deletion would be a silent strip, ADR-0104). - `RETIRED_KEYS_BY_MAJOR[18]`: `integration/Connector:syncConfig`, - `integration/Connector:fieldMappings`, - `integration/DeclarativeConnectorEntry:syncConfig` and - `integration/DeclarativeConnectorEntry:fieldMappings`. Neither key had a default, - so no retired-default residue is owed. -- **Four defs leave whole** (`RETIRED_DEFS_BY_MAJOR[18]`): `integration/DataSyncConfig`, - `integration/SyncStrategy`, `integration/ConnectorConflictResolution`, - `integration/ConnectorFieldMapping`. The two `RENAMED_DEFS` entries that targeted - them left the rename table. -- **D2 conversion `connector-sync-keys-removed`** (step 18, retired from the load - path): strips both keys from `connectors[]` and from stored `sys_metadata` - connector rows (the rehydration seam replays it), one notice per key, as a - lossless delete. It never writes a `mapping`: a pulled mapping would start writes - that never happened. -- **D3 entry `connector-sync-keys-retired`** carries the family's judgement: which - syncs should now exist as target-side mappings, and what an author who relied on - `export`, `bidirectional`, `soft_delete` or a conflict policy does without them. -- **No deprecation window**, per the project's startup-stage posture. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is -published, so this is breaking for consumers no telemetry was consulted for. - -Clause-②: yes (narrowing) - - diff --git a/.changeset/20282-analytics-cube-format-granularities-enforced.md b/.changeset/20282-analytics-cube-format-granularities-enforced.md deleted file mode 100644 index 2881db884c9..00000000000 --- a/.changeset/20282-analytics-cube-format-granularities-enforced.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-analytics': minor ---- - -An authored analytics cube's measure `format` and time-dimension `granularities` now take effect on the analytics query doors, the way a compiled dataset's always have (#20282). - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one. The raw-SQL path declines every bucketed query, so such a query now runs on the engine aggregate path, which answers `400 INVALID_FIELD` for every member it cannot evaluate: a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression); and, on a cube whose members resolve through its `joins`, a measure or a `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a `dateRange` window, so grouping by a one-granularity time dimension over a joined object is refused too), a dimension that traverses more than one relationship, and an `avg` or `count_distinct` measure beside any dimension over a joined object. The raw-SQL path answers every one of these, with one group per distinct timestamp; each is now refused, exactly as it already was when the caller stated that granularity by hand. On a host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both), no strategy remains for a bucketed query, so every newly bucketed query, a plain `count` included, now answers "No strategy can handle query" instead of grouping raw timestamps. The remedy: run such a query without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key); on a raw-SQL-only host, add the engine aggregate bridge. It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect. - -Until this change both keys were read on the compiled-dataset path only. One cube shape has three producers — cubes authored with `defineCube()` / `defineStack({ analyticsCubes })`, cubes the dataset compiler mints, and cubes inferred for an ad-hoc query — and only a compiled dataset's cube reached the two readers: - -- **`measures..format`** reached a caller as `fields[].format` only because the dataset door copies it from the DATASET measure. An authored cube has no dataset, so `POST /api/v1/analytics/query` described its measure columns with `name` and `type` alone. Now every measure column a query names carries the `format` its cube measure declares, whichever strategy answered, and a column that declares none carries no `format` key at all. `GET /api/v1/analytics/meta` is unchanged: its projection stays `name`, `type` and `title`, and a client reads `format` off the query result's `fields[]`, as the Data API page already says. The value is relayed verbatim; the vocabulary `fields[].format` documents is a numeral pattern such as `"$0,0.00"` or `"0.0%"`. -- **`dimensions..granularities`** was the default bucket only for a compiled dataset, which the dataset executor filled in before querying. An authored cube's time dimension grouped raw timestamps whatever it declared. Now `query()` and the `generateSql()` dry run read it the same way, through the one rule both paths share: a single-entry list is the dimension's default bucket for a query that groups by it; a granularity the query states always wins, and one the list does not name is not refused (the dataset path compares against no list either); a list of two or more states no default; and a `timeDimensions` entry that carries only a `dateRange` for a dimension the query does not group stays a filter. - -What to expect after upgrading: - -- **A cube measure that declares `format`** now carries it on `POST /api/v1/analytics/query` results. A client that formats amounts from `fields[].format` starts formatting that column. -- **A cube time dimension that declares one granularity** (`granularities: ['month']`) is now bucketed by it when a query groups by it without stating one: one row per month where there was one row per timestamp. Name another granularity in the query's `timeDimensions` to bucket differently. -- **A cube time dimension that declares several, or none**, behaves exactly as before. -- **Compiled datasets** (`POST /api/v1/analytics/dataset/query`) answer exactly as before: the value read off their cube is the one the dataset door already used. - -In `@objectstack/spec`, `MetricSchema.format` and `DimensionSchema.granularities` now carry descriptions that state what the analytics service does with them (the metric's example values move from the names "currency" / "percent" to numeral patterns, the vocabulary the `fields[].format` slot documents), and the liveness ledger rows `analytics_cube.measures.format` and `analytics_cube.dimensions.granularities` move from `dead` to `live`, citing the new readers. diff --git a/.changeset/20282-analytics-cube-meta-descriptions.md b/.changeset/20282-analytics-cube-meta-descriptions.md deleted file mode 100644 index 9b5454ae077..00000000000 --- a/.changeset/20282-analytics-cube-meta-descriptions.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-analytics': minor ---- - -`GET /api/v1/analytics/meta` now publishes an analytics cube's `description`, each measure's and dimension's `description`, and each measure's `format`, when the cube definition declares them (#20282). - -Clause-②: yes (widening) - -- `CubeMeta` (`@objectstack/spec/contracts`) gains an optional `description` on the cube and on each measure and dimension, and an optional `format` on each measure. `AnalyticsMetadataResponseSchema` declares the same members. A definition that declares none of them is published exactly as before. -- `AnalyticsService.getMeta` copies what the definition declares and fills in nothing. A cube compiled from a dataset carries each dataset measure's `format` and no `description`. -- The liveness ledger rows `analytics_cube.description`, `measures.description` and `dimensions.description` move from `dead` to `live`. - -This supersedes one sentence of this release's note on an authored cube's measure `format` and `granularities`: it says `GET /api/v1/analytics/meta` is unchanged and keeps `name`, `type` and `title`. With this change `/meta` also publishes each measure's declared `format`. A client that formats a result column still reads `format` off the query result's `fields[]`. diff --git a/.changeset/20287-connector-actions-and-app-areas-live.md b/.changeset/20287-connector-actions-and-app-areas-live.md deleted file mode 100644 index 089b6c246ab..00000000000 --- a/.changeset/20287-connector-actions-and-app-areas-live.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Liveness ledger: `connector.actions.description`, `connector.actions.outputSchema` and `app.areas.description` are now `live`, not `dead`. Studio reads each of them at the `.objectui-sha` pin, and each row cites that reader and its producer. Ledger data, two README Notes cells and the regenerated count shards only. ⛔ No schema, parse, `.describe()` or accept-set change. - -The ledgers ship inside this package (`files[]` includes `liveness`), and `@objectstack/lint` reads them to decide which authored keys draw an advisory warning. None of the three rows sets `authorWarn`, so the set of warnings does not change. - -- `connector.actions.description`: the flow designer's Action picker on a `connector_action` node shows each action's description beside its label. -- `connector.actions.outputSchema`: the flow designer offers a `connector_action` node's downstream references from the top-level `properties` of its action's `outputSchema`. -- `app.areas.description`: the Studio app preview lists each area, with its description beneath it when one is authored. -- Both connector rows are fed from the plugin and provider door, as their sibling `actions.*` rows are: the `actions` an author writes on a metadata connector entry never reach the registry the designer reads. -- The regenerated count shards: `connector` has 31 live and 23 dead (was 29 and 25), and `app` has 50 live and 8 dead (was 49 and 9). diff --git a/.changeset/20287-connector-triggers-retired.md b/.changeset/20287-connector-triggers-retired.md deleted file mode 100644 index 5527cfd07e3..00000000000 --- a/.changeset/20287-connector-triggers-retired.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: retire the connector `triggers` array — the `ConnectorTrigger` shape nothing ever registered, polled or received (#20287) - -**BREAKING** — `connector.triggers` (the `ConnectorTrigger` array: `key`, `label`, -`description`, `type: 'polling' | 'webhook'`, `intervalSeconds`) is removed from -`ConnectorSchema` and `DeclarativeConnectorEntrySchema` — so from `defineConnector`, -`stack.connectors[]`, the `PUT /api/v1/meta/connector/:name` door and -`AutomationEngine.registerConnector` — and the `ConnectorTriggerSchema` / -`ConnectorTrigger` exports leave `@objectstack/spec/integration` with it. ADR-0049 -enforce-or-remove, ruled RETIRE on the maintainer's criterion for a -declared-but-unenforced family; ADR-0041 is unchanged: connector-event triggers stay -in its third tier, as their own trigger package, promoted when real projects ask for -them — and then in the mainstream shape (subscribe / unsubscribe lifecycle, -signature verification, a dedupe cursor), which these five keys could not carry. - -Measured before removal: `registerConnector` walks a connector's `actions` only and -stores the rest of the def unread; the engine's trigger registry holds FLOW trigger -kinds (`record_change`, `time_relative`, `schedule`, `api`) and no connector trigger -ever entered it; no polling loop read `intervalSeconds`; no receiver was driven by a -`webhook` trigger; and no connector package, provider or example declared one. A -declared trigger parsed clean and never started a flow. - -### FROM → TO - -| removed | what to write instead | -| --- | --- | -| `connector.triggers` with a `type: 'polling'` trigger (`intervalSeconds`, or the pre-rename `interval`) | delete the key, and write a `schedule` flow whose `connector_action` node calls the connector's action — at the cadence you meant, in seconds. | -| `connector.triggers` with a `type: 'webhook'` trigger | delete the key, and write an `api` flow that the external sender calls, with a `connector_action` node calling the connector's action. It opens an inbound endpoint that never existed before: an `api` flow is refused without a per-flow secret and every call must carry its signature, so the sender must be able to sign. | -| `ConnectorTriggerSchema`, `ConnectorTrigger` | no replacement — nothing parsed or constructed a connector trigger. | - -**The one-line fix: delete `triggers:` from every connector.** -`os migrate meta --from 17` lists the mechanical edits for existing sources. - -⚠️ Runtime behaviour is deliberately **unchanged**: no connector trigger ever started -anything. What changes is the answer an author gets — the key is refused at parse -with a prescription naming the two shapes that work, and in `tsc` (its input type is -`never`), instead of being saved with no effect. - -### The retirement kit - -- **Tombstone.** `triggers` is a `retiredKey()` tombstone on the private - `ConnectorBaseSchema` both published carriers wrap (the schema is not `.strict()`, - so a bare deletion would be a silent strip, ADR-0104). - `RETIRED_KEYS_BY_MAJOR[18]`: `integration/Connector:triggers` and - `integration/DeclarativeConnectorEntry:triggers`. The key had no default, so no - retired-default residue is owed. -- **The provider-bound refusal is gone.** `DeclarativeConnectorEntrySchema` used to - refuse `triggers` on a provider-bound instance, reasoned "the provider derives them - from the upstream at boot" — untrue, since no provider ever derived a trigger. The - tombstone refuses every value on every carrier, so that rule became unreachable and - was deleted rather than re-reasoned; a provider-bound instance now meets the - retirement prescription. -- **The def leaves whole** (`RETIRED_DEFS_BY_MAJOR[18]`: `integration/ConnectorTrigger`). -- **D2 conversion `connector-triggers-removed`** (step 18, retired from the load path): - strips the array from `connectors[]` and from stored `sys_metadata` connector rows - (the rehydration seam replays it), one notice per connector, as a lossless delete. - A trigger is stripped, never turned into a flow. -- **The chain.** In the same step, `connector-health-and-trigger-durations-unit-in-key` - renamed `triggers[].interval` to `intervalSeconds`. That trigger half is absorbed by - this removal, as its breaker half already was by the `health` removal, so with neither - half left the rename conversion is gone from the table and from step 18; an author - holding either spelling ends with no `triggers` at all. The retired-key row - `integration/ConnectorTrigger:interval` stays as the record. -- **D3 entry `connector-triggers-retired`** carries the family's judgement: which - triggers should exist now as flows, the cadence in seconds, and whether an external - sender can sign the calls a signed `api` flow requires. The absorbed rename's own D3 entry - (`connector-resilience-durations-unit-in-key`) is gone with its - conversion. -- **No deprecation window**, per the project's startup-stage posture. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is -published, so this is breaking for consumers no telemetry was consulted for. - -Clause-②: no (narrowing) - - diff --git a/.changeset/20288-realtime-event-type-emitted-vocabulary.md b/.changeset/20288-realtime-event-type-emitted-vocabulary.md deleted file mode 100644 index eb57bd5631b..00000000000 --- a/.changeset/20288-realtime-event-type-emitted-vocabulary.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: `RealtimeEventType` names the realtime events the runtime emits — `data.record.*` and `data.records.*`; `record.created` / `record.updated` / `record.deleted` / `field.changed` retired (#20288) - -Clause-②: yes (narrowing) - -**BREAKING** — shipped as `minor` under the launch-window convention -(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by -this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, -never by the level). The enum both gains and loses values: it gains the five -names the runtime emits and loses the four it never emitted. - -`RealtimeEventType` (`@objectstack/spec/api`) types `SubscriptionEvent.type`, so -it is the event vocabulary of `Subscription.events[]` and -`RealtimeConfig.subscriptions[].events[]`, and the generated API reference -published it as such. None of its four values was ever emitted by anything. The -ObjectQL engine publishes `data.record.created` / `data.record.updated` / -`data.record.deleted` for each written record and `data.records.updated` / -`data.records.deleted` for a predicate write (`multi: true`), and it parses every -event through `DataEventSchema` / `BulkDataEventSchema` before publishing. A -subscription written with the names the reference showed could never fire. -The runtime's published event names do not change; the enum moves to them. - -- **Accepted now:** exactly `DataEventType` + `BulkDataEventType` — - `data.record.created`, `data.record.updated`, `data.record.deleted`, - `data.records.updated`, `data.records.deleted`. Metadata change events - (`metadata.{type}.{action}`) are not members: a subscription event is - record-shaped, and metadata events keep their own `MetadataEventType` contract - and `subscribeMetadata` client primitive. -- **Refused now:** `record.created`, `record.updated`, `record.deleted` and - `field.changed`, each with its own prescription. Any other unknown value keeps - zod's own message, which lists the legal names. - -``` -FROM SubscriptionSchema.parse({ id, transport: 'websocket', events: [{ type: 'record.created', object: 'account' }] }) - -> parsed; nothing ever published 'record.created', so the subscription never fired -TO -> ZodError at events[0].type (invalid_value): `record.created` was removed from - `RealtimeEventType` in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — … Write - `data.record.created` — the same event, spelled the way the engine publishes it. - -FROM { type: 'record.updated' } -> TO { type: 'data.record.updated' } - (add { type: 'data.records.updated' } to also hear predicate writes, which carry a count, not records) -FROM { type: 'record.deleted' } -> TO { type: 'data.record.deleted' } - (add { type: 'data.records.deleted' } to also hear predicate writes) -FROM { type: 'field.changed' } -> TO { type: 'data.record.updated' } - (read the field from the DataEvent payload's `changes`; no event is published per field) -``` - -**Fix.** Rename each value as mapped above. `tsc` refuses the old values at a -`RealtimeEventType` / `SubscriptionEvent` position (the type no longer contains -them), and a `SubscriptionSchema`, `SubscriptionEventSchema` or -`RealtimeConfigSchema` parse refuses them with the prescription. A handler keyed -on an old name never ran, so the rename changes behaviour only by making it fire. -Nothing in the open framework parses a subscription today — it mounts no realtime -transport — so no running deployment changes behaviour. - -### The kit - -- **Schema.** `RealtimeEventType` lists the five emitted names; its error map - gives each retired value its own prescription (the `HookBodyCapability` - precedent for a removed enum value). `realtime.test.ts` pins the enum equal to - `DataEventType` + `BulkDataEventType`, and pins each refusal's code, path and - first sentence through `SubscriptionSchema`. -- **ADR-0087.** The D3 entry `realtime-event-type-unemitted-values-retired` - carries the prescription to `os migrate meta` and the upgrade guide. No D2 - conversion and no `RETIRED_KEYS_BY_MAJOR` row: an enum value is not a key, and - a subscription is neither a stack collection member nor a stored row, so no - conversion seam would ever see one. -- **Docs.** The `realtime` reference page is regenerated; the - `RealtimeEventPayload.type` and `RealtimeEvent.type` examples name emitted - events. - - diff --git a/.changeset/20288-service-realtime-emitted-event-examples.md b/.changeset/20288-service-realtime-emitted-event-examples.md deleted file mode 100644 index 7c7a1519221..00000000000 --- a/.changeset/20288-service-realtime-emitted-event-examples.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/service-realtime': patch ---- - -docs(service-realtime): the README and `InMemoryRealtimeAdapter` examples name the events the ObjectQL engine actually publishes (#20288) - -The usage example subscribed to and published `record.created`, and the security -posture section said the engine publishes `record.created` / `record.updated`. -The engine publishes `data.record.created` / `data.record.updated` / -`data.record.deleted` (and `data.records.updated` / `data.records.deleted` for a -predicate write), with the spec's `DataEvent` as the envelope's `payload` — the row -is `payload.after`, not the payload itself. Both examples now subscribe to -`data.record.created` and publish that shape. No code changes: the adapter matches -whatever event type a subscription names, exactly as before. diff --git a/.changeset/20300-cube-member-inner-name-retired.md b/.changeset/20300-cube-member-inner-name-retired.md deleted file mode 100644 index b1d632e5c7d..00000000000 --- a/.changeset/20300-cube-member-inner-name-retired.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-analytics': patch ---- - -**BREAKING** — the inner `name` on an analytics cube's measures and dimensions (`MetricSchema.name`, `DimensionSchema.name`) is now refused at parse: nothing ever read it. The record key a member is declared under IS its name — the analytics API publishes it as `.` and a query names it that way. Delete the inner `name`; to rename a member, rename its key. - -Clause-②: no (narrowing) - -`measures` and `dimensions` are records, and the key was always the member's identity: `GET /api/v1/analytics/meta` publishes every member as `${cube.name}.${key}` (in `@objectstack/service-analytics` and in `@objectstack/driver-memory`), and both SQL strategies and the in-memory driver resolve a member by indexing the bag with that key. Measured before removal, with a lit control: zero reads of a member's inner `name` in non-test source, against four reads of the neighbouring `measure.label` / `dimension.label` in the same two `getMeta` projections. So the inner `name` was a REQUIRED second copy of the identity that nothing read — and one that disagreed with its key was silently ignored (this repository's own in-memory driver fixtures authored `totalAmount: { name: 'total_amount', … }` and queried `orders.totalAmount`). - -**Removed rather than enforced** (ADR-0049 enforce-or-remove; the triage verdict on the card, by the maintainer's criterion for declared-but-unenforced families): Cube.dev and LookML key a member by its declared name, with no second inner name that can disagree — and here the record key already delivered it. - -## FROM → TO - -| you wrote (17.4 and earlier) | write instead | -| --- | --- | -| `measures: { total_amount: { name: 'total_amount', label: 'Total', type: 'sum', sql: 'amount' } }` | `measures: { total_amount: { label: 'Total', type: 'sum', sql: 'amount' } }` | -| `dimensions: { status: { name: 'status', label: 'Status', type: 'string', sql: 'status' } }` | `dimensions: { status: { label: 'Status', type: 'string', sql: 'status' } }` | -| an inner `name` that DIFFERS from its key, e.g. `totalAmount: { name: 'total_amount', … }` | nothing changes at runtime — `orders.totalAmount` was already the name every query used. Delete the inner `name`, or, if `total_amount` is the name you meant, re-key the member and update every query, dashboard and report that names `orders.totalAmount` | - -**The one-line fix:** delete `name` from every metric and dimension; the key it is declared under is its name. - -**What an author who still writes it sees.** `tsc` fails at the authoring site (`Metric` / `Dimension` type the key `never`), and the parse — `defineCube()`, `defineStack({ analyticsCubes })`, `PUT /api/v1/meta/analytics_cube/:name` — refuses it at `measures..name` / `dimensions..name` with the prescription: - -> `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. … Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. - -`os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. - -## The retirement kit - -- **`retiredKey()` tombstones, not a bare deletion** — even though both member shapes are `strictObject`s (the `action.aria` posture). A bare delete would still be loud, but only as a generic unrecognized-key report that cannot carry the prescription; the tombstone types the key `never` for `tsc` and raises the upgrade text at parse. The keys therefore stay in the walked shape: both liveness rows stay `dead` with a `REMOVED` note, and the authorable-surface baseline marks `data/Metric:name` and `data/Dimension:name` `[RETIRED]`. -- **The D2 conversion `cube-member-inner-name-removed`** (protocol 18, retired from the load path) deletes the inner `name` from every metric and dimension of every `analyticsCubes[]` entry. It is owed because the key was REQUIRED, so every stored or built cube carries it. It strips a disagreeing value too — the key already won everywhere, so no query or discovery answer changes — and its notice prints both spellings (`from: name "total_amount"`, `to: (removed; the record key "totalAmount" is the name)`). Its D3 record is the semantic entry `cube-member-inner-name-retired`, which asks the author of a disagreeing name which spelling they meant. -- **The producers stop writing it** (`@objectstack/service-analytics`): the dataset compiler (`compileDataset`), `CubeRegistry.inferFromObject` and the ad-hoc query mint no longer put an inner `name` on the members of the cubes they build. The members are filed under the same keys as before, so `/analytics/meta`, `/analytics/query` and `/analytics/sql` answer exactly as they did. The package README's cube example is corrected. -- **The `measures` / `dimensions` descriptions now say it**: "keyed by metric name: the record key IS the metric's name, published and queried as `.`". - -## Reach, measured - -- This repository, non-test: the showcase cube (`examples/app-showcase`, 8 members), the published `objectstack-ui` skill's `defineCube` example (6), the `service-analytics` README (3), and the three internal cube mints above — every one wrote the inner `name` EQUAL to its key, and all are corrected here. Test fixtures: about 300 member literals and map-built members across 84 test and fixture files in eight packages, all EQUAL to their key except 21 in `driver-memory`, which disagreed (camelCase key, snake_case inner name) and were already queried by key. -- Out-of-repo authors: NOT MEASURED. - -## What an operator with a STORED cube sees - -A `sys_metadata` `analytics_cube` row or a built artifact written before this release carries the inner `name` on every member. Nothing breaks at read: the conversion replays on rehydration and at the artifact door and strips it, so the cube is served canonical and parses. `os migrate meta --stored --apply` rewrites the rows. - - diff --git a/.changeset/20305-inline-row-filter-converts.md b/.changeset/20305-inline-row-filter-converts.md deleted file mode 100644 index 3ba18bd99e8..00000000000 --- a/.changeset/20305-inline-row-filter-converts.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): the stored-filter conversion rewrites a filter on a block whose rows are inline, as it does on any other block - -The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` no longer leaves every filter of a page component whose rows are inline (`data: { provider: 'value', … }`, a `data` array, or `staticData`) as stored. Such a filter, the binding's `dataSource.filter` included, is now rewritten to the `[{ field, operator, value }, ...]` rule array exactly as it is on a block that queries an object. What still stays as stored, and is still reported as a TODO, is only a filter with a part that has no lossless rule spelling: a combinator, a null value, or an operator the rule vocabulary does not spell. That holds on any block. - -Why the conversion declined, and why it no longer needs to: the `object-map`, `object-tree`, `object-calendar` and `object-gantt` blocks match that filter against their own rows in objectui's in-memory data source (`ValueDataSource.find`). The conversion was written against an objectui version whose `find` excluded every row for a rule array, so it left those filters alone and said so in the TODO. The objectui version this repository pins (`.objectui-sha`, the same pin the previous release shipped) lowers a rule array before it matches, and it selects the rows the stored form selected. That was measured over every operator the conversion maps: 114 filters on eight rows, null and missing values included. The same filters select no row on the objectui build just before that fix. So the decline was already protecting nothing: it only left convertible filters unconverted and reported TODOs that no longer needed to exist. - -What an operator sees: - -- `os migrate meta --stored` now lists such a page as a pending rewrite. It used to list it as a `skipped` row with a TODO. A preview over a database whose only legacy filters sat on inline-row blocks therefore exits 1 until `os migrate meta --stored --apply` rewrites them. -- Until then, every stored-row read replays the same rewrite, so the block reads the rule array and shows the same rows. -- Nothing an author writes is accepted or refused differently. The conversion stays retired from the authoring path, and no schema changes. - -The migration entries `element-data-source-and-object-block-filter-rule-array` and `object-grid-default-filters-rule-array`, and the protocol-18 step rationale, no longer say that inline-row filters are left as stored. - -ADR-0087 disposition: already registered. This changes the behaviour of the registered D2 conversion `page-component-filter-record-to-rule-array` and edits its two D3 entries. There is nothing new to register. - -Clause-②: no diff --git a/.changeset/20312-save-door-compiles-html-page-source.md b/.changeset/20312-save-door-compiles-html-page-source.md deleted file mode 100644 index 8a87b3abe86..00000000000 --- a/.changeset/20312-save-door-compiles-html-page-source.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/cli': minor ---- - -`os serve` hands the runtime metadata save door the deployment's SDUI component manifest, and the save door compiles an html page's `source` against it: an unknown component or a `requires` that disagrees with the source is refused with a `422`, and `requires` is stamped from the compiled source (ADR-0080 §5). - -Clause-②: yes (narrowing — on a host that registers a manifest, the runtime metadata save door newly refuses an html page whose source uses a component the manifest does not declare, or whose `requires` disagrees with its source; the new exported `SDUI_MANIFEST_SERVICE` widens `@objectstack/metadata-protocol`) - - - -**BREAKING** — an accept-set narrowing on the runtime metadata save door, shipped -as `minor` under the launch-window convention (`check-changeset-no-major` refuses -`major` until GA; breaking-ness is carried by this banner and the ADR-0087 -disposition above, not by the level). On a server that has a manifest, a -`PUT /api/v1/meta/page/NAME` (and the draft publish) of a `kind: 'html'` page used -to store the source unjudged; it now answers `422 INVALID_METADATA` when the source -uses a component the deployment's console does not provide, naming the component in -each issue's `where` and `message`, or when a hand-written `requires` lists a -namespace the source does not use, omits one it does, or names one no component in -the manifest carries. **One-line fix:** use a component the manifest declares (or -install the plugin that provides it in the console the deployment serves), and omit -`requires` — it is derived from the source. - -**The channel.** `@objectstack/metadata-protocol` exports `SDUI_MANIFEST_SERVICE` -(`'sdui-manifest'`), a plain service key. `os serve` resolves the manifest once at -boot through the same resolver `os validate` uses — the project's own -`sdui.manifest.json` beside the served config, then the copy `@objectstack/console` -ships — and registers it under that key. The save door reads the key on every -publish, so a host that registers or replaces it later is seen by the next save. - -**The compile.** The save door runs `@objectstack/sdui-parser`'s `compile()`, the -compiler behind `os validate`'s JSX page gate, against the registered manifest. Its -diagnostics carry the same rule ids the CLI reports (`jsx-forbidden-tag`, -`jsx-unknown-component`, …); errors refuse the publish, warnings ride the response's -`advisories`. A disagreeing `requires` is refused under -`page-requires-disagrees-with-source`. A page that compiles is stored with the -`requires` its source yields, on a draft save too; a draft that does not compile, or -whose `requires` disagrees, is stored as written (drafts are not gated) and its -publish refuses it. - -**Without a manifest nothing changes.** A host that resolves no manifest registers -nothing and prints one boot line — `Page source and \`requires\` not validated at -save`, naming every place looked — and the save door stores html pages exactly as -before. A registered value with no `components` map is warned about once and never -compiled against. - -Measured before the refusal shipped: the three html pages in this repository -(`examples/app-showcase`: `showcase_capability_map`, `showcase_command_center_jsx`, -`showcase_start_here`) all compile against the pinned console's manifest with no -diagnostic and yield `requires: ['ui']`; none authors `requires`. diff --git a/.changeset/20313-use-grouping-live.md b/.changeset/20313-use-grouping-live.md deleted file mode 100644 index 9bb1dcd37cb..00000000000 --- a/.changeset/20313-use-grouping-live.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The `field` liveness ledger grades `useGrouping` `live`, and the key's docblock stops describing a grouping heuristic the renderer does not use - -Clause-②: no - -A number field's authored `useGrouping` is honoured by the console this release builds against: -an authored `true` or `false` decides whether the value renders with thousands separators, and an -absent key keeps the renderer's interim rule. The `liveness/field.json` row therefore moves from -`planned` to `live`, citing the objectui reader and the sites that carry the key to the number -cell, and `liveness/state-counts/field.md` is regenerated to match. The `FieldSchema.useGrouping` -docblock in `src/data/field.zod.ts` said the interim rule looked at a field's `min` / `max` -bounds, and that the renderer half had not landed; both are corrected: the rule reads only a -declared `scale: 0` (ungrouped) against any other `scale` or none (grouped), and the renderer half -reads an authored value first. Text and ledger only: the schema, its `.describe()` string, every -export and all runtime behaviour are unchanged. diff --git a/.changeset/20318-automation-result-flow-label.md b/.changeset/20318-automation-result-flow-label.md deleted file mode 100644 index 3d50ecd181b..00000000000 --- a/.changeset/20318-automation-result-flow-label.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-automation': minor ---- - -feat(spec,service-automation): a flow run's result carries the flow's authored label as `flowLabel` (#20318) - -Clause-②: yes (widening) - -**The widening.** `AutomationResult` (`@objectstack/spec/contracts`) gains one -optional member, `flowLabel?: string`, and `TriggerFlowResponseSchema` -(`@objectstack/spec/api`) mirrors it on `data`. The automation engine sets it to -the flow definition's `label`, copied verbatim, the same way it copies -`successMessage` and `errorMessage`. Nothing is removed or renamed, and no -existing member changes meaning. - -**Why.** A flow runner names the flow it is running, in its header and in its -completion toast, and translates that name against the `flows..label` -translation key, falling back to the authored label. The runner only held the -flow's API name, so there was no authored label to fall back to. The console's -reader of the translation key is a separate change. - -**Which results carry it.** - -- **Set** on every result of an evaluation of a registered flow: `status: 'paused'` - (first attempt, retry attempt, a resume that pauses again), a terminal success - (including the two skip exits), `'failed'` (including an exhausted retry budget), - `'stranded'`, `'refused'`, and a resumed parent whose delegated child failed. -- **Absent** on every refusal that carries a `code` (the run never dispatched, or a - resume never continued it) and when the flow is not registered. -- **Subflow chains** answer with the label of the run the caller addressed, which - is the parent. The child that supplied the screen does not lend its label. -- **Never the API name.** `FlowSchema` requires `label`, so the value is always - what the author wrote, an empty string included. - -**At the wire.** Both runner doors relay the result verbatim on a `200`, so -`data.flowLabel` arrives on `POST /api/v1/automation/:name/trigger` (a paused or -finished launch) and on `POST /api/v1/automation/:name/runs/:runId/resume` (a -further pause or the completion). A `400 FLOW_FAILED` answer is unchanged: its -`error.details` keep their fixed set (`errorMessage`, `summary` and, on resume, -the stranded verdict), with no `flowLabel`. - -**For a consumer.** A client that parses the trigger response with -`TriggerFlowResponseSchema` now keeps `data.flowLabel`, where an undeclared key -would have been stripped. A caller that deep-compares a whole `AutomationResult` -from `execute()` or `resume()` sees one more key on the results listed above. diff --git a/.changeset/20431-explain-cross-class-refusal.md b/.changeset/20431-explain-cross-class-refusal.md deleted file mode 100644 index 93d015f2581..00000000000 --- a/.changeset/20431-explain-cross-class-refusal.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): `security/explain` answers with enforcement's refusal for a row-level policy that compares two fields of no shared comparison class, instead of a record verdict (#20431) - -Clause-②: no - -A row-level policy can compare two fields that share no comparison class: text against a number, or any field against a file field, a formula field, or a field that holds a list or an object. The platform defines no answer for such a comparison. The SQL driver refuses to compile it, so every find the policy scopes answers `INVALID_FILTER` / 400. A by-id update or delete fails closed at its row-level gate, because that gate's pre-image read is the same refused read. - -The explain engine's record attribution judged the same predicate in-process, without the object's declared columns. So it compared the two raw values, and it reported `record.visible` as `true` or `false` depending on how those values happened to compare. For one ordering of a pair, it reported the record visible where enforcement refuses the read. - -The record matcher now receives the object's declared columns, as the RLS write check already does, and it refuses the comparison the way the driver does. A record-grained explanation (`recordId`) under such a policy is now refused with the matcher's envelope: `INVALID_FILTER` / 400, the same envelope the find answers with. No record verdict is reported. The message names the policy and both fields with their declared types. This is the answer explain already gives to the matcher's other `INVALID_FILTER` refusals, including a field-to-field comparison against a field that holds a list. Both orderings of one pair now get this one answer. - -Unchanged: - -- Enforcement admits and refuses exactly what it did before. -- A comparison between two fields of one class keeps its record verdict. -- A schema that cannot be read hands over no columns, so the matcher judges values only, as before. -- An object-level explanation (no `recordId`), which runs no record matcher. diff --git a/.changeset/20437-turso-forced-replica-needs-sync-url.md b/.changeset/20437-turso-forced-replica-needs-sync-url.md deleted file mode 100644 index 575aec30e5e..00000000000 --- a/.changeset/20437-turso-forced-replica-needs-sync-url.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/driver-turso': minor ---- - -fix(spec,driver-turso)!: a turso config that forces `mode: 'replica'` with no `syncUrl` is refused where it is written and when the driver is built, instead of running as a plain local database that never syncs - -Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves. - -An embedded replica is a local file kept in sync with the remote named in `syncUrl`. A config that forced `mode: 'replica'` on a `file:` url with no `syncUrl` (or an empty one) was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the built driver before this change, with and without `sync`: it constructed with `transportMode` `'replica'`, `isSyncEnabled()` answered `false`, no sync interval started, the sync call did nothing, and every read and write went to the local file. A datasource declared as a replica ran as a plain local database that never replicated, with no error and no warning. - -**BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out: - -- **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service; -- **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened. - -The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. The sibling refusals keep their order. A forced replica on a remote url, an in-memory url or a bare path still meets its `url` refusal first. One with `sync` and no `syncUrl` still meets the `sync` refusal first; the schema now reports the `mode` issue beside it. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it. - -### Migration: FROM → TO - -| You wrote | Write instead | -| --- | --- | -| `url: 'file:./data/replica.db', mode: 'replica'` (no `syncUrl`, or `syncUrl: ''`) | an embedded replica: keep the `file:` url and name the remote, `syncUrl: 'libsql://my-db.turso.io'` | -| the same | a plain local database: drop `mode` (`url: 'file:./data/app.db'` alone) | - -A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and ran as a local database. The way out is the table above. - -Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero. - - diff --git a/.changeset/20446-empty-joins-filter-operators.md b/.changeset/20446-empty-joins-filter-operators.md deleted file mode 100644 index 3f4ec047967..00000000000 --- a/.changeset/20446-empty-joins-filter-operators.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/driver-memory': minor -'@objectstack/service-analytics': patch ---- - -feat(spec)!: `$empty` joins `FILTER_OPERATORS`, and the view operators `is_empty` / `is_not_empty` lower to it (#20446) - -A stored 「is empty」 / 「is not empty」 — `['field', 'is_empty', …]`, `isempty`, `is_not_empty`, `isnotempty`, in a view rule, a sharing rule or any filter array — now lowers to `{ field: { $empty: true | false } }` instead of `$null`. `$empty` is answered by the field's DECLARED type: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. So an 「is empty」 rule on a text field now also finds `''`, and on a multi-value field also finds `[]`, which the `$null` lowering missed. `is_not_empty` is its exact complement. `$empty` is in `FILTER_OPERATORS` (and `ALL_OPERATORS`) now, and `canonicalAstOperator` folds the empty pair onto `is_empty` / `is_not_empty` rather than onto `is_null` / `is_not_null`. On `@objectstack/driver-memory`, a QueryAST comparison node (`{ type: 'comparison', operator: 'is_empty' }`) is answered by the same declared-type arm. - -**BREAKING**: two things accepted before are refused now, each loudly and with its fix. - -- **A `{ $empty: … }` object written as a field value** (a `where` pasted into an insert or update payload) is refused with `VALIDATION_FAILED` (`invalid_type`, "$empty is a filter operator, not a value"). Before, a text-like field stored it as data. - FROM `update('task', { title: { $empty: true } })` → TO write the value itself (`{ title: '' }`, `{ title: null }`); a filter belongs in `where`. -- **`is_empty` / `is_not_empty` where no face holds the column's declared type** is refused with `INVALID_FILTER` / 400 (`READ_SCOPE_COMPILE_FAILED` / 500 on an analytics read scope). The `$null` lowering answered these. The compositions: - - the built-in `id`, which no object declares. FROM `['id', 'is_empty', true]` → TO `['id', 'is_null', true]` / `is_not_null`; - - a federated (external) object on a driver that does not implement `registerExternalObject` (driver-memory, driver-mongodb). The boot already reports such an object as NOT bound to its remote table, naming it, and its reads answered from a table named after the object. FROM `is_empty` on such an object → TO bind it on a driver that implements federation (driver-sql and its heirs, driver-turso); - - an `AnalyticsService` constructed without `sourceFieldMeta`. FROM such a host → TO pass `sourceFieldMeta` (the package README shows it), or filter with `is_null` / `is_not_null`; - - a multi-value column on a SQL dialect `driver-sql` does not model (a knex client other than SQLite, PostgreSQL or MySQL). FROM `['tags', 'is_empty', true]` there → TO `['tags', 'is_null', true]` / `is_not_null`. - -Stored sharing rules and views that use 「is empty」 are not rewritten; they are re-read under the new meaning. Production rules that use 「is empty」 on a text or multi-value field were not measured; each finds more rows (the `''` / `[]` ones) from this release. - -Clause-②: yes (narrowing) - - diff --git a/.changeset/20476-define-stack-conversions-reach-doors.md b/.changeset/20476-define-stack-conversions-reach-doors.md deleted file mode 100644 index 8b392c8a865..00000000000 --- a/.changeset/20476-define-stack-conversions-reach-doors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/cli": minor ---- - -**`objectstack validate` and `objectstack build` now report the ADR-0087 conversions `defineStack` applied, and `objectstack validate --strict` fails on them.** - -`defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, in either mode, and prints one `defineStack: PATH: 'OLD' → 'NEW' (converted at load; conversion 'ID', retires in protocol N)` line on stderr. The two commands received that already-converted stack, so their own conversion pass found nothing to convert: `--json` answered `conversions: []` for every `defineStack` config, and `objectstack validate --strict` exited 0 on a spelling that stops loading in a named protocol major. A CI job gating on either could not see the retirement coming. - -- `@objectstack/spec`: `defineStack` (both modes) records every conversion notice it applied on the stack it returns, beside the provenance mark and stamped in the same act. The record is non-enumerable and frozen, so the schema, `Object.keys` and `JSON.stringify` never see it and no compiled artifact changes. `composeStacks` records its inputs' records in input order, counting the same built stack passed twice once. **New export:** `stackConversionsOf(value)` returns the `ConversionNotice[]` a producer recorded, the same element the commands' `conversions` field publishes, and `[]` for a value no producer returned. Like the mark, the record does not survive a spread or JSON copy. -- `@objectstack/cli`: the config loader reads the record off the default export before it merges named exports into it (that merge is a spread, which drops the record as it drops the mark). `objectstack validate` and `objectstack build` add it to their `conversions` list right after the config loads. Their own conversion pass still runs, and still reports what it converts on a key merged in from a named export of the config module, which `defineStack` never saw. The `--json` envelope keeps its shape (`valid` / `success`, `errors`, `warnings`, `conversions`): `conversions` now lists each conversion once. - -**What a CI job sees:** `objectstack validate --strict` and `objectstack validate --json --strict` now exit 1 for a config whose only advisory is a live conversion, which is what `--strict` ("treat warnings as errors") documents. Without `--strict` the exit stays 0. The fix is the one the notice names: author the canonical spelling it prints, for example `subtitle` instead of `description` on a `page:header` component. The text face lists the conversion in its warning block. The stderr line from `defineStack` is unchanged and still printed once per conversion. - -Clause-②: yes (widening) — one new export, `stackConversionsOf`, on the spec package root. Nothing `objectstack build`, or `objectstack validate` without `--strict`, accepted before is refused. `--strict` now applies its documented meaning to the conversions a `defineStack` config carries. It does not add a new rule. diff --git a/.changeset/20510-aggregated-column-refusal-words.md b/.changeset/20510-aggregated-column-refusal-words.md deleted file mode 100644 index a25a800ca63..00000000000 --- a/.changeset/20510-aggregated-column-refusal-words.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -"@objectstack/spec": patch -"@objectstack/objectql": patch ---- - -The number-comparand refusal now says "a numeric aggregated column" at `having`, and names PostgreSQL's server error only where a driver actually binds the comparand - -Clause-②: no - -**Two false phrases, at two positions.** At `having`, filtering a `count` / -`sum` / `avg` result (or a groupBy column) against a non-numeric comparand -answered `filter on 'total' compares a declared number field …` — `total` is -the aggregated row's own column, not a declared field of the object; the -verdict is handed the numeric class the column belongs to, which has no -`FieldType` of its own. And at `having` and the per-aggregation `filter`, the -`not-a-number`, `boolean` and `date` clauses each named "(PostgreSQL with a -server error)", a fact about `where`: the engine evaluates both of those -clauses itself, on every driver, before any row is read, so a comparand there -never reaches a driver bind and PostgreSQL never answers it. - -**Measured, unchanged: the per-aggregation `filter`'s column IS a declared -field.** That position narrows the object's RAW rows before any aggregation -runs, against the object's real field map — so its refusal keeps "a declared … -field", exactly as `where`'s does. Only the PostgreSQL clause moves there, -because the engine evaluates that position itself too. - -**FROM** `filter on 'total' compares a declared number field against "abc" at -having.total.$gt, which is not a number: it has no numeric reading, and -backends answer it differently (PostgreSQL with a server error). …` - -**TO** `filter on 'total' compares a numeric aggregated column against "abc" -at having.total.$gt, which is not a number: it has no numeric reading. …` - -The `where` message is unchanged, byte for byte, and so is the accept set: no -comparand that was refused before is now accepted, and none that passed is now -refused. This is a wording fix. - -**What moved to carry it.** `NumberComparandRefusalSite` (`@objectstack/spec`) -gains two optional fields the engine door already knew and now passes along: -`aggregated` (the column is an aggregated-row column, not a declared field — -`having` sets it; `where` and the per-aggregation `filter` do not) and -`boundByDriver` (this position reaches a live driver bind — `where` alone sets -it true; unset defaults to `true`, so a site built before this change, or any -caller who never sets these fields, renders exactly as it always has). -`@objectstack/objectql`'s door passes both explicitly at each of its three -call sites; no second rule and no driver-level change. diff --git a/.changeset/20513-driver-sql-runtime-strings-state-the-decision.md b/.changeset/20513-driver-sql-runtime-strings-state-the-decision.md deleted file mode 100644 index e423f02de83..00000000000 --- a/.changeset/20513-driver-sql-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -driver-sql refusals, drift reports and log lines no longer cite tracker numbers; each states the reason in words - -Clause-②: no - -Many messages the SQL driver shows to authors and operators ended with an issue-tracker number where -the reason belonged. The number goes, and where the sentence did not already say what was decided, it -now does: - -- Filter refusals (`INVALID_FILTER`): the withheld-detail wording ("withheld from the message; the full - diagnostic is in the server log"), the JSON-column, zero-operator, `$null` / `$exists`, undefined - comparand and unknown-combinator refusals, and the filter-array refusal. -- Schema and index messages: the `reference_to` DDL refusal (the FOREIGN KEY DDL that key used to gate - is retired, because it could never fire for a spec-conformant lookup), the MySQL TEXT-key and - row-size explanations, the hash-shadow UNIQUE messages, and the `os migrate plan` drift entries. -- The NULL-safe UNIQUE messages now say why rows without an organization were never constrained: SQL - UNIQUE is NULL-distinct. -- Boot log lines for the SQLite datetime, time and json canonicalisation and the MySQL `TIMESTAMP` / - `TIME` widening now say what the conversion is for. - -Text only: no error code, field name, status or behaviour changes. Three aggregate refusals keep their -citation for now, because a test in `@objectstack/driver-turso` compares them byte for byte with the -Turso remote transport's copies; they change together with those copies. diff --git a/.changeset/20513-driver-turso-runtime-strings-state-the-decision.md b/.changeset/20513-driver-turso-runtime-strings-state-the-decision.md deleted file mode 100644 index 1f9a348068b..00000000000 --- a/.changeset/20513-driver-turso-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/driver-turso': patch -'@objectstack/driver-sql': patch ---- - -driver-turso refusals and log lines, and driver-sql's last three aggregate refusals, no longer cite tracker numbers; each states the reason in words - -Clause-②: no - -Many messages the Turso driver shows to authors and operators ended with an issue-tracker number where -the reason belonged. Most of the Turso remote transport's numbers were bare ids from the repository that -file used to live in, so here they pointed at unrelated cards. The number goes, and where the sentence -did not already say what was decided, it now does: - -- Aggregate refusals, on both drivers: the undeclared-function, `count_distinct`-without-`field` and - per-aggregation `filter` refusals lose their citation on the SQL driver and the Turso remote - transport together, so the two faces still read one sentence. The remote transport's - declared-but-uncompiled and date-bucket refusals lose theirs too, and read exactly like the SQL - driver's again. -- Turso remote filter refusals (`INVALID_FILTER`): the withheld cross-field and unbindable-comparand - wording, and the full diagnostics behind every filter refusal (unsupported operator, unlowered - `$between`, undeclared or non-list combinator, non-node operand, non-object `where`, empty operator - map, undefined comparand, non-boolean `$exists`) lose only the citation, because their sentences - already said it. The non-boolean `$null` diagnostic now says every driver refuses it, so one filter - no longer gets a different answer per backend. -- The Turso remote `auto_number` refusal (`NOT_IMPLEMENTED`) now says why it refuses rather than - resolves: resolving would write NULL into the slot and persist the row without its record number. -- Log lines: the unnumbered-upsert warning loses its citation; the remote canonical backfill's info line - says what a conversion buys (the column drops the unindexable read-side repair only once a pass finds - nothing left to convert); the unresolvable-remainder warning says a value that cannot be read as an - instant is counted and reported, never guessed at. - -Text only: no error code, field name, status or behaviour changes. diff --git a/.changeset/20513-metadata-core-case-labels-state-the-case.md b/.changeset/20513-metadata-core-case-labels-state-the-case.md deleted file mode 100644 index c5d13115951..00000000000 --- a/.changeset/20513-metadata-core-case-labels-state-the-case.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/metadata-core': patch ---- - -The shared engine case tables and the published contract suites in metadata-core no longer cite tracker numbers in their case labels; each label states its case in words - -Clause-②: no - -Several labels these tables and suites ship ended with an issue-tracker number where the case belonged. A -test driven from them printed that number as part of its name, and a failing assertion quoted it as the -reason. The number goes; where the label did not already say what the case is, it now does. - -- `ENGINE_DELETE_DISPATCH_CASES`, `ENGINE_UPDATE_DISPATCH_CASES` and `ENGINE_FINDONE_PREDICATE_CASES`: - the `what` labels of 22 rows. Among them, the compare-and-set rows now say the by-id path would drop the - CAS guard; the payload-id rows say which declared `where.id` would be silently dropped; and the falsy - `where.id` boundary says it is a scalar, so neither the different-row refusal nor the non-scalar refusal - applies. -- `@objectstack/metadata-core/testing`: the repository contract suite's `serialized-form identity` group - title, and two `why` texts of `OBJECT_SCHEMA_MASK_CASES` (the empty-readable-set refusal, and the - write-capable exemption, which now names the schema write gate, `manage_metadata`). - -Text only: no case is added, removed or re-ordered, and no `options`, `data`, `expect`, `expectId`, `id`, -`readable` or `context` value moves. A suite that selects or skips these cases by their label text (a -`-t` filter, a skip list) needs the new spelling. diff --git a/.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md b/.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md deleted file mode 100644 index 663166eafc4..00000000000 --- a/.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -metadata-protocol refusals, hints and log lines no longer cite tracker numbers; each states the reason in words - -Clause-②: no - -Many messages the metadata protocol shows to authors, administrators and operators ended with an -issue-tracker number where the reason belonged. The number goes, and where the sentence did not -already say what was decided, it now does: - -- Refusals: `insertManyData` without an engine `insertMany` now names what that method is (the - partial-success batch insert, so a bad row neither fails the whole batch nor runs the good rows' - `beforeInsert` hooks twice); the unknown-metadata-type refusal says a plugin cannot declare a type - because `additionalTypes` was retired, having never been read; the stored non-canonical type - refusals on publish and revert say the `/meta` URL door now folds a type to its canonical spelling - before it writes, so such a row predates that. -- The schedule-flow `organization_id` hint says why the author's value is the only source: the engine - fills only an organization the run resolved, and a schedule resolves none. -- Log lines: the three `kernel:ready` "migration skipped" warnings now say what the migration that did - not run would have ensured; the history-counter abort says the old path took a failed read for an - empty table; the publish-closure degrade says the batch's own drafts are left out of the closure; - the cold-boot org-scoped audit calls the write refusal it points at declared-types-only. The - overlay, `sys_view_definition` and `sys_setting` index messages, the seed/API tenancy repair and its - receipt, the batch-row withhold and the object-existence gate's no-registry warning lose only the - citation, because their sentences already said it. -- The live-MySQL testkit's isolation error loses its citation. - -Text only: no error code, field name, status or behaviour changes. diff --git a/.changeset/20513-named-runtime-strings-state-the-decision.md b/.changeset/20513-named-runtime-strings-state-the-decision.md deleted file mode 100644 index 56e188f8e60..00000000000 --- a/.changeset/20513-named-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/service-automation': patch -'@objectstack/runtime': patch ---- - -Warnings, refusals and hints that cited a tracker number now say what was decided - -Clause-②: no - -Several runtime strings an author or operator reads sent the reader to an issue-tracker number for -the reason behind them. Each now states that reason in the sentence itself: - -- `@objectstack/objectql`: the two data-event warnings. A write that names no single record publishes - no per-record event rather than one with an empty `recordId`; a predicate (`multi: true`) write - publishes its own `data.records.*` event carrying the affected-row count and nothing else, so a - driver result that is not a count publishes no bulk event either. -- `@objectstack/service-automation`: the warning for a pausing node type that never declares - `resumeAuthority`, the generic-route resume refusal (its log line and its error text), and the - refusal of a suspension from a type that declares `supportsPause: false`. An undeclared - `resumeAuthority` resolves to `'service'` (fail-closed), so the generic resume route refuses those - pauses; guessing `'any'` is how a raw resume once walked past an approval decision no service had - recorded. -- `@objectstack/runtime`: the endpoint step's `NOT_IMPLEMENTED` message and its two hints (the - composed runtime always threads the policy context and the execution wiring, because execution is - reachable only past the policy chain), and the endpoint mapping refusals (the publish gate rejects - the same shapes, so a declaration that reaches the runtime check was stored without passing it). - -Text only: no error code, field name, status or behaviour changes. diff --git a/.changeset/20513-objectql-runtime-strings-state-the-decision.md b/.changeset/20513-objectql-runtime-strings-state-the-decision.md deleted file mode 100644 index dc22d334c56..00000000000 --- a/.changeset/20513-objectql-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -objectql refusals, log lines and metadata text no longer cite tracker numbers; each states the reason in words - -Clause-②: no - -Many messages the query engine shows to authors, administrators and operators ended with an -issue-tracker number where the reason belonged. The number goes, and where the sentence did not -already say what was decided, it now does: - -- Refusals: the bulk update and bulk delete row-scoping refusals now name the seed they are missing - (the AST seeded before the middleware chain, which RLS and sharing compose their row-scoping onto, - so a bulk write reaches only the rows the caller may edit); the hook-target rebind refusal says - why `delete()` stopped honouring a rebind (a handler that silently redirects which row gets - deleted is a trap) and names the `dispatchUnscopedMultiWrite` registration the whole-operation - dispatch goes to, on update and delete alike. The unknown-option, filter-array, - credential-aggregation, HAVING-operator, empty-hook-target, strict read-only and system-write - organization refusals lose only the citation, because their sentences already said it. -- Metadata text: the lifecycle `retention_overrides` setting description and the search companion - field description lose their citation. -- Log lines: the non-atomic cascade warning says a single-datasource cascade is now one - transaction; the system-ledger transaction line calls the ledger the one class carved out of the - cross-datasource write refusal; the dangling-reference audit summary says findings are reported, - never rewritten, because a system-context write is exempt from the write-time reference check; - the legacy `apiMethods` warning says the authorable values are the six primitives only, every - other operation being derived from them or retired; the two unevaluable-rule warnings say such a - rule fails closed and is never skipped. The ADR-0104 value-shape gate lines, the delegated - protocol-assembly line and the read-only and runtime-owned strip warnings lose only the citation. - -The `findOne` no-predicate refusal keeps its citation for now: `@objectstack/metadata-core` -carries a byte-identical copy that this package's tests compare against, and both move together. - -Text only: no error code, field name, status or behaviour changes. diff --git a/.changeset/20513-seven-packages-runtime-strings-state-the-decision.md b/.changeset/20513-seven-packages-runtime-strings-state-the-decision.md deleted file mode 100644 index 0d6ab05e81b..00000000000 --- a/.changeset/20513-seven-packages-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/driver-memory': patch -'@objectstack/driver-mongodb': patch -'@objectstack/formula': patch -'@objectstack/metadata': patch -'@objectstack/metadata-core': patch -'@objectstack/objectql': patch -'@objectstack/platform-objects': patch ---- - -Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words - -Clause-②: no - -Many messages these packages show to authors, administrators and operators ended with an issue-tracker -number where the reason belonged. The number goes, and where the sentence did not already say what was -decided, it now does. Where an ADR stood beside the number, the ADR stays. - -- Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals - (the contract refuses loudly and names the mismatch, never coerces a value into storability), the - kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB - filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` - source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the - function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate - refusal says the builder used to sum an unrecognised name before this refusal existed. -- The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s - `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. -- The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, - `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; - the sentence above it already says the driver refuses rather than run or answer unisolated. -- Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN - translations), the `sys_email` headers help and the SCIM credential store's protection reason. -- Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's - excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore - attestation info lines. - -Text only: no error code, field name, status or behaviour changes. diff --git a/.changeset/20534-import-date-cell-iso-real-day.md b/.changeset/20534-import-date-cell-iso-real-day.md deleted file mode 100644 index 86693a4d079..00000000000 --- a/.changeset/20534-import-date-cell-iso-real-day.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -'@objectstack/rest': minor ---- - -fix(rest): `POST /api/v1/data/:object/import` reads a `date`, `datetime` or `time` cell only in ISO 8601, the export's own `YYYY-MM-DD HH:mm:ss` or a year-first date (`2026/7/15`), on a calendar day that exists, and keeps a `date`'s year at four digits (#20534) - -Clause-②: no (narrowing) - -**BREAKING for callers of the import door.** A text cell for a `date`, -`datetime` or `time` field is now read only in one of these spellings, after -trimming: - -- `YYYY-MM-DD`; -- `YYYY-MM-DDTHH:MM[:SS[.fraction]]`, then `Z`, a `+HH:MM` / `-HH:MM` / - `+HHMM` offset, or nothing (a wall clock, read in the importing user's - business timezone, as before); -- `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no offset, which is what the export - writes for a `datetime` cell; -- a year-first date, `YYYY/M/D` or `YYYY-M-D` (a four-digit year, a one- or - two-digit month and day, the same separator twice), optionally followed by - one space and `H:MM` or `H:MM:SS` with no offset, read exactly as the - export shape is; -- for a `time` field, also a bare `HH:MM` / `HH:MM:SS`. - -The day must exist. Every other cell is that row's `invalid_date` error, with -the importer's existing sentence ("is not a valid date" / "datetime" / -"time"). The reader used to hand such a cell to the JavaScript date parser, -which read it in the SERVER PROCESS's timezone and month-first, and rolled an -impossible day into the next month, so the import reported success and stored -a different value. For each shape, change the cell FROM the refused spelling -TO an admitted one: - -- **An impossible day.** FROM `2026-02-30`, `2026-02-29`, `2026-04-31` in any - spelling (a `datetime` `2026-02-30` was stored as 2 March, and so was a - `date` written `2026-02-30T10:00:00Z`) TO the day you mean. Nothing is - rolled over. -- **A locale or prose date.** FROM `07/15/2026`, `07/15/2026 10:00`, - `07/08/2026`, `15 July 2026`, `Jul 15 2026 10:00` (stored hours apart on a - New York and a Shanghai server, a `date` a day apart, and `07/08/2026` read - as 8 July) TO `2026-07-15`, `2026-07-15 10:00`, `2026-07-08` or - `2026-08-07`. No timezone and no field order is guessed. Converting a - spreadsheet column to ISO (in Excel, the cell format `yyyy-mm-dd` or - `yyyy-mm-dd hh:mm:ss`) before export is the fix. -- **A year-first date outside its one form.** FROM a mixed separator - (`2026/7-15`) TO `2026/7/15` or `2026-07-15`. FROM a `T` or a zone on the - year-first form (`2026/7/15T9:00`, `2026/7/15 9:00Z`) TO `2026/7/15 9:00` - (a wall clock in the business timezone) or the ISO `2026-07-15T09:00:00Z`. - FROM a fraction of a second (`2026/07/15 10:00:00.123`) TO - `2026-07-15 10:00:00.123`. A two-digit year (`26/7/15`) is refused, as it - was. -- **A zone after a space, or lower-case `t` / `z`.** FROM - `2026-07-15 10:00Z`, `2026-07-15 10:00:00+08:00`, `2026-07-15t10:00:00z` TO - `2026-07-15T10:00Z`, `2026-07-15T10:00:00+08:00`, `2026-07-15T10:00:00Z`, - the spellings the create and update doors take. -- **A zone-naive `24:00`.** FROM `2026-07-15 24:00` or `2026/7/15 24:00` (read - in the server's zone) TO `2026-07-16 00:00` or `2026/7/16 0:00`. - `2026-07-15T24:00:00Z`, which names its instant, reads as before. -- **A number, reduced or expanded forms.** FROM a JSON number such as `2026` - or an Excel serial, `2026`, `2026-07`, `+002026-07-15` TO `2026-01-01`, - `2026-07-01`, `2026-07-15`. - -**Kept: year-first dates.** `2026/7/15`, `2026/07/15`, `2026-7-15`, -`2026/7/15 9:00` and `2026/08/01 06:00:00`, Excel's default short date in -zh-CN and ja-JP, stay admitted. They are now held to the same rules as every -other cell: the day must exist (`2026/2/30` is refused, never rolled into -March), the hour runs 0 to 23, and the day is stored in its padded ISO form -(`2026/7/15` is stored as `2026-07-15`). A year-first date with no clock -given to a `time` field reads as `00:00:00`, as an ISO day does; it used to -read the server's zone (`04:00:00` on a New York server). - -**The year keeps four digits.** A `date` cell for a year from 0001 to 0999 -(`0500-01-01`) used to leave the reader as `500-01-01`, which the write door -refuses, so the import refused a day the create door takes. It is stored as -written now. A bare day read into a `datetime` field in years 0001 to 0099 -(`0050-01-01`) used to be stored in the 1900s (`1950-01-01T00:00:00.000Z`); it -is stored in its own year now. - -**What is not affected.** Every ISO 8601 cell and every export-shape cell on -a real day reads exactly as before, including a zone-naive cell read in the -business timezone and an offset-bearing cell honoured as written. An xlsx -cell that Excel stores as a date is unaffected: it reaches the reader as the -export shape. The dry run answers the same verdicts as the real write. A -refused cell fails only its own row, and the rest of the batch imports as -before. The same narrowing applies to the exported `coerceRow` helper. - -**If you are refused.** The row's result carries `code: 'invalid_date'` and -quotes the cell, so the file can be corrected and re-imported. - - diff --git a/.changeset/20535-step18-rationale-fragments.md b/.changeset/20535-step18-rationale-fragments.md deleted file mode 100644 index dc8ab1b36e6..00000000000 --- a/.changeset/20535-step18-rationale-fragments.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -refactor(spec): protocol 18's migration step keeps its `rationale` as key-sorted fragments and derives its `conversionIds` — no value changes (#20535) - -Nothing a consumer reads changes. `MIGRATIONS_BY_MAJOR[18].rationale` (48,953 -characters), `MIGRATIONS_BY_MAJOR[18].conversionIds` (45 ids, same order) and the -whole `MIGRATIONS_BY_MAJOR` value are byte-identical to the previous release, and so -is the rationale `migrate meta` prints for the 17 → 18 hop. - -What changed is how the step is written, so two major-18 retirements can be in -flight at once without conflicting in `packages/spec/src/migrations/registry.ts`: - -- The rationale is `STEP18_RATIONALE`, one `{ id, order, text }` fragment per - retirement, kept sorted by `id` and rendered by `order`, joined with one space. - A retirement adds ONE fragment where its `id` (its D3 semantic entry id) sorts — - never at the end — with `order` one more than the highest present. -- `conversionIds` is read off `CONVERSIONS_BY_MAJOR[18]`, which it copied value - for value. A retirement adds its conversion there only. diff --git a/.changeset/20537-remote-skipped-index-durability.md b/.changeset/20537-remote-skipped-index-durability.md deleted file mode 100644 index c26b5954b88..00000000000 --- a/.changeset/20537-remote-skipped-index-durability.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/driver-turso': patch ---- - -fix(driver-turso): a declared index the remote face skips because a key column never materializes is logged at `error`, not `warn` - -Clause-②: no - -In remote mode (a `libsql://` URL), schema sync skips a declared index whose key column is not -a stored column: a name that is not a field of the object (a misspelling), or a virtual -`formula` field, which is computed on read and has no column. The skip itself is unchanged, since -DDL naming a missing column would fail the whole sync. It used to be reported through the -driver's `warn` diagnostics, so a skipped UNIQUE index left duplicates accepted while the log -said `warn`. - -The skip is now logged at `error`, on the same channel the remote face already uses for a -declared index it could not create, and the local face uses for the same skip. There is one -line per skipped index per sync. It names the object, the index and the missing column, says -whether the index was UNIQUE, states what is not enforced (duplicates for a UNIQUE index, a full -table scan for a plain one), and says how to fix it: make every key column a stored field of -the object, or remove the index. - -No DDL, accept set or refusal changes: the same indexes are created and the same ones are -skipped. diff --git a/.changeset/20539-stale-authored-object-leaves.md b/.changeset/20539-stale-authored-object-leaves.md deleted file mode 100644 index 4c1e9e75764..00000000000 --- a/.changeset/20539-stale-authored-object-leaves.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -fix(platform-objects): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20539) - -Clause-②: no - -A translated object leaf that a translator wrote by hand is kept as written -when its English source changes later, so some leaves went on saying what the -old source said. On a ja-JP, es-ES or zh-CN console the `sys_two_factor` record -page described `backup_codes` as JSON-serialized, where the English help says -the codes are one opaque ciphertext and not readable JSON. - -Nineteen leaves whose meaning contradicted the current English now match it: - -- all three locales: `sys_two_factor.backup_codes` help (an opaque ciphertext, - not JSON), the `sys_notification` description (one notification event per - `emit()`, not a per-user inbox entry), the `sys_job_run` description (job run - history, not an audit trail), and the `sys_metadata.environment_id` label - (Environment, not Project); -- es-ES only: seven `sys_business_unit` / `sys_business_unit_member` labels and - help texts that still named the business unit a department. - -Leaves whose English source only gained detail or was reworded, without -retracting what the translation says, are unchanged. Values only: no key is -added or removed, and no provenance table changes. diff --git a/.changeset/20544-compensated-sum-every-face.md b/.changeset/20544-compensated-sum-every-face.md deleted file mode 100644 index 2e7eb7e4a6b..00000000000 --- a/.changeset/20544-compensated-sum-every-face.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/objectql': patch -'@objectstack/driver-memory': patch -'@objectstack/service-analytics': patch ---- - -fix: `sum` / `avg` answer the same double on every face the platform owns, added with one compensated fold that `@objectstack/core` now exports as `compensatedSum` (#20544) - -Clause-②: yes - -**New export.** `@objectstack/core` exports `compensatedSum(nums)`: the sum of -`nums`, added in order with Kahan-Babuska-Neumaier compensation, which is the -summation SQLite (3.43 and later) uses for its own `sum` and `avg`. It moved -here from `@objectstack/objectql`'s rows path (`in-memory-aggregation.ts`), -which now imports it instead of keeping a private copy. - -**What changed.** Three folds still added a group's values naively, and now call -the same function: - -- `@objectstack/driver-memory`'s `aggregate()` and `find()` with aggregations, - the path `engine.aggregate` takes on an in-memory datasource; -- `@objectstack/driver-memory`'s analytics face (`MemoryAnalyticsService`), - whose `sum` / `avg` measures are now a `$group` `$accumulator` in place of - mingo's `$sum` / `$avg`; -- `@objectstack/service-analytics`' draft preview. - -Over a `number` column holding `0.1`, `0.2` and `0.3`, each of them answered -`0.6000000000000001` / `0.20000000000000004`. They now answer `0.6` / -`0.19999999999999998`, as SQLite and the engine's rows path do. Over -`1e16, 1, -1e16` they answered `0` and now answer `1`. On driver-memory, -`engine.aggregate` gave two answers depending on its path: `having { s: { $eq: -0.6 } }` kept the group on the rows path and dropped it on the native path. It -now keeps it on both. - -**What did not move.** Two addends, integers whose running total stays within -2^53, and a non-finite total give the same answer as before. Which values count -as addends did not change either: booleans as 1 / 0, and nulls and non-numeric -strings left out, as each face already had it. `count`, `min` and `max` are -untouched. The analytics face's pipeline dump (`result.sql`) now renders the -accumulator's functions by name, so a `sum` measure and an `avg` measure still -dump differently. - -**Residual.** PostgreSQL and MySQL add their doubles natively without -compensation, and the platform does not wrap that arithmetic. So over three or -more fractions their native path can still differ from these faces in the last -place. An exact `$eq` on a fractional sum compares doubles; compare with a range. diff --git a/.changeset/20546-no-operator-object-on-scalar.md b/.changeset/20546-no-operator-object-on-scalar.md deleted file mode 100644 index f0bb380083d..00000000000 --- a/.changeset/20546-no-operator-object-on-scalar.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: a plain object with no `$` operator where a scalar field's value belongs is refused with `INVALID_FILTER` / 400 at `where`, a per-aggregation `filter` and `having`, on every driver - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a filter may put beneath a scalar field. A plain object with no `$`-operator key — `{ "amount": { "a": 1 } }`, `{}` included — where a value of a field that holds scalar values belongs is refused by the engine before any driver is asked, where the in-memory driver answered it with no records (every record under `$not`) and the SQL driver refused it in its own words. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -The judged fields are the spec's scalar-valued classes: every type in `SCALAR_FILTER_HEAD_TYPES` (text-like, numeric, boolean, date, datetime, time, single-option, `autonumber`, `summary`), with or without `multiple: true`, and the multi-option types (`multiselect`, `checkboxes`, `tags`). A `having` column is judged by the type it carries: a `count` / `sum` / `avg` is a number, a groupBy or `min` / `max` column the type of its field, a date bucket a date or text label. - -The refusal names the field, its declared type, the object's keys and the position (`where.amount`, `aggregations[1].filter.amount`, `having.total`). No mechanical rewrite exists, because which value or operator the caller meant is not in the object; the fix is one line by hand: compare the field with a value (`{ "amount": 12 }`) or an operator (`{ "amount": { "$gt": 12 } }`), and to filter by a related record, name a relation field. - -Measured through `engine.find` / `engine.aggregate` and `POST /data/:object/query`, three rows: - -| position | filter | before: memory · SQLite · PostgreSQL 16 | now, on all three | -|:--|:--|:--|:--| -| `where` | `{ amount: { a: 1 } }` (number), `{ title: { a: 1 } }` (text), and the select, boolean, date, autonumber, multi-select and `multiple: true` select twins | no records · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400, the engine's words | -| `where` | `{ $not: { amount: { a: 1 } } }` | every record · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400 | -| per-aggregation `filter` | `{ amount: { a: 1 } }`, `{ amount: {} }` | count 0 on all three | `INVALID_FILTER` / 400 | -| `having` | `{ total: { a: 1 } }` (a `sum`), `{ title: { a: 1 } }` (a groupBy) | no group on all three | `INVALID_FILTER` / 400 | -| `where` | control: `{ owner: { region: "NA" } }` on a `lookup`, `{ meta: { a: 1 } }` on a `json` field | no records / one record · the driver's 400 · the driver's 400 | unchanged: reaches the driver as written | - -**Who is affected.** A caller that sends a no-operator object beneath a scalar field to the in-memory driver — a test suite, a local or embedded deployment on `InMemoryDriver`, a flow or hook calling the engine in-process — and read the empty answer as a real one. On `SqlDriver` the same filter was already a 400, now in the engine's words; at the per-aggregation `filter` and `having` it was a silent count of 0 or an empty group set on every driver. No existing test in `@objectstack/objectql` or `@objectstack/rest` sent the shape: both suites pass with no fixture changed. - -**Unchanged.** A relation field (`lookup`, `master_detail`, `user`, `tree`, single or multiple) keeps its nested-relation form, and a structured-JSON field (`json`, `composite`, `address`, …) its object comparand; both reach the driver as written, which answers them as before. File and media fields, `formula` (refused one door earlier, `INVALID_FIELD`), undeclared keys, a `{ $field }` reference and every operator bag are not judged by this refusal. A `Map` or a class instance keeps the comparand-type door's refusal in its own words. diff --git a/.changeset/20549-comparand-door-real-day-iso.md b/.changeset/20549-comparand-door-real-day-iso.md deleted file mode 100644 index b2dd104a665..00000000000 --- a/.changeset/20549-comparand-door-real-day-iso.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@objectstack/core": minor -"@objectstack/objectql": minor ---- - -fix(core,objectql)!: a temporal filter comparand is refused with `INVALID_FILTER` / 400 exactly when the same value is refused as a written value — a day that does not exist (`"2026-02-30"`) is no longer rolled over or compared as text, and a non-ISO `datetime` spelling (`"07/15/2026 10:00"`) is no longer read in the server's zone (#20549); and a `time` comparand whose instant has no four-digit UTC year (`"+010000-01-01T10:00:00Z"`) is refused rather than compared as text (#20480) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a `date`, a `datetime` and a `time` field accept as a filter comparand. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). - -The record validator already refused the `date` / `datetime` classes as written values (`VALIDATION_FAILED` / `invalid_date`). The comparand door was wider, so a filter admitted what a write refused and answered the wrong rows. The two predicates moved into `@objectstack/core`'s `isUninterpretableTemporalComparand`, and both doors now ask that one rule. A comparand is refused with `INVALID_FILTER` / 400, naming the field, before any driver read, at `where`, a per-aggregation `filter` and `having`: - -- **A day that does not exist**, on a `date` or as the day part of a `datetime`: `"2026-02-30"`, `"2026-02-29"` (2026 is not a leap year), `"2026-04-31"`, `"2026-02-30T10:00:00Z"`. `"2028-02-29"` is a real day and is read. -- **A `datetime` string in any spelling but the ISO 8601 ones the platform writes**, after trimming: `YYYY-MM-DD` (midnight UTC); `YYYY-MM-DDTHH:MM[:SS[.fraction]]` followed by `Z`, a `±HH:MM` or `±HHMM` offset, or nothing (a zone-naive wall clock is UTC, ADR-0074); and `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no zone. Refused now, for example: `"07/15/2026 10:00"`, `"2026/07/15 10:00"`, `"15 July 2026 10:00"`, `"07/08/2026"`, `"Wed, 15 Jul 2026 10:00:00 GMT"`, `"2026-07-15 10:00:00+08:00"` (write it with a `T`), and a bare integer string such as `"2026"` or `"1784109600000"`. -- **An instant on a `time` column in either class above.** A `time` column reads a comparand that is not a bare wall clock as an instant, by the `datetime` rule, and keeps its UTC time of day — so `"07/15/2026 10:00"` was the host zone's time of day, and `"1784109600000"` a string of epoch milliseconds. A wall clock (`"10:00"`, `"10:00:00.5"`), an ISO instant, a `Date` and an epoch-millisecond number are read as before, in a four-digit year (next). -- **An instant on a `time` column whose UTC year has no four-digit spelling**, in every spelling (#20480): `"+010000-01-01T10:00:00Z"`, `"-000001-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` (year 10000 in UTC), and the epoch-millisecond number or `Date` of any of them. A `time` column keeps the UTC time of day of an instant only when that instant spells a four-digit year; any other one reached the driver as written and was compared with the stored `HH:MM:SS` text. No time of day is read from an extended year. Year 0 (`"0000-06-15T10:00:00Z"`) spells four digits, and its time of day is read as before. - -Epoch milliseconds stay a `datetime` comparand as a JSON number: `{ "$gt": 1784109600000 }` is read exactly as before. As a string, a bare integer was read as epoch milliseconds, so `"2026"` meant two seconds after 1970 and matched every later row; send the number, or an ISO instant. - -What a caller sees through `POST /api/v1/data/:object/query`, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`: - -| `where` | memory | SQLite | PostgreSQL | now, on all three | -|:--|:--|:--|:--|:--| -| `datetime` `$eq "2026-02-30T10:00:00Z"` | 200, the row stored at `2026-03-02T10:00:00.000Z` | the same | the same | 400 `INVALID_FILTER` | -| `datetime` `$eq "07/15/2026 10:00"`, `"2026/07/15 10:00"` | 200, the row at `2026-07-15T14:00:00.000Z`, the server process's zone | the same | the same | 400 `INVALID_FILTER` | -| `date` `$eq "2026-02-30"` | 200 `[]`, compared as text | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | -| `datetime` `$gt "2026"` | 200, every row (read as 2026 epoch milliseconds) | the same | the same | 400 `INVALID_FILTER` | -| `time` `$gt "+010000-01-01T10:00:00Z"`, rows `09:00` / `10:30` / `12:00` | 200, 3 of 3 (compared as text) | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | -| `time` `$gt` the number of that instant | 200 `[]` | 200, 3 of 3 | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | - -The refusal names the field and the value, says the filter was not applied, and names the spellings that are read. The rows a non-ISO comparand matched were a property of the deployment host: the same request answered differently on two servers. - -**Who is affected.** A caller that filters a `date` or `datetime` field with a string: a REST or SDK client, a saved report or view filter, a dashboard's analytics query (the raw-SQL strategy declines such a comparand, and the engine refuses it), an MCP `query_records` call written by a model. A `{placeholder}` such as `{30_days_ago}`, the empty string, a JS `Date` and an epoch-millisecond number are unchanged. - -**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL: - -- a real leap day: `date` `"2028-02-29"`, `datetime` `"2028-02-29T10:00:00Z"`; -- each ISO spelling above, compared as the same instant whatever the host's zone: `"2026-07-15T14:00:00Z"`, `"2026-07-15T22:00:00+08:00"`, `"2026-07-15 14:00"` (UTC, not the host zone); -- a `date` comparand with a leading real `YYYY-MM-DD`, still compared as that day (`"2026-07-15T10:00:00Z"` on a `date` is July 15); -- the same wall clock as a 2026 instant on a `time` column: `$gt "2026-07-15T10:00:00Z"` answers the `10:30` and `12:00` rows, as does its epoch-millisecond number or `Date`; -- the year range 0001..9999, and every written value (the record validator now asks the same rule it copied, and answers exactly as before). diff --git a/.changeset/20550-calendar-day-years-below-100.md b/.changeset/20550-calendar-day-years-below-100.md deleted file mode 100644 index 8d08bfa2f26..00000000000 --- a/.changeset/20550-calendar-day-years-below-100.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -fix(spec): `nextUtcCalendarDay` and `utcInstantMs` read a bare day in the years 0001..0099 as written, not as 1900..1999 - -`nextUtcCalendarDay` proves a bare `YYYY-MM-DD` is a real day by building it and reading it back. It built the date with `Date.UTC`, which reads a year from 0 to 99 as 1900 + year, so `0050-01-01` came back as `1950-01-01`, the round trip failed, and the helper answered `null` for every day of those years. `utcInstantMs` asks the same round trip about a bare day, so it answered `null` for them too. The date is now built with `setUTCFullYear`, which takes the year as written; an impossible day (`0050-02-30`, `0100-02-29`) is still refused, not rolled over. - -What an author sees: a `datetime` filter `$lte '0050-01-01'`, or a `$between` whose maximum is that day, now includes the whole day, as it already did for `'2026-07-15'`. Before, it stopped at the day's first instant, so a row stored at `0050-01-01T10:00:00.000Z` was missed. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16: `$lte '0050-01-01'` answered only the row of `0049-12-31` and now also answers the two rows of `0050-01-01`; `$between ['0050-01-01', '0050-01-01']` answered no rows and now answers both. The next day's midnight stays out, and the 2026 control answers the same before and after. The other callers of the two helpers (the memory and mongo drivers, the analytics strategies, the engine's `having` filter and `formula`'s RLS `check` evaluator) import them from this package, so the correction reaches them with no change of their own. diff --git a/.changeset/20552-flow-hook-secret-read-projection.md b/.changeset/20552-flow-hook-secret-read-projection.md deleted file mode 100644 index 8224d4eb5eb..00000000000 --- a/.changeset/20552-flow-hook-secret-read-projection.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@objectstack/service-automation': patch -'@objectstack/metadata-protocol': minor -'@objectstack/metadata': patch -'@objectstack/runtime': patch ---- - -fix(security): a flow's inbound-hook secret is withheld from every served flow definition, and a read → edit → republish round trip keeps it (#20552) - -Clause-②: yes (widening) - -**The widening.** `@objectstack/metadata-protocol` gains one public method, -`ObjectStackProtocolImplementation.getMetaItemsForExecution`. It returns the stored -bodies without the serving decorations, for in-process binders that execute what they -read. No door that answers a caller may use it. - -An `api` flow's start node carries its inbound hook's HMAC secret (`config.secret`, -ADR-0041), the one credential that hook has. Every read that served the flow's -definition served the secret with it, to any authenticated caller. It is now -withheld from what is SERVED, and from nothing the engine executes. - -**What no longer carries the secret.** The automation domain's flow-definition read -and the flow its `POST` / `PUT` / clone doors answer with; and on the metadata plane, -every read of a flow — item, list, layered, draft preview, published snapshot, diff, -audit — plus a package export. The key is removed, not masked: a mask is a non-blank -string the registration gate would accept as the secret. - -**Consequence for a reader.** A client that read the secret back from a definition -no longer can. A package exported from one deployment and imported into another -arrives without it, and its `api` flows are refused at registration until a secret is -set on the start node again. - -**The round trip.** A save that carries the projected form — no `secret` where the -read served none — keeps the stored secret, on both authoring surfaces (the metadata -plane's save door and the automation domain's `PUT` / `POST`). Only an explicit value -replaces it, so a rotation is written as before. The start node is matched by its -`id`, not its position, so an edit that reorders `nodes` keeps it too. The first save of an item that has no stored row yet, such as a code-authored flow or datasource, takes the value from the code layer the read served, for every type with a registered redactor. - -- `@objectstack/service-automation` owns the projection (`redactFlowCredentials`) and - registers it as the `flow` read-path redactor at plugin `init`. The engine keeps - binding with the stored secret: it now reads flows from the protocol's execution - face, because the served face no longer holds the credential its hooks verify - against. -- `@objectstack/metadata-protocol` gains `getMetaItemsForExecution` on - `ObjectStackProtocolImplementation` — the same flattened list `getMetaItems` - serves, without the serving decorations (no `_diagnostics`, no credential - redaction). It is for in-process engines that execute what they read; every door - that answers a caller keeps serving `getMetaItems`. `carryForwardRedactedValues` - now follows a redacted path through an array by the element's `id`. -- `@objectstack/metadata`'s `getPublished` applies the type's registered read-path - redactor to the body it returns. It was the one metadata read exit that served a - stored body without it. diff --git a/.changeset/20553-validate-api-flow-secret.md b/.changeset/20553-validate-api-flow-secret.md deleted file mode 100644 index b432ee2fbfa..00000000000 --- a/.changeset/20553-validate-api-flow-secret.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -`os validate`, `os build` and `os lint` refuse an `api` flow with no per-flow secret, the flow the automation engine already refuses to register (#20553). - -Clause-②: yes (narrowing — `os validate` / `os build` / `os lint` newly refuse a secretless `api`-bound flow; the new exported rule id `FLOW_API_TRIGGER_SECRET_MISSING` widens `@objectstack/lint`) - - - -**BREAKING** — an accept-set narrowing on three authoring commands, shipped as -`minor` under the launch-window convention (`check-changeset-no-major` refuses -`major` until GA; breaking-ness is carried by this banner and the ADR-0087 -disposition above, not by the level). A stack that declares an `api`-bound flow -whose start node carries no usable `config.secret` used to pass `os validate`, -`os build` and `os lint`; they now exit non-zero and name the flow. -**One-line fix:** set a non-blank `config.secret` on the flow's start node — or, -for a flow that is only ever started explicitly, declare `type: 'autolaunched'` -with no `triggerType: 'api'`. - -`@objectstack/lint` gains one rule id, `flow-api-trigger-secret-missing`, at `error`, emitted by a new exported rule, `validateFlowApiTriggerSecret`, in the `validate-flow-trigger-readiness` family. It names a flow whose binding resolves to the inbound `api` trigger when that flow's start node carries no usable `config.secret`. A usable secret is a string that is non-empty after trimming. The rule fires for a missing, blank or non-string secret, and for an `api` flow with no start node. - -**Why.** ADR-0041's `trigger-api` acceptance criteria require a per-flow secret with HMAC verification. Since 17.5.0 the automation engine refuses such a flow in `registerFlow`, whatever its `status`: the `/automation` write doors answer `400`, and a boot skips the flow with a warning. `ApiTrigger.start()` also refuses to arm it. `os validate` builds neither, so it answered `✓ Validation passed` for a flow no runtime would register. It now exits non-zero and names the flow. - -**Which flows count as `api`-bound.** The rule uses the engine's own binding, `deriveTriggerBinding`. An array-form record `triggerType` goes to the record-change trigger first. Otherwise the flow gets the kind `resolveFlowTriggerKind` answers, which is a flow declaring `type: 'api'` or a start-node `triggerType: 'api'`. The engine gives the record-change, time-relative and schedule triggers precedence over `api`. So a `type: 'api'` flow whose start node also carries a `record-*` token, a `timeRelative` descriptor or a `config.schedule` binds that other trigger. The engine never asks that flow for a secret, and the rule stays silent on it. - -**Where the refusal surfaces.** `os validate`, `os build` and `os lint`. The runtime metadata publish gate is deliberately not covered yet (#20611): it judges a `/meta` save before the stored secret the flow read path withholds is restored, so for now a secretless flow saved there is still stored, and the engine then refuses it at registration. - -**Fix.** Set a non-blank `config.secret` on the flow's start node, and sign each post with it in the `x-objectstack-signature` header. A flow that is only ever started explicitly and never receives inbound posts is `type: 'autolaunched'`, with no `triggerType: 'api'` on its start node, and it needs no secret. - -`validateFlowApiTriggerSecret` and `FLOW_API_TRIGGER_SECRET_MISSING` are exported from `@objectstack/lint`. diff --git a/.changeset/20555-orgless-permission-set-read.md b/.changeset/20555-orgless-permission-set-read.md deleted file mode 100644 index 7d2c1b94d8c..00000000000 --- a/.changeset/20555-orgless-permission-set-read.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/plugin-security": patch ---- - -A permission-set resolution with no active organization now reads the organization-less permission sets only. A permission set scoped to an organization applies only while that organization is active. This is the rule `resolveUserAuthzGrants` already applies to grant rows. - -Clause-②: no - -Before, the by-name `sys_permission_set` read carried no organization when the caller had none active. Every organization's row of each requested name came back, and the first one won. The names requested include the principal's positions. So a permission set another organization had authored under the name of a built-in role could reach an organization-less principal's resolved sets and effective object map. The same read could also resolve another organization's same-named copy of a set the principal holds through a global grant. - -- **Unchanged:** a principal with an active organization resolves exactly as before. That read was already scoped to the organization and the organization-less rows, and it still prefers the organization's own row. Global grants still apply everywhere. A global position folded onto a global permission set of the same name still resolves with no organization active, and so does a global user grant. Permission sets declared in metadata or bootstrap resolve as before, because they never reach this read. -- **If a principal relied on it:** make the organization active, or grant the permission set globally (no organization) when it is meant to apply everywhere. diff --git a/.changeset/20558-comment-reactions-mentions-description.md b/.changeset/20558-comment-reactions-mentions-description.md deleted file mode 100644 index 59d33873f85..00000000000 --- a/.changeset/20558-comment-reactions-mentions-description.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -`sys_comment.reactions` and `sys_comment.mentions` now describe the shape they actually store (#20558) - -The two field descriptions are served metadata (field help in the console, and what an AI client reads before it seeds a comment), and both named a shape no producer writes: - -| Field | Description was | Description is now | Stored value | -| --- | --- | --- | --- | -| `reactions` | `JSON array of emoji reaction objects` | `JSON object mapping each emoji to the list of user ids who reacted` | `{"👍":["usr_1","usr_2"]}` | -| `mentions` | `JSON array of @mention objects` | `JSON array of the user ids @mentioned in the comment` | `["usr_1","usr_2"]` | - -The console's record discussion panel reads and writes `reactions` as that map, and writes `mentions` as that list of ids; the `collab.mention` notification hook reads the ids. - -Description text only: no stored value, validation rule or hook changes, and nothing to migrate. The English translation bundle is regenerated from the source description, and the zh-CN, ja-JP and es-ES help texts for both fields are rewritten to match (values only, no key added or dropped). diff --git a/.changeset/20573-knowledge-record-branch.md b/.changeset/20573-knowledge-record-branch.md deleted file mode 100644 index fb0bacdd742..00000000000 --- a/.changeset/20573-knowledge-record-branch.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/service-knowledge': patch ---- - -fix(service-knowledge): the realtime event bridge no longer keeps a `record.created` / `record.updated` / `record.deleted` branch, and its docs name the events ObjectQL really publishes (#20573) - -No producer emits a bare `record.*` event: the ObjectQL engine publishes `data.record.created` / `data.record.updated` / `data.record.deleted` for a single-record write and `data.records.updated` / `data.records.deleted` for a predicate write (`multi: true`), and `@objectstack/spec` already dropped the bare names from `RealtimeEventType`. The `KnowledgeServicePlugin` subscription handler still carried a branch for them, with a `payload.record ?? payload` fallback for a shape nothing sends. That branch is removed. The `data.record.*` sync (record body from `after`, id from `recordId`) and the `data.records.*` stale-index warning are unchanged. - -The `enableEventSync` option's TSDoc and the `handleRecordUpsert` / `handleRecordDelete` docs now name `data.record.created|updated|deleted` instead of `record.*`. - -If a plugin of yours publishes a bare `record.created`, `record.updated` or `record.deleted` event through `IRealtimeService` and relied on the knowledge index following it, publish `data.record.created|updated|deleted` with the `DataEvent` payload instead (the record in `after`, its id in `recordId`). diff --git a/.changeset/20574-major18-conversions-sorted-entries.md b/.changeset/20574-major18-conversions-sorted-entries.md deleted file mode 100644 index 9d0c774f5ce..00000000000 --- a/.changeset/20574-major18-conversions-sorted-entries.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -refactor(spec): protocol 18's conversions are authored as identifier-sorted entries with an explicit application order — no value changes (#20574) - -Nothing a consumer reads changes. `CONVERSIONS_BY_MAJOR[18]` (46 conversions, same -order), `ALL_CONVERSIONS` (113, same order) and `MIGRATIONS_BY_MAJOR[18].conversionIds` -are value-identical to the previous release, so the loader and the migration chain -apply the same conversions in the same sequence. - -What changed is how the list is written, so two major-18 retirements can be in flight -at once without conflicting in `packages/spec/src/conversions/registry.ts`: - -- `CONVERSIONS_BY_MAJOR[18]` is read off `MAJOR_18_CONVERSIONS`: one - `{ conversion, order }` entry per conversion, kept sorted by the conversion's - identifier and applied by ascending `order` (ties by the conversion's `id`). A - retirement adds ONE entry where its identifier sorts — never at the end — with - `order` one more than the highest present. -- A new conversion is defined directly above the definition of the entry that follows - it in that list, not at the end of the definitions. diff --git a/.changeset/20578-sdui-parser-base-props-one-list.md b/.changeset/20578-sdui-parser-base-props-one-list.md deleted file mode 100644 index 355c637e022..00000000000 --- a/.changeset/20578-sdui-parser-base-props-one-list.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/sdui-parser': minor ---- - -`@objectstack/sdui-parser` now reads one base-prop list, ported from objectui's `SDUI_BASE_PROPS` at the console pin `db11afd4967c` (objectui#11008, #11044). Both `validateTree` and the generated JSX types (`generateDts`'s `SduiBaseProps`) are driven by it. - -- On every node, whatever the component declares: `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId` are newly accepted. They no longer draw `unknown-prop`, and the generated types accept them as attributes. -- Only on a type whose registration declares no input of that name: `name`, `label`, `description`, `placeholder`, `data`, `ariaLabel` are newly accepted. A type that declares one keeps its declared type check and its declared attribute type; its generated interface `Omit`s that key from `SduiBaseProps`. - -Effect for consumers: `os validate` stops warning `unknown-prop` on those keys, and a `.tsx` page that authors them now type-checks against `generateDts` output where it was a TypeScript error before. Measured on the tracked `sdui.manifest.json` (107 components), no component declares any of the five every-node keys, and every declared where-undeclared key is checked as before, so no diagnostic of error severity is removed for that manifest. The wider type surface is why this is a minor, not a patch. diff --git a/.changeset/20580-explain-removed-member.md b/.changeset/20580-explain-removed-member.md deleted file mode 100644 index 1b68921f74b..00000000000 --- a/.changeset/20580-explain-removed-member.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/core': minor ---- - -fix(plugin-security): `security/explain` resolves the user it explains in the organization enforcement resolves them in, so a member whose membership in the caller's organization has ended is no longer explained holding that organization's grants (#20580) - -When an administrator explains another user, the explanation is computed in the administrator's own organization. Enforcement does one more thing for that same user first: under a walled tenancy posture (`isolated` or `group`), it drops an organization claim that no current membership backs, and the user resolves with no active organization, so only their global grants apply. The explainer skipped that check. For a user whose membership in the administrator's organization had ended, the explanation listed that organization's grants, and the verdicts they decide, while enforcement applied none of them. - -The explainer now asks the same check before it resolves the user, and resolves them where it says. `@objectstack/core` exports that check as `vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds, tenancyPosture)`. It returns the claimed organization while a current membership backs it or while no wall is enforced, and `undefined` once the claim is dropped. `resolveAuthzContext` asks the same function for a session's claim, so the two cannot disagree. This is a new export with no behaviour change to `resolveAuthzContext`. - -Unchanged: - -- Enforcement admits and refuses exactly what it did before. -- A current member's explanation. -- The `single` posture, where no claim is dropped on either side. -- Explaining yourself, and a caller with no active organization. diff --git a/.changeset/20583-lint-json-define-stack-conversions.md b/.changeset/20583-lint-json-define-stack-conversions.md deleted file mode 100644 index 2c0c7e546e9..00000000000 --- a/.changeset/20583-lint-json-define-stack-conversions.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -**`objectstack lint --json` now reports the ADR-0087 conversions `defineStack` applied, as `objectstack validate` and `objectstack build` already do.** - -`defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads and prints one `defineStack: PATH: 'OLD' → 'NEW' (converted at load; conversion 'ID', retires in protocol N)` line on stderr. `objectstack lint` filled its `conversions` list only from its own conversion pass over the loaded config, which a `defineStack` default export hands over already converted. So `--json` answered `conversions: []` for a config carrying a retiring spelling, such as `description` on a `page:header` component, and the notice reached stderr alone. - -`objectstack lint` now adds the conversions the stack producer recorded on the default export to that list right after the config loads, and its own pass still reports what the producer never saw: an unbuilt default export, or a key merged in from a named export of the config module. Each conversion is listed once. The text face prints the same notices in its warning block. - -What `objectstack lint` accepts does not change: it still lints an unbuilt default export (a plain object literal), whose conversions come from its own pass as before. The exit code, `passed`, `issues` and the counts are unchanged, because a conversion notice is not a lint finding. - -Clause-②: no diff --git a/.changeset/20583-refusal-conversions-json.md b/.changeset/20583-refusal-conversions-json.md deleted file mode 100644 index 10c382e9a35..00000000000 --- a/.changeset/20583-refusal-conversions-json.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -**When a `defineStack` or `composeStacks` call converts a deprecated spelling and then refuses the config, `objectstack validate --json`, `objectstack build --json` and `objectstack lint --json` now report both the refusal and the ADR-0087 conversions it applied.** - -`defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, such as `description` on a `page:header` component (canonical `subtitle`). When the same call then refused the config, for example on an unknown `requires` token (`STACK_CAPABILITY_UNKNOWN`), each of the three commands exited 1 with the refusal's `error` and `code` and with `conversions: []`. The conversion reached stderr only, as a warn-once line. - -The refusal now carries the conversions the producer applied before it refused (`stackConversionsOf(error)` in `@objectstack/spec`), and each command adds them to the `conversions` list of its failure payload, beside the refusal. Each conversion is listed once. A refusal whose source needed no conversion, and any other failure at load, still answers `conversions: []`. - -Nothing is accepted or refused differently: the exit code, `error`, `code` and every other key of each payload are unchanged, and no key is added. The text face is unchanged. - -Clause-②: no diff --git a/.changeset/20586-turso-forced-local-refuses-sync-url.md b/.changeset/20586-turso-forced-local-refuses-sync-url.md deleted file mode 100644 index fe7c25cfce8..00000000000 --- a/.changeset/20586-turso-forced-local-refuses-sync-url.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/driver-turso': minor ---- - -fix(spec,driver-turso)!: a turso config that forces `mode: 'local'` beside a `syncUrl` is refused where it is written and when the driver is built, instead of running as an embedded replica under a `local` label - -Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves. - -A `syncUrl` names the remote an embedded replica syncs with. A config that forced `mode: 'local'` on a `file:` url (or `:memory:`) beside a non-empty `syncUrl` was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the driver source before this change, with a client that counts syncs: it constructed with `transportMode` `'local'`, then synced on connect, started the sync interval, and `isSyncEnabled()` answered `true` — exactly what the same config with no `mode` (a replica) did. A datasource declared local was kept in sync with a remote, and only a label said otherwise. - -**BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out: - -- **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service; -- **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened. - -The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. It is the twin of the forced `mode: 'replica'`-without-`syncUrl` refusal, the other way round: honouring `mode: 'local'` by skipping the sync would ignore a declared `syncUrl` instead, which is the same defect with the keys swapped. The sibling refusals keep their order: a forced local mode on a remote url or a bare path still meets its `url` refusal first. An empty `syncUrl` is unset and is still accepted. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it. - -### Migration: FROM → TO - -| You wrote | Write instead | -| --- | --- | -| `url: 'file:./data/replica.db', mode: 'local', syncUrl: 'libsql://my-db.turso.io'` | an embedded replica: drop `mode` (`url` and `syncUrl` select the replica) | -| the same | a plain local database: drop `syncUrl` (and `sync`), keeping `url: 'file:./data/app.db'` with or without `mode: 'local'` | - -A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and synced with the remote under a `local` label. The way out is the table above. - -Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` or `syncUrl` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero. - - diff --git a/.changeset/20590-flow-credential-positions.md b/.changeset/20590-flow-credential-positions.md deleted file mode 100644 index 53076603308..00000000000 --- a/.changeset/20590-flow-credential-positions.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/service-automation': patch -'@objectstack/metadata-protocol': patch -'@objectstack/runtime': patch ---- - -fix(security): every credential a flow definition holds is withheld from what is served, at every depth, and an edit round trip keeps each one where it belongs (#20590) - -Clause-②: no - -**What is now withheld.** Beside an `api` flow's inbound-hook secret (the start node's -`config.secret`), every served flow definition now also withholds an `http` node's -outbound signing secret (`config.signingSecret`), and both are withheld wherever the -node sits: at the top level, or inside a `loop` body, a `parallel` branch, or a -`try_catch` region. The engine still executes the stored values. - -**Removing a signing secret.** A definition saved back without the key keeps the -stored secret, because an absent key is what every read serves. To remove it, save -the key as the empty string (`signingSecret: ''`): the durable callout is then -delivered unsigned, and the empty value is served as written, so the next round trip -keeps it cleared. - -**Changing a node's kind.** An edit that keeps a node's `id` and changes its kind no -longer carries that node's stored credential onto it. The credential belonged to the -old kind; a start node that needs a secret asks for one again at registration. - -**Moving a node.** A node moved into or out of a `loop` body, a `parallel` branch or a -`try_catch` region keeps its stored credential across the round trip, as long as its -`id` and kind are unchanged and it is the only node, at the top level or in any region, -that carries that `id`. An edge or a config value with the same `id` does not count. - -**The `/meta` list read on a dispatcher host.** When the metadata protocol's list read -fails, the list answers that failure (`503 SERVICE_UNAVAILABLE` for a store outage, or -the protocol's own refusal) instead of serving the metadata service's stored list, -which applies no credential redaction. A host whose protocol has no list verb keeps -its metadata-service fallback. diff --git a/.changeset/20590-http-credential-guidance.md b/.changeset/20590-http-credential-guidance.md deleted file mode 100644 index 72b914d971f..00000000000 --- a/.changeset/20590-http-credential-guidance.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -The `http` node's designer form says an outbound credential never goes in the node's `url` or `headers`, and where it goes instead (#20590) - -Clause-②: no - -The `http` action descriptor's `configSchema` is what the flow designer's palette and property form read (`GET /api/v1/automation/actions`). It described `url` as "Target URL" and `headers` as "Request headers", with no word about credentials. Both values are stored in the flow definition, and a flow definition is served to every member who can read flows; of the node's config, only `signingSecret` is withheld. The two field descriptions now say this, and name where the credential goes instead: - -- a credential in a header: a `connector_action` on a declarative connector whose `auth.credentialRef` names the secret; -- a key in the query string: a declarative `rest` connector with `api-key` auth, whose `paramName` names the parameter and whose `auth.credentialRef` names the secret; -- a webhook whose path is the secret: a token-authenticated connector instead, such as the `slack` connector with its bot token. No `credentialRef` variant carries a secret in the url path. - -Description text only. No config key is added or removed, nothing more is withheld on read, and there is nothing to migrate. diff --git a/.changeset/20592-ui-html-page-div-refused.md b/.changeset/20592-ui-html-page-div-refused.md deleted file mode 100644 index edf3e9d1b22..00000000000 --- a/.changeset/20592-ui-html-page-div-refused.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec): the protocol-18 migration step records the html-tier `div` refusal as the semantic entry `ui-html-page-div-refused` (#20592) - -Clause-②: no - -`MIGRATIONS_BY_MAJOR[18].semantic` gains one entry, `ui-html-page-div-refused`. -It records the narrowing `@objectstack/cli` takes on when its JSX page gate -reaches the SDUI component manifest that `@objectstack/console` ships: a project -with no `sdui.manifest.json` of its own has its `kind: 'html'` pages checked -against that manifest, which does not declare `div`, so `objectstack validate`, -`compile` and `lint` refuse a `div` there (`jsx-forbidden-tag`, -`jsx-unknown-component`). The entry prescribes `box` for a plain wrapper, names -the layout containers to reach for instead only when their layout is wanted, and -says how to prove the rewrite done. - -What moves for a consumer of this package: `MIGRATIONS_BY_MAJOR` carries the -entry, and `objectstack migrate meta` prints it, because its default range -already runs to protocol 18, the highest major with a step. Nothing else does. -The protocol-18 step is not cut yet, so `spec-changes.json` and the protocol -upgrade guide, which project the steps up to the current protocol major, are -unchanged, and no schema accepts or refuses anything it did not before. diff --git a/.changeset/20594-cli-bin-form-d.md b/.changeset/20594-cli-bin-form-d.md deleted file mode 100644 index d638d348c93..00000000000 --- a/.changeset/20594-cli-bin-form-d.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -A docblock line in `@objectstack/cli`'s `bin/run.js` no longer cites a tracker number - -The docblock above `bin/run.js`'s `process.stderr` `error` listener ended a -sentence with a tracker number that no longer resolves on GitHub. The number is -gone and the sentence stays: `files` names only `dist`, but npm packs a `bin` -target regardless, which is the measured fact the number was pointing at. The -file ships because of that same packing rule, which is why this is a release -note at all. Comment only: no command, flag, exit code, error code, export or -runtime behaviour changes. diff --git a/.changeset/20594-cli-bin-provenance-anchors.md b/.changeset/20594-cli-bin-provenance-anchors.md deleted file mode 100644 index 1d7071e6e3e..00000000000 --- a/.changeset/20594-cli-bin-provenance-anchors.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -Provenance comments in `@objectstack/cli`'s `bin/run.js` were re-anchored - -Three docblock lines above `bin/run.js`'s `process.stderr` `error` listener -cited a tracker number that no longer resolves on GitHub. They now cite the -commit in this repository's history that made a failed stderr write non-fatal -on the dev shim. The file ships because npm packs a `bin` target regardless of -`files`, which is why this is a release note at all. Comment only: no command, -flag, exit code, error code, export or runtime behaviour changes. diff --git a/.changeset/20594-observability-provenance-anchors.md b/.changeset/20594-observability-provenance-anchors.md deleted file mode 100644 index b5683c22602..00000000000 --- a/.changeset/20594-observability-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/observability': patch ---- - -A provenance comment in `@objectstack/observability` was re-anchored - -The `SEMCONV` comment beside the retired `http_request_errors_total` entry -cited a tracker number that no longer resolves on GitHub. It now cites the -commit in this repository's history that moved `http_request_duration_ms` to -the transport seam. Comment only: no metric name, label, export, type or -runtime behaviour changes. diff --git a/.changeset/20595-metadata-protocol-provenance-anchors.md b/.changeset/20595-metadata-protocol-provenance-anchors.md deleted file mode 100644 index 8ba87a08a56..00000000000 --- a/.changeset/20595-metadata-protocol-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -Provenance comments in `@objectstack/metadata-protocol` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes -(and ADR-0005's design-principle-3 correction where that record exists). Some of these docblocks sit -on exported members, so the reworded text appears in the published `index.d.ts` / `index.d.cts`, and -a few comments that esbuild keeps appear in the JavaScript output. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-objectql-provenance-anchors.md b/.changeset/20595-objectql-provenance-anchors.md deleted file mode 100644 index 28d27fce775..00000000000 --- a/.changeset/20595-objectql-provenance-anchors.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -Provenance comments in `@objectstack/objectql` cite the commits and ADRs that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes, or the -ADR that records it (ADR-0029 D9.2a, ADR-0104's 2026-09-05 addendum, ADR-0126 §7.2, ADR-0130 D3). -Some of these docblocks sit on exported members, so the reworded text appears in the published -`index.d.ts` / `index.d.mts`, `core.d.ts` / `core.d.mts` and the shared type chunk, and comments that -esbuild keeps appear in the JavaScript output. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20596-plugin-approvals-provenance-anchors.md b/.changeset/20596-plugin-approvals-provenance-anchors.md deleted file mode 100644 index 909c12660e8..00000000000 --- a/.changeset/20596-plugin-approvals-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -Provenance comments in `plugin-approvals` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-plugin-audit-provenance-anchors.md b/.changeset/20596-plugin-audit-provenance-anchors.md deleted file mode 100644 index 97d9e7dcaa7..00000000000 --- a/.changeset/20596-plugin-audit-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -Provenance comments in `plugin-audit` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-plugin-auth-provenance-anchors.md b/.changeset/20596-plugin-auth-provenance-anchors.md deleted file mode 100644 index 2a5a7d2441c..00000000000 --- a/.changeset/20596-plugin-auth-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -Provenance comments in `plugin-auth` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-plugin-email-provenance-anchors.md b/.changeset/20596-plugin-email-provenance-anchors.md deleted file mode 100644 index d233bd3faa8..00000000000 --- a/.changeset/20596-plugin-email-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-email': patch ---- - -Provenance comments in `plugin-email` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-plugin-security-provenance-anchors.md b/.changeset/20596-plugin-security-provenance-anchors.md deleted file mode 100644 index 737ec3418a9..00000000000 --- a/.changeset/20596-plugin-security-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -Provenance comments in `plugin-security` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the record in this repository that decided -the matter (an ADR where one exists, otherwise the commit in this repository's -history), and say in their own words what was decided. Comments only: no type, -schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-plugin-sharing-provenance-anchors.md b/.changeset/20596-plugin-sharing-provenance-anchors.md deleted file mode 100644 index 9dc01cd6532..00000000000 --- a/.changeset/20596-plugin-sharing-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-sharing': patch ---- - -Provenance comments in `plugin-sharing` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-service-analytics-provenance-anchors.md b/.changeset/20596-service-analytics-provenance-anchors.md deleted file mode 100644 index 7002507ab29..00000000000 --- a/.changeset/20596-service-analytics-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -Provenance comments in `service-analytics` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-service-automation-provenance-anchors.md b/.changeset/20596-service-automation-provenance-anchors.md deleted file mode 100644 index a747df12857..00000000000 --- a/.changeset/20596-service-automation-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -Provenance comments in `service-automation` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the record in this repository that decided -the matter (an ADR where one exists, otherwise the commit in this repository's -history), and say in their own words what was decided. Comments only: no type, -schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-service-datasource-provenance-anchors.md b/.changeset/20596-service-datasource-provenance-anchors.md deleted file mode 100644 index 519e47da701..00000000000 --- a/.changeset/20596-service-datasource-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -Provenance comments in `service-datasource` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-service-package-provenance-anchors.md b/.changeset/20596-service-package-provenance-anchors.md deleted file mode 100644 index 44811c125a4..00000000000 --- a/.changeset/20596-service-package-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-package': patch ---- - -Provenance comments in `service-package` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-service-settings-provenance-anchors.md b/.changeset/20596-service-settings-provenance-anchors.md deleted file mode 100644 index a07ea32f559..00000000000 --- a/.changeset/20596-service-settings-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-settings': patch ---- - -Provenance comments in `service-settings` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-service-storage-provenance-anchors.md b/.changeset/20596-service-storage-provenance-anchors.md deleted file mode 100644 index a3b1f1f140e..00000000000 --- a/.changeset/20596-service-storage-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -Provenance comments in `service-storage` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-trigger-record-change-provenance-anchors.md b/.changeset/20596-trigger-record-change-provenance-anchors.md deleted file mode 100644 index ebf861c1f29..00000000000 --- a/.changeset/20596-trigger-record-change-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/trigger-record-change': patch ---- - -Provenance comments in `trigger-record-change` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20596-trigger-schedule-provenance-anchors.md b/.changeset/20596-trigger-schedule-provenance-anchors.md deleted file mode 100644 index 7d25f06447f..00000000000 --- a/.changeset/20596-trigger-schedule-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/trigger-schedule': patch ---- - -Provenance comments in `trigger-schedule` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, log or refusal text, or runtime behaviour changes. diff --git a/.changeset/20597-lint-authoring-rules-provenance-anchors.md b/.changeset/20597-lint-authoring-rules-provenance-anchors.md deleted file mode 100644 index d69882fa55e..00000000000 --- a/.changeset/20597-lint-authoring-rules-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -Provenance comments in `@objectstack/lint`'s authoring-rule registry were re-anchored - -Five comment and docblock lines in `src/authoring-rules.ts` that cited tracker -numbers which no longer resolve on GitHub now cite the commit in this -repository's history that decided the matter, and keep saying what was -decided. Comments only: no rule id, finding message, hint, severity, type or -runtime behaviour changes. diff --git a/.changeset/20597-react-prop-deprecated-message-words.md b/.changeset/20597-react-prop-deprecated-message-words.md deleted file mode 100644 index 7e6fb797ca9..00000000000 --- a/.changeset/20597-react-prop-deprecated-message-words.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -The `react-prop-deprecated` warning states its reason in words instead of citing a tracker number - -The finding `validateReactPageProps` reports for a react-page prop written in a -deprecated react-tier spelling used to end by pointing the author at an issue -number that no longer resolves. It now says what that decision was, in the -sentence being read: the react tier converges on the metadata-tier vocabulary, -so the deprecated spelling keeps working through the deprecation window and is -removed after it. The block tag, the prop and the canonical metadata-tier -spelling it names are unchanged, and so are the rule id, the `warning` -severity, the hint and every other finding. diff --git a/.changeset/20599-utc-instant-from-parts.md b/.changeset/20599-utc-instant-from-parts.md deleted file mode 100644 index 9e45de7f852..00000000000 --- a/.changeset/20599-utc-instant-from-parts.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@objectstack/core": minor -"@objectstack/service-analytics": patch -"@objectstack/trigger-schedule": patch ---- - -fix(core): a date or time in the years 0001..0099 is read as written, not as 1900..1999, wherever a UTC instant is built from year / month / day / time parts - -`Date.UTC(year, …)` and `new Date(year, …)` read a year from 0 to 99 as 1900 + year. Core built its instants from parts that way, so every day of the years 0001..0099 (inside the supported range 0001..9999) landed in the 1900s at the sites below, with no error. - -- `@objectstack/core`: **new export** `wallClockToUtcMs(parts)`, the epoch milliseconds of a `WallClockParts` read as UTC. It is `Date.UTC` without the two-digit-year remap: `month` is 1-12, omitted time components are 0, and every component rolls over past its end as `Date.UTC` rolls it (`month: 13` is next January, `day: 0` the previous month's last day, `hour: 24` the next midnight). A `NaN` component gives `NaN`. Every site below now builds through it: - - `zonedWallClockToUtcMs` and `zonedDateStartToUtcMs`, the wall clock and the zone-offset read. The offset read also takes the zone's era, so a wall clock early on 0001-01-01 in a zone west of UTC, whose offset probe lands in year 0, reads right. - - `bucketKeyToCalendarRange` (`0050` spans 0050-01-01..0051-01-01, not 1950..1951; `0050-01-01` as a `day` key is no longer `null`) and `bucketDateKey`'s ISO week (0050-01-01 is in week 52 of 0049, not of 1949). - - The date macros: `{1976_years_ago}` resolves to `0050-09-30`, not `1950-09-30`. A macro that lands in 0001..0999 is now spelled with a four-digit year, as the `date` storage form spells it (`0055-06-15`, not `55-06-15`, which names no day). -- `@objectstack/service-analytics`: the preview evaluator's `week` key and the `compareTo` bucket alignment build their days through `wallClockToUtcMs`. -- `@objectstack/trigger-schedule`: a time-relative window's day bounds build through `wallClockToUtcMs`. - -What an author sees: `POST /api/v1/data/:object/import` stores the `datetime` cell `0050-01-01 10:00` as `0050-01-01T10:00:00.000Z`, and in `Asia/Shanghai` as `0050-01-01T01:54:17.000Z` (the zone's local mean time for that year). Before, it stored `1950-01-01T10:00:00.000Z` and `1950-01-01T02:00:00.000Z` and reported the row `ok`. Measured through the import route and read back through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16; the `2026-07-15 10:00` control is stored the same before and after. Every year from 0100 on builds exactly as before. diff --git a/.changeset/20600-last-day-unbounded-above.md b/.changeset/20600-last-day-unbounded-above.md deleted file mode 100644 index 632bba2a3f0..00000000000 --- a/.changeset/20600-last-day-unbounded-above.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor -'@objectstack/driver-sql': patch -'@objectstack/driver-turso': patch -'@objectstack/driver-memory': patch -'@objectstack/driver-mongodb': patch -'@objectstack/formula': patch -'@objectstack/objectql': patch -'@objectstack/service-analytics': patch ---- - -fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) - -Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. - -**BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. - -`9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. - -Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: - -- `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. -- A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. -- The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. -- `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. - -Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. - -**If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. - -**If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. - -The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. - - diff --git a/.changeset/20602-export-year-four-digits.md b/.changeset/20602-export-year-four-digits.md deleted file mode 100644 index d0d2f7541bb..00000000000 --- a/.changeset/20602-export-year-four-digits.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): `GET /api/v1/data/:object/export` writes a `date` or `datetime` cell with a four-digit year, so an export of a year from 0001 to 0999 re-imports (#20602) - -Clause-②: no - -A `date` of `0500-01-01` exported as `500-01-01`, in CSV, xlsx and JSON alike, -and so did the day of a `datetime` cell whose business-timezone day fell before -year 1000: the instant `1000-01-01T02:00:00.000Z` exported in America/New_York -as `999-12-31 21:03:58`. `POST /api/v1/data/:object/import` reads a four-digit -year only, so re-importing the platform's own file refused that row as -`invalid_date`. The export now spells every `date` and `datetime` cell's day -with the storage rule the write doors use (`temporalStorageForm` from -`@objectstack/core`): `0500-01-01` and `0999-12-31 21:03:58`, which the import -reads back as the same day and the same instant. - -**What is not affected.** Every cell whose day falls in the years 1000 to 9999 -exports byte for byte as before, in every business timezone and with none. The -clock of a `datetime` cell is unchanged. A `datetime` stored before year 1000, -which the write doors now refuse, exports with a padded year as well, and the -import refuses it as `invalid_date`, as the write doors do. A year outside 0001 -to 9999 stays unpadded, and a `datetime` whose business-timezone day falls in -such a year now spells that year as the storage rule does (`0-12-31`, not the -era year `1-12-31`); the import refuses both spellings, as before. diff --git a/.changeset/20603-explain-route-classified-refusal.md b/.changeset/20603-explain-route-classified-refusal.md deleted file mode 100644 index 6990834f2bc..00000000000 --- a/.changeset/20603-explain-route-classified-refusal.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): `GET` / `POST /security/explain` answers a refusal the security service classified with that refusal's own status and code, instead of `500 EXPLAIN_FAILED` (#20603) - -Clause-②: no - -The explain service can refuse a request with an ADR-0112 envelope: a `code` and a 4xx `status`. The measured case is a row-level policy that the record matcher cannot evaluate. The service then answers `INVALID_FILTER` / 400, the same answer the find it explains gives for that filter. This happens for a record-grained explanation (`recordId`), for an object-level explanation, and for a `recordId` that no row carries. - -The route's error handler recognised only `PERMISSION_DENIED` (403) and `OBJECT_NOT_FOUND` (404). Every other throw answered `500` with `error.code: 'EXPLAIN_FAILED'`. So through HTTP the explain call reported a server fault, while the find it explains reported the caller's error. A client reading that 500 retries or reports an outage, where the platform means "this policy cannot be evaluated". - -The route now asks the same classification the `/data` door uses. A throw that declares a 4xx `status` (or `statusCode`) and a `code` answers with that status and that code in the nested envelope, `{ success: false, error: { code, message } }`. The message is bounded the way `/data` bounds a refusal's message. - -Unchanged: - -- A throw that is not classified still answers `500 EXPLAIN_FAILED`. That covers a plain `Error`, a declared 5xx, and a `code` with no `status`. -- The `403 PERMISSION_DENIED` and `404 OBJECT_NOT_FOUND` answers. -- A successful explanation's body. diff --git a/.changeset/20604-explain-enforce-closeout.md b/.changeset/20604-explain-enforce-closeout.md deleted file mode 100644 index b90f9cc2c0a..00000000000 --- a/.changeset/20604-explain-enforce-closeout.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/core': patch ---- - -fix(plugin-security): `security/explain` answers enforcement's refusal at the object level too, and explains another user in the organization they are resolved in (#20604) - -Clause-②: no - -Two answers of `POST /api/v1/security/explain` disagreed with what the same principal's own request gets from enforcement. - -**A row-level policy that compares two fields of no shared comparison class** (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object). The SQL driver refuses to compile such a read, so the find answers `INVALID_FILTER` / 400. A by-id update or delete fails closed at its row-level gate, and an insert whose check judges the policy is refused with `INVALID_FILTER` / 400. An object-level explanation (no `recordId`) still answered `allowed: true`, the `rls` layer `narrows`, and the predicate as `readFilter`, for every operation. A `recordId` that no row carries was answered `visible: false` with no deciding layer. Both are now refused with the envelope a record-grained explanation already gives: `INVALID_FILTER` / 400, with the message that names the policy and both fields. A request that the capability gate or the CRUD grant denies is still explained as denied there. - -**Another user explained by an administrator.** The explanation now carries the organization the user is resolved in, as enforcement's context for that user does. Before, a current member of the administrator's organization was explained with no organization. Under `isolated`, that member was reported denied on a tenant object their own find reads. Under every posture, a permission set that their organization authored (a `sys_permission_set` row scoped to that organization) was missing from the explanation and from the verdicts it decides. - -`@objectstack/core`: the API-key arm of `resolveAuthzContext` asks `vetOrganizationClaim` for its membership rule, as the session arm does. This is a refactor with no behaviour change. A key whose owner is no longer a member of its organization is still refused. - -Unchanged: - -- Enforcement admits and refuses exactly what it did before. -- A comparison between two fields of one class keeps its verdicts, at the object level and per record. -- Explaining yourself. -- A removed member's explanation (no organization, as enforcement resolves them). diff --git a/.changeset/20611-gate-reads-redaction-context.md b/.changeset/20611-gate-reads-redaction-context.md deleted file mode 100644 index 5a4c4e867f1..00000000000 --- a/.changeset/20611-gate-reads-redaction-context.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/metadata-protocol': minor ---- - -The runtime metadata publish gate refuses an `api` flow with no per-flow secret, and reads a secret the flow read path withheld as present (#20611). - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on the runtime metadata write door, -shipped as `minor` under the launch-window convention (`check-changeset-no-major` -refuses `major` until GA; breaking-ness is carried by this banner and the ADR-0087 -disposition above, not by the level). An `active` save through `/meta` of an -`api`-bound flow whose start node carries no usable `config.secret` (a -`PUT /api/v1/meta/flow/:name`, or the publish of such a draft) used to be stored; -the automation engine then refused to register it (`400` on the `/automation` -doors, a skip with a warning at boot). It is now refused at the save with -`422 INVALID_METADATA`, the issue naming `flow-api-trigger-secret-missing` at the -start node's `config.secret`, and nothing is stored. A draft save is still -accepted; its publish is refused the same way. -**One-line fix:** set a non-blank `config.secret` on the flow's start node — or, -for a flow that is only ever started explicitly, declare `type: 'autolaunched'` -with no `triggerType: 'api'`. - -**What does not change: a signed flow's round trip.** Every served flow definition withholds the start node's `config.secret`, so a body saved back after a read arrives without it, and the save restores the stored secret only after every gate has run, so that no gate handles a restored credential. The gate is now told WHERE the save will restore a credential from the stored row: those positions only, never the values. `flow-api-trigger-secret-missing` reads a secret that was withheld and is stored as present, and one that is absent and not stored as missing. So a GET → edit → PUT of a signed flow, and the first save of a code-authored flow whose secret is in the app's source, keep passing and keep their secret. An explicit empty `config.secret` is the author's own value and is refused as blank. - -`@objectstack/lint`: - -- `validateFlowApiTriggerSecret` now runs on the runtime publish gate too (`surfaces` `['cli', 'runtime-publish']`, `runtimeTypes: ['flow']`). Its `surfaceReason` is gone. -- `AuthoringRuleContext` gains an optional `restoredCredentialPaths`: a `ReadonlySet` of stack-relative positions in the rules' own finding-path spelling (`flows[0].nodes[1].config.secret`). Only the runtime publish gate sets it; `runAuthoringRules` never forwards it, so `os validate`, `os build` and `os lint` judge the author's own values as before. -- `runRuntimeAuthoringRules` accepts an optional `restoredCredentialPaths`: item-relative dotted positions in the `@objectstack/spec/kernel` redactor registry's `redactedKeys` spelling (`nodes.1.config.secret`). The gate re-spells them against the written item's place in its snapshot. -- `validateFlowApiTriggerSecret(stack, options?)` accepts an optional `{ restoredCredentialPaths }`, and treats a listed start-node secret position as present. - -`@objectstack/metadata-protocol`: `saveMetaItem` hands the runtime authoring gate the positions its own credential carry-forward will fill, computed from the same stored body. This costs one indexed `sys_metadata` read on an `active` save of a type with a registered redactor (`datasource`, and `flow` where the automation plugin registers one), and nothing for any other type or for a draft save. The carry-forward itself is unchanged and still runs after every gate. The draft→active promotion judges the stored draft row, which already holds what that draft's save carried forward, so it needs no such positions. diff --git a/.changeset/20618-refusal-carries-conversions.md b/.changeset/20618-refusal-carries-conversions.md deleted file mode 100644 index 66ff1a35be2..00000000000 --- a/.changeset/20618-refusal-carries-conversions.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -**A `defineStack` or `composeStacks` call that refuses now carries the ADR-0087 conversions it applied on the error it throws, so `stackConversionsOf(error)` reads them off a caught refusal.** - -`defineStack` rewrites a deprecated metadata spelling to its canonical shape before it validates, and records each conversion on the stack it returns (`stackConversionsOf(stack)`). A call that then refused returned no stack, so the conversions it had applied were lost: they reached stderr only, as a warn-once line that a second stack with the same path does not print again. A tool that catches the refusal, such as a `--json` door, had no way to report both the refusal and the retiring spelling. - -- `defineStack` (strict and `strict: false`) stamps the conversions applied so far on every ADR-0112 refusal it throws after its conversion pass: the schema parse, the six cross-field refusals and the bound-action merge's shape refusal. The record is the same `ConversionNotice[]` a built stack carries, under the same symbol key, non-enumerable and frozen. A refusal whose source needed no conversion carries an empty record. -- `composeStacks` stamps its inputs' records on every refusal it throws, by the same rule it uses for the artifact it returns. -- `stackConversionsOf(value)` now also reads the record off such a refusal: `catch (error) { const conversions = stackConversionsOf(error); }`. It still answers `[]` for any other value, including a plain `Error` and a throw that is not one of these refusals. - -Nothing is accepted or refused differently. Each refusal keeps its `code`, `status`, `name`, message and `issues`, and `hasStackProvenance` still answers `false` for it. No export is added. - -Clause-②: no diff --git a/.changeset/20620-migrate-meta-review-pairing.md b/.changeset/20620-migrate-meta-review-pairing.md deleted file mode 100644 index 2a24e68f8d3..00000000000 --- a/.changeset/20620-migrate-meta-review-pairing.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os migrate meta --from N` prints the manual change that judges an applied edit beside that edit, marked review - -Clause-②: no - -Some applied mechanical edits are not the end of the job. A default flip such as -`flow-decision-mode-inclusive-explicit` writes `mode: 'inclusive'` onto a decision so -the flow keeps its old behaviour, and the manual change that judges it says the right -edit is usually none: delete the key where the branch conditions partition. That -judgment used to print hundreds of lines below the edit, among every other manual -change of the major. - -A manual change can now declare which conversions' edits it judges. In the -`Applied N mechanical change(s):` group, each run of edits by such a conversion is -followed by one line: - - ↳ review the N edits above against the manual change [protocol M] surface → replacement - -The line copies the headline the manual change prints in its own group, so its `why` -and `verify` lines are found there under the same text. Two pairs are declared today: -`flow-decision-mode-inclusive-explicit` with the decision-mode entry, and -`time-default-utc-suffix-dropped` with the time-default entry. Only declared links -pair; a manual change that merely mentions a conversion in its prose does not. - -Nothing else moves. The `N manual change(s) require your judgment:` group still lists -every manual change, byte for byte, with the same count. An edit no manual change -judges prints exactly as before. `--json`, `--out`, the exit code and the loader's -stderr are unchanged, and `--stored` is not affected. diff --git a/.changeset/20620-migrate-meta-verdict-first.md b/.changeset/20620-migrate-meta-verdict-first.md deleted file mode 100644 index 72cd5412d80..00000000000 --- a/.changeset/20620-migrate-meta-verdict-first.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os migrate meta --from N` prints the schema verdict and the refusals first, then the applied mechanical edits, then the manual changes - -Clause-②: no - -`os migrate meta --from N` prints every semantic entry of every protocol major the -chain crosses, whatever the stack uses: a `--from 17` run prints 242 manual changes. -Those used to come before the schema verdict, which was the last line of the report -and said "resolve the manual changes above". The refusals that keep the migrated -stack from parsing were not listed at all. The only refusal list was the one printed -while the config loaded, and that list names the stack as authored, including the -keys the chain goes on to convert. - -The human-readable report now prints three groups, each opened by one header line -that counts it: - -1. the verdict: `Migrated stack is schema-valid`, or - `Migrated stack does not yet pass schema validation — N refusals left after the chain` - followed by one `✗ path: message` line per refusal of the migrated stack; -2. `Applied N mechanical change(s):`; -3. `N manual change(s) require your judgment:`. - -No manual change is dropped, merged or reworded. Every applied edit and every -manual change prints byte for byte as before, in the same order within its group. -The data-migration advice is still the last thing printed. A range that holds no -migration step also leads with the verdict, which now lists the source's refusals, -and the note naming the range to use follows it. - -`--json` is unchanged: same keys, same values, same array order. The exit code is -unchanged too: a run whose migrated stack does not parse still exits 0. diff --git a/.changeset/20621-retired-key-tsc-names-retirement.md b/.changeset/20621-retired-key-tsc-names-retirement.md deleted file mode 100644 index 4b37bff5626..00000000000 --- a/.changeset/20621-retired-key-tsc-names-retirement.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): writing a retired key now fails `tsc` with an error that says the key was retired and where its migration is printed (#20621) - -Clause-②: yes - - - -**What an author sees.** Every key a `retiredKey()` tombstone declares used to be typed `undefined`, so writing one failed `tsc` with an error that named no retirement: - -``` -error TS2322: Type 'string[]' is not assignable to type 'undefined'. -``` - -Upgrading authors read those errors as typing bugs. The same line now fails like this, for arrays, objects and primitive values alike: - -``` -error TS2741: Property ''[REMOVED] Key retired: run `os validate` for its migration.'' is missing in type 'string[]' but required in type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'. -error TS2322: Type 'string' is not assignable to type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'. -``` - -A hover on the key shows the same text. `os validate` and the parse print the key's own prescription, which says what replaced the key and gives the one-line fix. - -**What changed.** The declared type of each tombstoned key, on both the input side (`z.input`, the bare `X` aliases) and the parsed side (`z.infer`, the `XParsed` aliases), is now `` { '[REMOVED] Key retired: run `os validate` for its migration.': never } | undefined `` instead of `undefined`. No value can have that object type, because its one property is typed `never`. So `tsc` still accepts only absence, as before. Both sides carry the same type, which keeps the ADR-0122 isomorphism pins true. - -**What did not change.** Runtime behaviour is the same. Each tombstone is still `z.never().optional()`. The parse error and its prescription, the text `os validate` prints, the ADR-0087 conversions, the JSON schemas and the authorable-surface artifacts are all unchanged. No export was added or removed. - -**BREAKING**: a read of a tombstoned key into a slot typed `undefined`, or typed with the key's old type, no longer compiles, because the key's declared type is now its tombstone mark. The key never holds a value, so delete the dead read. It ships as `minor` under the launch-window convention. - -**Who might notice.** Code that assigns a tombstoned key's value to a slot typed exactly `undefined` (for example `const x: undefined = page.assignedProfiles`) no longer compiles. The key never holds a value, so delete the read. The published declaration files are about 7.5% larger, because the declaration emitter writes the type out at every site of the 287 `retiredKey()` calls. diff --git a/.changeset/20622-release-aftercare-upgrade-and-unannounced-notes.md b/.changeset/20622-release-aftercare-upgrade-and-unannounced-notes.md deleted file mode 100644 index 8e45ebfd0ec..00000000000 --- a/.changeset/20622-release-aftercare-upgrade-and-unannounced-notes.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -Upgrade note for a lockfile-preserving upgrade, and the changes 17.5.0 shipped without release notes (#20622) - -Clause-②: no - -**Upgrading with a scanner.** The raised dependency floors (`hono ^4.13.5` in `@objectstack/plugin-hono-server`) cover the `hono` that objectstack loads. A lockfile-preserving upgrade can keep an older `hono` copy under `@modelcontextprotocol/sdk` (reached through `@objectstack/cli` → `@objectstack/mcp`). objectstack never loads that copy: `@objectstack/mcp` imports only the SDK's `server/mcp`, `server/stdio`, `server/webStandardStreamableHttp` and `types` modules, none of which imports `hono`. A scanner still reports it. Run `pnpm update hono` (or your package manager's equivalent) to move it to the patched line. - -**Shipped in 17.5.0 without notes.** The changesets below were in the tree 17.5.0 was published from, but its version commit did not consume them, so they shipped inside 17.5.0 without release notes. Their notes appear in this release for the first time. Breaking entries come first. - -Breaking: - -- #20458 feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name -- #20504 fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction -- #20567 feat(spec)!: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (carries two changesets) - -Also shipped: - -- #20568 fix(spec): os migrate meta guidance for fourteen more migration-entry families states each lesson in words, not tracker numbers (stage 7) -- #20572 refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean -- #20576 docs(spec): re-anchor the dead tracker citations in ui/ and two freed sites to the commits that decided them (stage 5) -- #20577 fix(spec,objectql): name the aggregated column at `having`, PostgreSQL only at `where` -- #20579 fix(spec,cli): os validate / os build read the ADR-0087 conversions defineStack applied — --json conversions and --strict see the producer's record -- #20582 feat(sdui-parser): the manifest marks the html tier's intrinsic tags `tier: 'html'`, ported from objectui's lockstep copy -- #20584 fix(plugin-security): an organization-less permission-set read resolves organization-less rows only -- #20585 fix(service-automation,metadata-protocol,metadata,runtime): withhold a flow's inbound-hook secret from every served definition, and keep it on a round trip -- #20591 fix(spec): nextUtcCalendarDay and utcInstantMs read years 0001..0099 as written, not as 1900..1999 -- #20598 fix(plugin-security): security/explain answers enforcement's refusal for a row-level policy comparing two fields of no shared comparison class -- #20605 fix(plugin-audit): describe sys_comment reactions and mentions by the shape they store -- #20606 docs(spec): re-anchor the dead tracker citations in the packages/spec/src remainder to the commits and ADRs that decided them (stage 6) diff --git a/.changeset/20628-http-node-signs-both-arms.md b/.changeset/20628-http-node-signs-both-arms.md deleted file mode 100644 index e3d1d81cf75..00000000000 --- a/.changeset/20628-http-node-signs-both-arms.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/core": minor -"@objectstack/service-automation": patch -"@objectstack/service-messaging": patch ---- - -**A flow `http` node's `signingSecret` now signs the request on every arm, with one scheme, and a secret that does not resolve refuses the node instead of letting the request leave unsigned.** - -`signingSecret` is declared as "HMAC-SHA256 secret → X-Objectstack-Signature", with no arm named. Only the durable arm honoured it, because only the messaging outbox signed. The default inline request, and a `durable: true` node on a host with no messaging HTTP outbox (which degrades to that inline request), were sent without the header while the run reported success. - -- `@objectstack/core`: **new exports** `signHttpBody(body, secret)` and `HTTP_SIGNATURE_HEADER`, the outbound HTTP signature scheme: `X-Objectstack-Signature: sha256=`, where a request with no body is signed over the empty string. They were `@objectstack/service-messaging`'s own, and they moved here so a sender with no outbox can sign with the same code. -- `@objectstack/service-messaging`: `signHttpBody` and `HTTP_SIGNATURE_HEADER` are still exported under the same names. They are now re-exports of the `@objectstack/core` bindings, not a second implementation. Delivery rows and the headers the outbox sends are unchanged. -- `@objectstack/service-automation`: the `http` node's inline request carries `X-Objectstack-Signature` whenever `signingSecret` is set. It is computed over the exact body the node sends (its JSON serialization of `config.body`, or the empty string when there is none), so a receiver that verifies with `signHttpBody` over the bytes it received accepts it on every arm. - - A non-empty `signingSecret` that renders to nothing at run time now fails the node with a guard refusal naming `config.signingSecret`, and nothing is sent. This covers a `{token}` with no value in the run, or one that renders the empty string. The refusal is on every arm, including the outbox arm, which used to enqueue such a delivery unsigned. A fault edge does not route it. The fix is to give the run the value the template reads. - - An authored `signingSecret: ''` still sends unsigned on purpose, on every arm. - -Clause-②: yes (widening) — two new exports on `@objectstack/core`'s root. Nothing is removed or renamed on any package. The one newly refused case is a node whose authored secret did not resolve, which the published contract already said signs. diff --git a/.changeset/20637-cube-refresh-key-retired.md b/.changeset/20637-cube-refresh-key-retired.md deleted file mode 100644 index 76b7b85bea4..00000000000 --- a/.changeset/20637-cube-refresh-key-retired.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: retire an analytics cube's `refreshKey` — the refresh cadence and data-change probe nothing read (#20637) - -**BREAKING** — `refreshKey` on an analytics cube (`CubeSchema`), with its `every` and `sql`, is now refused at parse: nothing ever read it, and no analytics result is cached, so a declared refresh cadence refreshed nothing. Delete the key. Every analytics query is computed when it is asked, as it always was. A refresh cadence is declared again when a result cache exists. - -Clause-②: no (narrowing) - -Measured before removal: `git grep refreshKey` over the non-test sources of `packages/services`, `packages/drivers` and `packages/rest` answered 0 lines (4 for the neighbouring `.public` in the same pathspec). `@objectstack/service-analytics` references no cache or job service; its one cache is request-scoped (dimension labels). The one in-repo author was the showcase app (`every: '1 hour'`), which no longer writes it. - -**Removed rather than enforced** (ADR-0049 enforce-or-remove; the maintainer's ruling on the card, letter C): a result cache keyed by cube, query, read scope and tenant is a subsystem with its own design, and a key that does nothing until then is the residue ADR-0049 removes. `sql` also had no safe seam: raw SQL on a schedule, outside the read scope every other cube `sql` goes through. - -## FROM → TO - -| you wrote (17.5 and earlier) | write instead | -| --- | --- | -| `refreshKey: { every: '1 hour' }` | nothing — delete the key | -| `refreshKey: { sql: 'SELECT MAX(updated_at) FROM orders' }` | nothing — delete the key | -| `refreshKey: { every: '1 hour', sql: '…' }` | nothing — delete the key | - -**The one-line fix:** delete `refreshKey` from every cube. - -**What an author who still writes it sees.** `tsc` fails at the authoring site (`Cube` types the key `never`), and the parse — `defineCube()`, `defineStack({ analyticsCubes })`, `PUT /api/v1/meta/analytics_cube/:name` — refuses it at `refreshKey` with the prescription: - -> `analytics_cube.refreshKey` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing read it: no analytics result is cached, so neither `every` nor `sql` ever refreshed anything. Delete the key; every analytics query is computed when it is asked. A refresh cadence is declared again when a result cache exists. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. - -`os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. - -## The retirement kit - -- **A `retiredKey()` tombstone** on `CubeSchema`, a `strictObject` — so the refusal carries the prescription rather than a bare unknown-key report, and `tsc` fails first. The nested `every` / `sql` shape is gone with it. `RETIRED_KEYS_BY_MAJOR[18]`: `data/Cube:refreshKey`. The key had no default, so no retired-default residue is owed. -- **The D2 conversion `cube-refresh-key-removed`** (protocol 18, retired from the load path) deletes the whole block from every `analyticsCubes[]` entry, one notice per cube, as a lossless delete. A built artifact or a stored `analytics_cube` row that carries it loads through the rehydration seams, which replay it. -- **The D3 entry `cube-refresh-key-retired`** asks the author whether anything they built assumed cube results were cached or refreshed on a schedule. They never were. -- **Ledger:** the `refreshKey.every` / `refreshKey.sql` rows collapse into one `dead` tombstone row. -- **No deprecation window**, per the project's startup-stage posture. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. - - diff --git a/.changeset/20644-dataset-answer-object.md b/.changeset/20644-dataset-answer-object.md deleted file mode 100644 index 211be8d4900..00000000000 --- a/.changeset/20644-dataset-answer-object.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -fix(service-analytics): every dataset answer names its base object as `object` (#20644) - -Clause-②: no - -**What was wrong.** `queryDataset` set `object`, the dataset's base object, only -while it built drill-through metadata, which it builds only when a drillable -dimension is selected and at least one row came back. A dimension-less (KPI) -answer, a zero-row answer and the degraded answer for an unavailable backing -object carried no `object`, and neither did a draft preview (`previewDrafts`), -grouped or not. `POST /api/v1/analytics/dataset/query` relays the service answer -as it is, so a consumer that refreshes on that object's record changes had -nothing to subscribe to for those answers. - -**What changed.** Every `queryDataset` answer carries `object`, the dataset's -`object` by machine name, whatever dimensions are selected and whether or not -rows came back, as `AnalyticsResult.object` in `@objectstack/spec` declares. A -grouped answer is unchanged: `object` sits beside the same drill-through keys -as before. A cube `query` answer still carries no `object`. diff --git a/.changeset/20646-cli-migrate-meta-migrations-entry.md b/.changeset/20646-cli-migrate-meta-migrations-entry.md deleted file mode 100644 index b8579c6667e..00000000000 --- a/.changeset/20646-cli-migrate-meta-migrations-entry.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os migrate meta` takes the migration chain from `@objectstack/spec/migrations` (#20646) - -`@objectstack/spec` moved the ADR-0087 migration chain and change-manifest names (`applyMetaMigrations`, `composeSpecChanges`, `MigrationFloorError`, `MIGRATION_MAJORS`, `MIGRATION_SUPPORT_FLOOR`, …) off the package root into the new `@objectstack/spec/migrations` entry, so the command now imports them from there. It replays the same chain and prints the same guidance; nothing a user types or reads changes. diff --git a/.changeset/20646-migrations-entry-split.md b/.changeset/20646-migrations-entry-split.md deleted file mode 100644 index 1a1ccf7d65d..00000000000 --- a/.changeset/20646-migrations-entry-split.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: the ADR-0087 migration chain leaves the package root for the new `@objectstack/spec/migrations` entry (#20646) - -**BREAKING** — the migration chain and change-manifest names (ADR-0087 D3/D4), with their types, are no longer exported from the package root `@objectstack/spec`. They are exported, unchanged, from the new entry `@objectstack/spec/migrations`. - -A `major`-class change — an existing import path stops resolving for these names — recorded as `minor` under the launch-window convention. - -**Why.** The migration registry is mostly the guidance text `objectstack migrate meta` prints, and the root re-exported it. The registry does work when its module loads (the list of majors and each step's rationale are computed then), so no bundler could prove it unused, and all of that text rode in every bundle of the root, whatever the consumer imported. This is the source-side payback of the Studio console's first-screen ceiling raise that the maintainer ruled on the 17.5.0 upgrade. Measured on the splitting PR against its merge base `1a75e39d4a` (tsup build, gzip -9): - -| | before | after | -| --- | --- | --- | -| `dist/index.js` (CommonJS root) | 3,780,033 B / 1,067,061 B gzip | 2,009,810 B / 565,386 B gzip | -| `dist/browser/index.mjs` (the ESM root a browser bundler pulls) | 3,764,293 B / 1,065,388 B gzip | 1,994,748 B / 563,787 B gzip | -| a browser bundle of the ten names the Studio console imports from the root (rolldown, minified) | 700,884 B gzip | 301,287 B gzip | - -The ADR-0087 **conversion layer stays on the root**: `defineStack` and `normalizeStackInput` read it at run time, so its names (`ALL_CONVERSIONS`, `CONVERSIONS_BY_MAJOR`, `applyConversions`, `applyConversionsToFlow`, `applyConversionsToStoredItem`, `collectConversionNotices`, the `CONVERSION_*_CODE` constants and their types) import from `@objectstack/spec` exactly as before. - -### FROM → TO - -| removed from `@objectstack/spec` | import instead from | -| --- | --- | -| `MIGRATIONS_BY_MAJOR`, `MIGRATION_MAJORS`, `MIGRATION_SUPPORT_FLOOR` | `@objectstack/spec/migrations` | -| `RETIRED_KEYS_BY_MAJOR`, `RETIRED_DEFS_BY_MAJOR` | `@objectstack/spec/migrations` | -| `applyMetaMigrations`, `composeMigrationChain`, `MigrationFloorError` | `@objectstack/spec/migrations` | -| `composeSpecChanges`, `composeReleaseChanges` | `@objectstack/spec/migrations` | -| `SpecChangesSchema`, `SpecConvertedSchema`, `SpecMigratedSchema`, `SpecSurfaceAddSchema`, `SpecSurfaceRemoveSchema`, `SpecReleaseChangesSchema`, `SpecReleaseSurfaceSchema` | `@objectstack/spec/migrations` | -| types `MigrationStep`, `MigrationApplication`, `MigrationChainResult`, `MigrationHopResult`, `MigrationTodo`, `SemanticMigration`, `SpecChanges`, `SpecConverted`, `SpecMigrated`, `SpecSurfaceAdd`, `SpecSurfaceRemove`, `SpecReleaseChanges`, `SpecReleaseSurface`, `SurfaceDiff`, `ReleaseSurfaceDiff`, `PreviousReleaseRegistries` | `@objectstack/spec/migrations` | - -**The one-line fix: change the import path.** - -```ts -// before -import { applyMetaMigrations, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec'; -// after -import { applyMetaMigrations, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec/migrations'; -``` - -The compiler finds every site: `TS2305` ("Module '"@objectstack/spec"' has no exported member …"); at run time the binding is `undefined`. Nothing else changes: the chain, its steps and semantic entries, the retired-key and retired-def tables and the change-manifest schemas are the same objects, and `objectstack migrate meta` replays the same chain. - -⚠️ **Out-of-repo consumers are NOT MEASURED beyond objectui.** Inside this repository the moved names had five importers — `os migrate meta` (the only runtime one) and four tests — all moved in the same PR. objectui at the pinned `.objectui-sha` imports none of the moved names from anywhere. The `cloud` repository was not measured. - -The ADR-0087 D3 semantic entry `migrations-entry-split` carries the judgement: an import path is TypeScript source, not metadata, so there is no source a D2 conversion could rewrite. - -Clause-②: yes (narrowing) - - diff --git a/.changeset/20647-analytics-result-object.md b/.changeset/20647-analytics-result-object.md deleted file mode 100644 index 204951b6086..00000000000 --- a/.changeset/20647-analytics-result-object.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): a dataset answer declares its base object as `object` (#20647) - -Clause-②: yes (widening) - -`AnalyticsResult` (`@objectstack/spec/contracts`) gains one optional member, -`object?: string`, and `AnalyticsResultResponseSchema` (`@objectstack/spec/api`) -mirrors it on `data`. It is the base object of the dataset the answer was -computed from, by machine name. Nothing is removed or renamed, and no existing -member changes meaning. - -**For a consumer.** Code typed against `AnalyticsResult` can read `object` from a -`queryDataset` answer without a cast, and a parse with -`AnalyticsResultResponseSchema` keeps `data.object` where it used to strip it. The -contract asks every dataset answer to carry it, whatever dimensions are selected -and whether or not rows came back. A cube query answer has no dataset behind it -and carries none. - -**Producers.** This release declares the member. `@objectstack/service-analytics` -sets it on every dataset answer once #20644 lands. diff --git a/.changeset/20648-sys-migration-flag-point-lookup.md b/.changeset/20648-sys-migration-flag-point-lookup.md deleted file mode 100644 index 503be158565..00000000000 --- a/.changeset/20648-sys-migration-flag-point-lookup.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/platform-objects': minor -'@objectstack/metadata-protocol': minor ---- - -fix(objectql,platform-objects,metadata-protocol)!: the platform's `sys_migration` primary-key lookups go through `findOne`, so an existing deployment no longer prints "Paged read of 'sys_migration' is NOT deterministic" on every boot and every `os migrate plan` (#20648) - -Clause-②: no (narrowing) - - - -The deployment ledger is read one row at a time, by primary key. Five readers -spelled that read as `find(sys_migration, { where: { id }, limit: 1 })`: the -engine's migration-gate read (`readMigrationFlagVerified`, behind -`haveFileColumnsMoved`, `isFileReferencesMigrationVerified` and -`isValueShapesMigrationVerified`), the engine's deviation marker and -creation-attestation revocation, `readDataMigrationFlag` in -`@objectstack/platform-objects/system`, and the seed-tenancy repair's receipt. -The SQL driver cannot tell that read from page one of a walk. The engine's gate -read runs at boot before the schema pass registers `sys_migration` with the -driver, and on a table the driver has not registered an unsorted paged read -warns that its pages may repeat or skip rows. Measured on a SQLite database -created by 17.4.0: every 17.5.0 boot and every `os migrate plan` printed that -warning once, for a lookup that cannot return two rows. All five readers now use -`findOne`, the single-row route the driver already exempts. The driver's check is -unchanged: an unsorted `limit` read on a table the driver did not create still -warns. - -**BREAKING**: this narrows what two published engine interfaces accept. The -first is `MigrationFlagEngine` in `@objectstack/platform-objects/system`. It is -the parameter type of `readDataMigrationFlag`, `isDataMigrationVerified`, -`mayActIrreversibly`, `recordDataMigrationRun`, `recordFileColumnMove` and -`attestFreshDatastore`, and part of `FilesToReferencesEngine` in -`@objectstack/service-storage`. The second is `SeedTenancyLedger` in -`@objectstack/metadata-protocol`, the type of a `SeedTenancySeam`'s `ledger`. -Each now requires `findOne` where it required `find`, so a hand-written stand-in -that provides only `find` no longer satisfies either type. It ships as `minor` -under the launch-window convention for accept-set narrowings. The ObjectQL engine -has both methods, so a host that passes the engine needs no change. - -**Your fix:** a stand-in that implemented `find` for these helpers implements -`findOne(object, options)` instead, answering the row whose `where.id` matches, -or `null`. - -At run time, a stand-in that still provides only `find` fails the read. -`readDataMigrationFlag` then answers `null`, the same answer as a missing row, so -the gates it feeds stay closed. `resolveSeedTenancySeam` now attaches a `ledger` -only for a host that has `getObject`, `findOne`, `insert` and `update`. For a -find-only host the seam's `ledger` is `undefined`, and when the seed-tenancy -repair applies, it says at `warn` that it could not record its receipt. diff --git a/.changeset/20649-div-entry-compile-condition.md b/.changeset/20649-div-entry-compile-condition.md deleted file mode 100644 index 9e4e174bd32..00000000000 --- a/.changeset/20649-div-entry-compile-condition.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the `ui-html-page-div-refused` migration entry states when `dev` and `start` compile (#20649) - -Clause-②: no - -The entry's `reason`, which `objectstack migrate meta` prints as its `why:` -line, said `dev` and `start` run `objectstack compile` first. They run it before -they boot only when the artifact is missing or `--compile` is passed, and `dev`'s -watch mode runs it when a watched file changes. The text now says so. No schema -accepts or refuses anything it did not before. diff --git a/.changeset/20653-stale-authored-plugin-bundle-leaves.md b/.changeset/20653-stale-authored-plugin-bundle-leaves.md deleted file mode 100644 index a75d115fab6..00000000000 --- a/.changeset/20653-stale-authored-plugin-bundle-leaves.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/plugin-webhooks': patch -'@objectstack/plugin-audit': patch -'@objectstack/plugin-security': patch ---- - -fix(plugin-webhooks,plugin-audit,plugin-security): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20653) - -Clause-②: no - -A translated object leaf that a translator wrote by hand is kept as written -when its English source changes later, so some leaves went on saying what the -old source said. On a ja-JP, es-ES or zh-CN console the `sys_webhook` record -page told an admin that `definition_json` carries the full headers / auth / -retry / payload configuration, where the English help says credentials are not -stored there: the signing secret and the custom headers live in the encrypted -`signing_secret` and `headers_secret` fields. - -Eighteen leaves (six paths, in all three locales) whose meaning contradicted -the current English now match it: - -- `@objectstack/plugin-webhooks`: the `sys_webhook.definition_json` help (no - credentials in the JSON) and the `sys_webhook` description (dispatched by the - webhook auto-enqueuer onto the shared HTTP outbox, not executed by an HTTP - connector plugin; declared through `defineStack({ webhooks })` too); -- `@objectstack/plugin-audit`: the `sys_activity.environment_id` label and help - (Environment, not Project), and the `sys_audit_log.user_id` label (User: the - object's separate `actor` field is the actor); -- `@objectstack/plugin-security`: the `sys_position` description (positions - distribute capability, not definitions for RBAC access control). - -Leaves whose English source only gained detail, was reworded, or was -title-cased (the `@objectstack/plugin-approvals` status and action options) -are unchanged. Values only: no key is added or removed, and no provenance -table changes. diff --git a/.changeset/20654-flow-credential-literal-advisory.md b/.changeset/20654-flow-credential-literal-advisory.md deleted file mode 100644 index 152910800ae..00000000000 --- a/.changeset/20654-flow-credential-literal-advisory.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/spec': patch ---- - -A credential typed as a literal into a flow position that every flow reader is served now draws one `flow-credential-literal` warning at `os validate`, `os build`, `os lint` and the runtime publish gate, and the spec describes of those positions route an outbound credential to a declarative connector's `credentialRef` (#20654). - -Clause-②: no - -**Why.** A flow definition is served, as authored, to every member who can read flows. The flow read path withholds the credential slots the spec declares, but it cannot withhold a value inside an open map or a url, because it cannot tell a credential there from an ordinary value. The supported home for an outbound credential is a declarative connector: its `auth: { type, credentialRef }` names a secrets-layer reference that is resolved at boot and never stored in metadata. - -**What the warning covers.** An `http` node's `config.headers` entry, a query parameter of an `http` node's `config.url`, and a node's `connectorConfig.input` at any depth, including nodes inside `try_catch`, `loop` and `parallel` regions. A value draws when it is a non-blank string with no `{…}` template, and either its name reads as a credential (`Authorization`, `Cookie`, `x-api-key`, a name carrying `token`, `secret`, `password` and similar) or it opens with an auth scheme (`Bearer`, `Basic`, `Token`, `Digest`, `ApiKey`) followed by a value. A `{variable}` template is resolved per run and draws nothing. - -**What it does not do.** It never refuses: every finding is a `warning`, and a save, validate, build or lint that passed before still passes (`--strict` promotes it, as it promotes every warning). It never echoes the value it names. Nothing is withheld on any read. - -**Fix, by where the credential sits.** Declare a `connectors:` entry with a `provider` and call it from a `connector_action` node. A header credential goes to `auth: { type: 'bearer', credentialRef }`, or to `auth: { type: 'api-key', headerName, credentialRef }` for a key in a named header. A key in the url's query string goes to `auth: { type: 'api-key', paramName, credentialRef }`. On a connector node, drop the credential from `input`: the connector authenticates through its own `auth.credentialRef`. - -`@objectstack/lint` exports the rule `lintFlowCredentialLiterals`, its id `FLOW_CREDENTIAL_LITERAL`, and the one predicate it asks, `isCredentialShapedLiteral(name, value)`. In `@objectstack/spec`, only the descriptions of `HttpConfigSchema.headers` and a flow node's `connectorConfig.input` change; no shape changes. diff --git a/.changeset/20661-memory-analytics-lte-whole-day-first.md b/.changeset/20661-memory-analytics-lte-whole-day-first.md deleted file mode 100644 index 1b46707f74b..00000000000 --- a/.changeset/20661-memory-analytics-lte-whole-day-first.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/driver-memory': patch ---- - -fix(driver-memory): a cube `where` `$lte` on a bare day keeps the whole day on a declared `datetime` field (#20661) - -Clause-②: no - -`MemoryAnalyticsService` put a `$lte` comparand into the field's storage form before it applied the whole-day rule for a bare-day upper bound. On a field declared `datetime` (through `syncSchema`) the storage form of `'2026-07-28'` is the instant `'2026-07-28T00:00:00.000Z'`, and the whole-day rule does not widen an instant. So `where: { created_at: { $lte: '2026-07-28' } }` compiled an inclusive bound at that midnight and dropped every row later in the named day, while `find()` with the same filter kept them. `generateSql()` echoed the same narrowed bound. - -Both exits now follow ADR-0053's order: the bare day is widened first, and only the resulting bound is converted to the storage form. On a declared `datetime` field the example compiles `created_at < '2026-07-29T00:00:00.000Z'` and answers the same rows as `find()`. On `9999-12-31`, the last supported day, a declared `datetime` field now asks only for a value (`IS NOT NULL` in the echo), as an undeclared field already did. - -Unchanged: an undeclared field, a declared `date` field, a full timestamp or `Date` comparand (inclusive, as written), and a `timeDimensions[].dateRange` end, which already widened the day before building its bounds. `$between` stays refused on this face (`INVALID_FILTER`, 400). Nothing is removed or renamed, and there is nothing to migrate. diff --git a/.changeset/20662-null-key-todo-reason.md b/.changeset/20662-null-key-todo-reason.md deleted file mode 100644 index b1b4121705a..00000000000 --- a/.changeset/20662-null-key-todo-reason.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the stored-filter conversion's TODO for a null-valued key is true on every block, and no longer tells the operator to drop the key - -The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` leaves a record-form filter with a `null`-valued key as stored and reports it as a TODO, which `os migrate meta --stored` lists. The TODO's reason used to say the renderer skips that key, so it "constrains nothing", and to "Drop the key". That holds only where the block queries an object. Where the block's rows are inline (`data: { provider: 'value' }` or `staticData`), the objectui version this repository pins matches the key against the rows and selects the rows whose value is null, so following the advice there widened what the block shows. - -The reason now states both behaviours, says no one rule keeps both, and leaves the choice to the operator. For a stored `{ owner_id: null }` it names the rule `{"field":"owner_id","operator":"is_null"}` for the rows with no `owner_id` value, and says that a filter leaving `owner_id` unconstrained has no rule for it. The protocol-18 migration entry `element-data-source-and-object-block-filter-rule-array` says the same. - -The TODO for a key set to an empty operator object (`{ amount: {} }`) also said it "constrains nothing". The renderer refuses it instead: where the block queries an object it refuses the filter with `INVALID_FILTER` (400), and where the block's rows are inline it shows no rows. The reason now says that, and keeps its advice to drop the key, which is the renderer's own remedy. - -Nothing else changes. Both filters are still left exactly as stored and still reported as a TODO, on any block. No schema, conversion verdict or exit code moves. - -Clause-②: no diff --git a/.changeset/20666-stale-authored-metadata-form-leaves.md b/.changeset/20666-stale-authored-metadata-form-leaves.md deleted file mode 100644 index eecbc92142b..00000000000 --- a/.changeset/20666-stale-authored-metadata-form-leaves.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -fix(platform-objects): the ja-JP, es-ES and zh-CN metadata-form descriptions, help texts and labels that contradicted their current English source are re-translated (#20666) - -Clause-②: no - -A translated metadata-form leaf that a translator wrote by hand is kept as -written when its English source changes later, so some leaves went on saying -what the old source said. On a ja-JP or es-ES console the permission-set form's -Tab & Row-Level Security section still offered custom context variables, and -the agent form's Capabilities section still offered tools; `en` dropped both -when the keys were removed. - -Twelve leaves whose meaning contradicted the current English now match it: - -- all three locales: the agent form's Capabilities section (skills and - knowledge sources, no tools), the permission-set form's Tab & Row-Level - Security section (tab visibility and RLS policies: ja-JP and es-ES no longer - offer custom context variables, and zh-CN no longer names the section after - sharing rules), and the report form's `blocks` help (dataset-bound - sub-reports, not a join of several objects); -- ja-JP and es-ES: the email-template form's Identity section (the template is - resolved by its `name` through `IEmailService.sendTemplate`, not by an `id`, - and the section carries no content type); -- zh-CN: the report form's Joined blocks section label, which named the section - after related objects. - -Leaves whose English source only gained detail or was reworded, without -retracting what the translation says, are unchanged. Values only: no key is -added or removed, and no provenance table changes. diff --git a/.changeset/20671-time-write-zone-less.md b/.changeset/20671-time-write-zone-less.md deleted file mode 100644 index e4aed71eec6..00000000000 --- a/.changeset/20671-time-write-zone-less.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -"@objectstack/objectql": minor -"@objectstack/spec": patch ---- - -fix(objectql)!: a `time` field is a zone-less wall clock — a time of day written with a `Z` or an offset (`"10:00Z"`, `"10:00+08:00"`), and an instant whose UTC year has no four-digit spelling (`"+010000-01-01T10:00:00Z"`), are refused with `VALIDATION_FAILED` / 400 (`invalid_time`) instead of being stored verbatim on memory and SQLite and read back differently, or failing with a 500, on PostgreSQL (#20671) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a `time` field accepts as a written value. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). - -The record validator's `time` arm now asks `@objectstack/core`'s one temporal rule, the same one the `time` filter comparand door asks, so a value is refused as a written `time` exactly when it is refused as a `time` comparand. It reads two things: a bare wall clock `HH:MM[:SS[.fraction]]` in range, and an instant in one of the ISO 8601 spellings a `datetime` is written in, on a calendar day that exists, whose UTC year has four digits (its UTC time of day is stored). Everything else is refused with `VALIDATION_FAILED` / 400 and the field code `invalid_time`, naming the field, on insert, update, a multi-row update and `engine.validate`, before anything is written. - -Refused now, where they were accepted: - -- **A time of day with a zone suffix**: `"10:00Z"`, `"10:00+08:00"`, `"10:00:00+0800"`, `"10:00:00.250Z"`. A `time` field carries no zone. The refusal has its own sentence, which `@objectstack/spec`'s validation-message catalog now carries in all four locales (`invalid_time_zoned`; English: "… is a time of day with no time zone: drop the Z or offset (HH:MM or HH:MM:SS), or use a datetime field for an instant"). The wire code stays `invalid_time`. -- **An instant the rule does not read as a time of day**: an extended year (`"+010000-01-01T10:00:00Z"`, or a `Date` of it), an instant whose UTC year is 10000 (`"9999-12-31T23:00:00-02:00"`), a day that does not exist (`"2026-02-30T10:00:00Z"`), and a spelling the `datetime` arm already refuses (`"2026-07-15 10:00Z"`, a space and a zone; `"2026-07-15t10:00:00z"`, lower case). - -What a caller sees, before and after, through `POST /api/v1/data/:object` and a read-back, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`: - -| written to a `time` | memory | SQLite | PostgreSQL | now, on all three | -|:--|:--|:--|:--|:--| -| `"+010000-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` | 201, read back as written | the same | 500 `DATABASE_ERROR` | 400 `invalid_time` | -| `"10:00Z"`, `"10:00+08:00"`, `"10:00:00+0800"` | 201, read back as written | the same | 201, read back `"10:00:00"` | 400 `invalid_time`, the zone sentence | -| `"2026-07-15 10:00Z"` | 201, `"10:00:00"` | the same | the same | 400 `invalid_time` | - -**Who is affected.** A caller that writes a `time` field as a string with a `Z` or an offset, or as an out-of-range instant: a REST or SDK client, a flow, an MCP `create_record` / `update_record` call written by a model. A row already stored with such a value keeps it; nothing rewrites it. PostgreSQL stored a zone-suffixed time of day as its bare wall clock, so only a memory or SQLite deployment can hold one. An update that omits the field is not affected; one that sends the old value back is refused, naming the field. A `time` field whose literal `defaultValue` carries a `Z` or an offset has each insert that falls back to that default refused the same way. The server import (`POST /api/v1/data/:object/import`) turns a `time` cell into `HH:MM:SS` itself before the write, and already refused a zone-suffixed time-of-day cell, so its cells are unchanged. - -**Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through REST: - -- a bare wall clock: `"10:00"` and `"10:00:00"` read back `"10:00:00"`, `"10:00:00.250"` reads back `"10:00:00.250"`; -- a full ISO instant with a four-digit year, stored as its UTC time of day: `"2026-07-15T10:00:00Z"` and `"2026-07-15T18:00:00+08:00"` read back `"10:00:00"`; -- a `Date` with a four-digit year, still accepted; an epoch-millisecond number, a `{placeholder}` and an out-of-range clock (`"25:00"`), still refused with `invalid_time`; -- every `date` and `datetime` value, and every filter comparand. diff --git a/.changeset/20676-mount-flow-clone.md b/.changeset/20676-mount-flow-clone.md deleted file mode 100644 index 85c880c041d..00000000000 --- a/.changeset/20676-mount-flow-clone.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): `POST /api/v1/automation/:name/clone` is served over HTTP - -Clause-②: no - -Cloning a flow under a new machine name (ADR-0126 §7.1) is how an admin customizes a packaged -flow whose base is locked. The runtime implemented the clone, but the dispatcher never mounted -its route, so every clone answered `404 ENDPOINT_NOT_FOUND` before the request reached it: from -the API, and from the Clone dialog on Setup's packaged-automation page, for every caller and -every body. - -The route is now mounted beside `POST /automation/:name/toggle`, at `/api/v1/automation/:name/clone` -and, when environment scoping is enabled, at `/api/v1/environments/:environmentId/automation/:name/clone`. -It answers what the clone implementation already answered: `200 { flow, notice }` for a legal -clone, `400` for a missing or illegal `name` or `label`, `404` for an unknown source flow, -`409 RESOURCE_CONFLICT` for a name already in use, `401` for an anonymous caller and `403` for a -caller without `manage_metadata`. No request or response shape changed. diff --git a/.changeset/20677-ledger-disabled-stays-unbound.md b/.changeset/20677-ledger-disabled-stays-unbound.md deleted file mode 100644 index c258c02e503..00000000000 --- a/.changeset/20677-ledger-disabled-stays-unbound.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(service-automation): a flow switched off in the activation ledger stays unbound after a restart, and a trigger-fired refusal no longer logs an ERROR claiming a run-history row (#20677) - -Clause-②: no - -**What was wrong.** A packaged flow switched off through the ADR-0126 activation -ledger (`POST /api/v1/automation/:name/toggle` with `enabled: false`) came back -`bound: true` after every cold restart. Its runs were still refused, so the switch -itself held, but its trigger was armed again. At boot the automation service pulls -the flows and applies the ledger, which leaves a switched-off flow unbound. The -trigger plugins register later, at `kernel:ready`, and registering a trigger armed -every matching flow without asking whether it may run. So `GET -/api/v1/automation/_status` reported the flow `enabled: false, bound: true`. Each -matching event also logged `ERROR Trigger-fired run of flow '…' failed`, saying the -failure "is recorded in the flow's run history", while no run row was written. - -**What changed.** - -- The engine checks whether a flow may run in one place: at the step that arms a - trigger. Every arming path goes through it: flow registration (boot pull, - publish, hot reload), trigger registration, and the enable toggle. A flow that - either disable dimension switches off (the activation ledger, or an `obsolete` / - `invalid` status) is never armed, whenever its trigger registers. -- Re-enabling a flow arms it on its trigger as before. Re-enabling the ledger bit - of a flow whose `status` is still `obsolete` or `invalid` no longer arms it, since - every run it fired would be refused. -- A trigger-fired run refused because the flow is disabled (for example, an event - already in flight when the flow was switched off) is logged at `info`, saying - nothing ran and no run-history row records it. It is no longer an `ERROR`. -- The `ERROR` line for any other trigger-fired failure says the failure is - recorded in the run history only for a run that dispatched and failed. A run - refused before it dispatched gets the same line without that claim. - -**What is not affected.** The runtime refusal (`FLOW_DISABLED`) and its message are -unchanged. The enabled flows beside a disabled one arm exactly as before. No export, -option, route or response shape changes. diff --git a/.changeset/20678-subflow-disable-parked-run.md b/.changeset/20678-subflow-disable-parked-run.md deleted file mode 100644 index 02d8cdc0010..00000000000 --- a/.changeset/20678-subflow-disable-parked-run.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -fix(service-automation)!: disabling a packaged subflow completes once its packaged callers are switched off and hold no parked run, and the refusal names the parked runs and the cancel door (#20678) - -Clause-②: yes (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. `toggleFlow(name, enabled)` on the automation service, and so `POST /api/v1/automation/:name/toggle`, now accepts some disables it used to refuse (the widening) and refuses one corner of enables it used to accept (the narrowing). - -**The disable direction (the widening).** Disabling a packaged flow that a packaged flow calls as a subflow (the `flowName` of a `subflow` or `map` node) was refused while any such caller existed, even one already switched off. So the refusal's own remedy, "disable the calling flow first", could never complete. A caller now guards the disable only while it can still reach the subflow node: - -- **An enabled caller** guards, as before. The refusal names it, and the step is to disable it first. -- **A disabled caller** (switched off in the activation ledger, or disabled by its definition's `status`) guards only while it holds a **parked run**: a run paused at a wait, an approval, a screen, or at a `map` node between items. Switching a flow off stops its new runs only, and a parked run still resumes into its subflow node. The refusal names each parked run id and the operator cancel door, `POST /api/v1/automation/:name/runs/:runId/cancel` (ADR-0044). Cancel those runs, or let them finish, and the disable completes. -- **A disabled caller with no parked run** no longer guards, so "disable the caller, then the callee" completes. - -Parked runs are read from both the in-process runs and the durable suspended-run store, including runs a previous process parked. If the durable store cannot be listed at that moment, the disable fails with the store's own error and nothing is written; it is never read as "no parked run". The refusal keeps `DELETE_RESTRICTED` / `409` and its `subflowCallers` list, which now names exactly the callers that guard. - -**The enable direction (the narrowing).** A subflow in a cycle of ledger-switched-off flows with the flow being enabled was skipped whole, even when its definition's `status` also disabled it. So the enable was accepted onto a subflow that stays disabled, and the publish remedy was never named. Such a subflow is now named, with both reasons and both steps (publish it with status `active`, then enable it). The cycle exemption covers the activation switch only, because no enable order changes a status. diff --git a/.changeset/20678-subflow-disable-sequence.md b/.changeset/20678-subflow-disable-sequence.md deleted file mode 100644 index 10e8bd53c9d..00000000000 --- a/.changeset/20678-subflow-disable-sequence.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -fix(service-automation)!: re-enabling a packaged flow is refused while a packaged subflow it calls is disabled, and the refusal names a remedy that subflow's state admits (#20678) - -Clause-②: no (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. `toggleFlow(name, true)` on the automation service, and so `POST /api/v1/automation/:name/toggle` with `{"enabled": true}`, now refuses an enable it used to accept. - -**What changed.** A packaged flow switched off in the activation ledger could be switched back on while a packaged flow it calls (the `flowName` of a `subflow` node, or of a `map` node) was itself disabled. The enable was accepted, and every run of the flow then failed at that node on the child's `FLOW_DISABLED` refusal. That enable is now refused with `RESOURCE_CONFLICT` / `409`, before anything is written: the ledger row still reads off, the trigger stays unbound, and runs are still refused. The message names each disabled subflow and what holds it off, and the remedy follows from that: - -- **Switched off in the activation ledger**: enable that subflow first, then this flow. -- **Disabled by its own definition's `status`** (`obsolete` or `invalid`): the activation switch never changes a status, so enabling the subflow through it would change nothing. Publish the subflow with status `active` (for a package that is read-only in this environment, that takes a package version that ships it active), then enable this flow. - -**Not refused:** - -- Enabling a flow that is already enabled. Nothing is re-armed. -- A subflow the customer authored. A flow the customer authored is not this switch's to enable at all: the activation switch switches packaged flows only, and it refuses a customer-authored flow for that reason before this guard is asked (see the entry "the toggle door refuses a flow no package ships, naming its status switch"). -- A subflow in a cycle of switched-off flows with the flow being enabled, including a flow that calls itself. Each flow in such a cycle would refuse the others, so no order could complete. A subflow in such a cycle whose definition's `status` also disables it is still named, with its publish remedy: no enable order changes a status. - -The disable direction of the same guard is described in its own entry, "disabling a packaged subflow completes once its packaged callers are switched off and hold no parked run". diff --git a/.changeset/20679-automation-door-package-lock.md b/.changeset/20679-automation-door-package-lock.md deleted file mode 100644 index 0f7fe590958..00000000000 --- a/.changeset/20679-automation-door-package-lock.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/runtime': patch -'@objectstack/metadata-protocol': minor ---- - -fix(runtime): the `/automation` write doors refuse a packaged flow, as the metadata door does (#20679) - -Clause-②: yes (widening) - -A flow that a code package ships has a locked base (ADR-0126 §2): changing or removing it in place is refused. `PUT /api/v1/meta/flow/:name` already refused it. The two `/automation` definition doors did not: an administrator holding `manage_metadata` could rewrite a packaged flow in the live engine with `PUT /api/v1/automation/:name` or with `POST /api/v1/automation` under its name (a create onto an existing name overwrites it), or remove it with `DELETE /api/v1/automation/:name`. - -All three now answer a packaged flow with the same code and status the metadata door gives (`403` `NOT_OVERRIDABLE`), and with the same sentence wherever the metadata protocol's own package door answers. The refusal comes before the engine is called, so nothing is registered or removed. On `DELETE`, it also comes before the engine's own `DELETE_RESTRICTED` / `409` for a packaged subflow that packaged callers still reach. - -What is not refused: - -- A flow that no code package ships, including a flow created with `POST /api/v1/automation` or authored through the metadata door. It is updated and removed as before. -- `POST /api/v1/automation/:name/clone`, which copies a packaged flow under a new name. This is the supported way to customize one (ADR-0126 §7.1). -- `POST /api/v1/automation/:name/toggle`, the switch that turns a packaged flow on or off (ADR-0126 §7.2). -- A deployment that sets `OS_METADATA_WRITABLE=flow`. It opens both doors, as the refusal message says. - -**The widening.** `@objectstack/metadata-protocol` gains one public method, `ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name, operation })`. It returns the refusal the metadata door would give for writing (`'save'`) or removing (`'delete'`) an existing item because a code package ships it, or `null` when that door would not refuse on this ground. `saveMetaItem` and `deleteMetaItem` call the same code, so the two doors cannot disagree. Their own refusals are unchanged. diff --git a/.changeset/20680-metadata-protocol-packaged-dashboard-base.md b/.changeset/20680-metadata-protocol-packaged-dashboard-base.md deleted file mode 100644 index 38569ea35f9..00000000000 --- a/.changeset/20680-metadata-protocol-packaged-dashboard-base.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol): `getPackagedDashboardBase(name)` answers the dashboard a code package ships, before any overlay (#20680) - -`ObjectStackProtocolImplementation` gains `getPackagedDashboardBase(name)`, the dashboard twin of `getPackagedObjectBase`. It returns the packaged (code-layer) declaration of a dashboard, which is the `packagedBase` that `translateDashboard` compares a served dashboard against, so that a tenant's published overlay is not overwritten by the packaged translation catalog. - -It reads the artifact registry's code-package entry only. An overlay that was hydrated under the plain registry key can therefore never be returned as the base it is compared against. It returns `undefined` for a dashboard that no code package ships, for an unknown or empty name, and for a registry that cannot answer. A caller treats `undefined` as "no base known", and the catalog applies as before. - -The method is additive. No existing read changes answer: the item and list reads already serve a published org overlay by the same identity the write stored (`type`, name, `package_id`, organization). Nothing is removed or renamed. diff --git a/.changeset/20680-spec-dashboard-catalog-override.md b/.changeset/20680-spec-dashboard-catalog-override.md deleted file mode 100644 index 279b99995a4..00000000000 --- a/.changeset/20680-spec-dashboard-catalog-override.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -fix(spec): `translateDashboard` lets an explicit override beat the packaged catalog when it is handed the packaged base (#20680) - -`translateDashboard` (`@objectstack/spec/system`) now follows the rule ADR-0029 D9.2a records for objects: an explicit override beats a packaged default. When the caller supplies `packagedBase` (the dashboard as its code package ships it, before any tenant overlay), a catalog string replaces a served string only while that string still equals its packaged counterpart. The comparison is made per string: the dashboard `label` and `description`, each widget's `title`, `description` and sub-caption (`options.description`), each global filter's `label`, and each static option `label`. Each string is matched by the key the bundle addresses it by (widget `id`, filter key, option value). A widget, filter or option that the packaged base does not carry counts as authored, so its strings keep their values. - -Why: an org overlay on a packaged dashboard (ADR-0126 Regime O) published, and `?layers=true` reported it as effective, but the served widget title stayed the shipped one whenever the dashboard's bundle carried that title. The platform's `system_overview` is one such dashboard, because `platform-objects` ships an `en` bundle that repeats every widget title. The catalog translated the packaged declaration and was applied over the tenant's edit. - -What changes for a caller: - -- `translateDashboard`'s third parameter is typed `TranslateDocumentOptions` (was `ResolveOptions`). That type is `ResolveOptions` plus the optional `packagedBase`, and `translateMetadataDocument` already passed it through. Every existing call compiles unchanged. -- Without `packagedBase` (`undefined` or `null`), the output is byte-identical to before: the catalog applies. No serving layer in this release passes a dashboard base yet, so no served dashboard changes answer with this package alone. -- An edit back to exactly the shipped string is a no-op: the catalog still translates it. - -Nothing is removed or renamed, and there is nothing to migrate. diff --git a/.changeset/20681-dev-no-watch.md b/.changeset/20681-dev-no-watch.md deleted file mode 100644 index 2118265bd00..00000000000 --- a/.changeset/20681-dev-no-watch.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os dev --no-watch` turns watch mode off, and `os dev` fails when its PACKAGE argument selects no workspace package - -Clause-②: no - -`os dev` watches `objectstack.config.ts` and `src/` by default, and it already -had a branch for running without that watcher. No argument reached it: - -- `os dev --no-watch` was refused as a nonexistent flag (exit 2). -- `os dev --watch=false` is not a form the CLI reads for a boolean flag. It - parsed as `--watch` plus the PACKAGE argument `false`, so the command switched - to monorepo mode, ran `pnpm --filter false dev`, printed "No projects found", - and exited 0 with nothing started. - -What changes: - -- `os dev --no-watch` boots the environment with the watch-and-rebuild loop off. - Plain `os dev` keeps it on, as before. -- In monorepo mode, a PACKAGE argument that selects no workspace package now - exits 1, and the failure line names the value. The command passes - `--fail-if-no-match` to pnpm, so pnpm decides whether the filter matched, for - every filter form it accepts. `os dev --watch=false` is one such run: it now - fails instead of reporting success. Write `--no-watch` instead. -- `os dev --no-watch` in monorepo mode (a PACKAGE argument, or a workspace root - with no `objectstack.config.ts`) exits 1 and names the flag. In that mode each - package's own `dev` script decides whether it watches, so the CLI cannot turn - watching off. Run it in the project directory, or pass `--artifact`. - -Monorepo mode now needs pnpm 8.13.1 or later, the release that added -`--fail-if-no-match`. diff --git a/.changeset/20686-pure-plugin-skips-strings.md b/.changeset/20686-pure-plugin-skips-strings.md deleted file mode 100644 index ff94518bc4a..00000000000 --- a/.changeset/20686-pure-plugin-skips-strings.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the published bundles' text equals the source again — the build marks pure calls only, never a string (#20686) - -The build annotates each call of `lazySchema`, `strictObject` and `defineForm` as pure so a consumer's bundler can drop the schemas it never reaches. The rule that placed those annotations also rewrote a marked name quoted inside a string, so one D3 migration entry of protocol 18 (`dashboard-widget-stage-order-non-funnel-refused`) shipped a build-time comment marker inside its `acceptanceCriteria` text in `@objectstack/spec/migrations`, where its source reads `` `z.strictObject(DashboardWidgetSchema.shape)` ``. `os migrate meta` prints that text as the entry's `verify:` line when protocol 18 is the migration target. The published value now equals the source, character for character. - -The annotations now come from the TypeScript parse of each file, so a marked name inside a string, template text, a comment or a declaration is never touched. Every call that carried an annotation still carries one, and the published code is otherwise unchanged. One call, `z.strictObject(…)` in the Turso driver config schema, now has its annotation in front of the call rather than after the `z.`, where esbuild had been dropping it. Measured with esbuild, a consumer that imports one schema from `@objectstack/spec/data` or `defineStack` from the root gets a bundle of the same size as before. - -Clause-②: no diff --git a/.changeset/20694-object-grid-description-empty-state-keyboard-navigation.md b/.changeset/20694-object-grid-description-empty-state-keyboard-navigation.md deleted file mode 100644 index 7ab8dd707c6..00000000000 --- a/.changeset/20694-object-grid-description-empty-state-keyboard-navigation.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -The `object-grid` page block now declares `description`, `emptyState` and `keyboardNavigation`, so a page that authors them validates clean instead of having each reported as a prop the block does not declare (#20694). - -Clause-②: yes (widening) - -- **`description`** — an `I18nLabel` (a string, or an inline locale map): one line of help text the grid draws above its rows, resolved against the display locale. -- **`emptyState`** — `{ title?, message?, icon? }`, what the grid draws instead of an empty table. It is the list view's own empty-state shape, now exported as `EmptyStateSchema` (author type `EmptyState`) and taken by reference on both `list-view` and `object-grid`, so the two cannot drift apart. -- **`keyboardNavigation`** — a boolean, marked `[EXPERIMENTAL — not enforced]`: arrow-key cell navigation on the WAI-ARIA grid pattern, on by default when `editable` is set. No renderer reads it yet, so authoring it changes nothing today. - -Nothing that parsed before is refused now. The `object-grid` row still refuses every key it does not declare, and the list view's `emptyState` accepts exactly the values it accepted before the shape was extracted. One refusal message is reworded: an `action` or `button` key inside an empty state is still refused and still points at the list view's `addRecord` block, now phrased so that it also reads true on `object-grid`, which has no add-record block. - -On `object-grid`, write `emptyState.title` and `emptyState.message` as plain strings for now: the grid renderer draws both as they are and does not yet resolve an inline locale map there the way it resolves `description`, so a map in either member fails to render. - -Where it surfaces: the component-props gate (`os validate`, `os build`, `os lint`) no longer reports these three keys on an `object-grid` node, and the published JSON Schema for `ObjectGridProps` describes them. diff --git a/.changeset/20696-migrate-meta-strict-factories.md b/.changeset/20696-migrate-meta-strict-factories.md deleted file mode 100644 index e90bba3ba15..00000000000 --- a/.changeset/20696-migrate-meta-strict-factories.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os migrate meta` converts an object built with `ObjectSchema.create(…)` instead of stopping at load when the object carries a retired key - -Clause-②: no - -`os migrate meta` reads a config the current schema refuses, so it can rewrite the -retired keys in it. It did that for artifacts built with a `define*` helper and for -plain object literals. It did not do it for artifacts built with a factory such as -`ObjectSchema.create(…)`, which validates when it is called. An object like this: - -```ts -ObjectSchema.create({ - name: 'ticket', - fields: { title: { type: 'text' } }, - tenancy: { enabled: true, organizationField: 'organization_id' }, -}) -``` - -stopped `os migrate meta --from 17` at load with exit 1 and a raw JSON array of -validation issues. The message in that array told the author to run -`os migrate meta --from 17`. - -The command now loads it, applies the conversion (here -`object-tenancy-organization-field-removed`), and reports `schemaValid` for the -migrated stack, exactly as it does for the same object written as a plain literal. -This covers the five factories in `@objectstack/spec` that validate when called: -`ObjectSchema.create` (`@objectstack/spec/data`) and `App.create`, -`Dashboard.create`, `Report.create` and `Action.create` (`@objectstack/spec/ui`). -The other `create` factories spec exports return their argument unchanged and -never refused anything, so nothing changes for them. - -A schema problem the migration cannot fix is still reported: it is listed among -the refusals under the verdict, and `schemaValid` is `false`. A check that only -the factory makes when it is called, such as `ObjectSchema.create` refusing a -`managedBy: 'system-data'` object that grants no create, edit or delete, is not -part of the stack schema. It is reported on the stderr line described below and -does not change `schemaValid`, the same as `defineStack`'s own call-time checks. -`os validate` still refuses it. - -While the config loads, `os migrate meta` prints one stderr line for each -artifact the current schema refused. A raw validation error on that line is now -printed as a block, for example `ObjectSchema.create validation failed (1 issue):` -followed by one `✗ path: message` line per issue, instead of a raw JSON array. -This also applies to `define*` helpers that throw a raw validation error, such -as `defineAgent`. - -Nothing else changes. `os validate`, `os build` and every other command still -refuse the retired key at load, with the same message. `ObjectSchema.create` and -the other factories stay strict everywhere outside `os migrate meta`. The keys -of the `--json` payload are unchanged, and a run whose migrated stack does not -parse still exits 0. diff --git a/.changeset/20697-semantic-migration-conversion-ids.md b/.changeset/20697-semantic-migration-conversion-ids.md deleted file mode 100644 index 31403cca39c..00000000000 --- a/.changeset/20697-semantic-migration-conversion-ids.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): a semantic migration names the D2 conversions whose applied edits it judges - -Clause-②: yes (widening) - -`SemanticMigration`, the ADR-0087 D3 entry type exported by `@objectstack/spec`, -gains one optional member, `conversionIds?: readonly string[]`. It lists the ids -of the D2 conversions whose applied edits the entry judges. Each id is the same -`conversionId` that the conversion's `MigrationApplication` rows carry, so a -printer of an `applyMetaMigrations` result can show the entry beside those edits -for review. The chain copies the field onto the entry's `MigrationTodo`, so -`objectstack migrate meta --json` shows it on that todo. Nothing is removed or -renamed, and every entry is still reported as a todo of its hop. - -One link ships: `flow-decision-edge-branching-first-match` judges the -`mode: 'inclusive'` edits that `flow-decision-mode-inclusive-explicit` writes. diff --git a/.changeset/20700-analytics-result-drill-sidecars.md b/.changeset/20700-analytics-result-drill-sidecars.md deleted file mode 100644 index f5f35b7581a..00000000000 --- a/.changeset/20700-analytics-result-drill-sidecars.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): a dataset answer declares its four drill-through sidecars (#20700) - -Clause-②: yes (widening) - -`AnalyticsResult` (`@objectstack/spec/contracts`) gains four optional members, and -`AnalyticsResultResponseSchema` (`@objectstack/spec/api`) mirrors them on `data`: -`dimensionFields` (drillable dimension name to its field), `drillRawRows` (each -row's stored grouped values, aligned to `rows`), `drillRawTotals` (the same for -`totals`) and `drillRanges` (each row's date-bucket range, `[gte, lt)`). Nothing is -removed or renamed, and no existing member changes meaning. - -`@objectstack/service-analytics` already sets them on a drillable `queryDataset` -answer. Code typed against `AnalyticsResult` can now read them without a cast, and -a parse with `AnalyticsResultResponseSchema` keeps them where it used to strip them. diff --git a/.changeset/20701-import-row-dropped-fields.md b/.changeset/20701-import-row-dropped-fields.md deleted file mode 100644 index f732053d7d8..00000000000 --- a/.changeset/20701-import-row-dropped-fields.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/spec': patch ---- - -An import row now says which of its fields the write dropped, on the dry run and on the commit. A column mapped to a `formula` field, a static `readonly` field or a runtime-owned field is legally stripped by the engine: the row still succeeds, and the create door already reported the strip as `droppedFields`. The import row answered a bare `ok` / `created` on both halves, so a file whose formula column was ignored read exactly like a file that wrote it. `runImport` now copies the engine's own per-row report onto each `ok` row as `ImportRowResult.droppedFields`: from the `validateData` verdict on the dry run, from the row's `insertManyData` outcome, and from the `createData` / `updateData` response of a single-row write. The synchronous route, the async job's results and the job's dry run all carry it; no REST change was needed. - -Clause-②: yes (widening) - -- **Verbatim, in the engine's vocabulary.** The events are the engine's `DroppedFieldsEvent`s, one per reason (`computed`, `readonly`, `readonly_when`, `primary_key`). The import reads no reason and keeps no list of non-writable types, so a reason the engine adds later reaches the row unchanged. A reader that branches on `reason` must stay exhaustive. -- **Where the key is absent although something may have been dropped.** A create batched through `createManyData` (a protocol without `insertManyData`) is reported only as a batch-level union that names no row, so those rows carry no key. And a row the import would UPDATE is previewed in `update` mode, which runs no `readonlyWhen` or primary-key strip, so its dry run can name fewer fields than its commit. The `ImportRowResultSchema.droppedFields` describe now says both. -- **Unchanged:** `ok`, `action`, the counters, the failed rows and the async job's results cap. A clean row, a failed row and a skipped row carry no `droppedFields`. - -`ImportProtocolLike.insertManyData`'s declared outcome now names the optional `droppedFields` it already answered with. diff --git a/.changeset/20701-import-row-field-and-missing-column-wording.md b/.changeset/20701-import-row-field-and-missing-column-wording.md deleted file mode 100644 index db27658fb4d..00000000000 --- a/.changeset/20701-import-row-field-and-missing-column-wording.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): an import row names the column the engine refused, and a missing database column is no longer called an unknown field (#20701) - -**`POST /api/v1/data/:object/import` — a failed row names its column.** A row the -engine refuses with `INVALID_FIELD` (a column that names no field of the object) -now carries `field` with that column, on the dry run and on the commit alike. It -used to carry only `code: 'INVALID_FIELD'`, while `POST /api/v1/data/:object` -named the field for the same key. The row reads the error's own `field` when no -field-level finding names one; a field-level finding still wins. A unique -conflict row now names its column too, when the database said which column it -was, as the `409` does. - -**A missing database column says what the database said.** When the database -reports that a table has no column for a field, the `400 INVALID_FIELD` answer -now reads "The database table of object 'X' has no column for field 'f'. If the -object declares 'f', its database schema has drifted from the metadata: run -'os migrate' to reconcile." It used to read "Unknown field 'f'", which was false -for a field the object declares: the engine refuses an undeclared key itself, -before the database is reached, and keeps its own "Unknown field" wording for -that case. `code`, `status`, `field` and `object` are unchanged. diff --git a/.changeset/20701-import-row-not-null-and-unique-door-answer.md b/.changeset/20701-import-row-not-null-and-unique-door-answer.md deleted file mode 100644 index 60496d3c307..00000000000 --- a/.changeset/20701-import-row-not-null-and-unique-door-answer.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): an import row for a NOT NULL refusal or a unique conflict answers what the create door answers (#20701) - -**`POST /api/v1/data/:object/import` and the async import job — a NOT NULL -refusal.** When the database refuses a row because a NOT NULL column has no -value (for example a field declared `storage: { notNull: true }` and not -`required`, which the engine's own check lets through), the committed row -now fails with `code: 'required'`, `field` set to the field, and the sentence -`POST /api/v1/data/:object` gives for it ("f is required"). It used to fail with -the database's own code (`SQLITE_CONSTRAINT_NOTNULL` on SQLite) and no `field`. -The create door answers `400 VALIDATION_FAILED` with a `required` finding for -the field; the row reports that finding the way it reports a `required` field -the engine refuses itself, so no database dialect's code reaches the row. - -**A unique conflict.** A committed row that repeats a unique value keeps -`code: 'UNIQUE_VIOLATION'` and its `field`, and now carries the create door's -sentence, "A record with this f already exists", in place of the engine's longer -sentence (which the create door returns as `developerMessage`). - -The async job's rows, read from `GET /api/v1/data/import/jobs/:jobId/results`, -change the same way. The row takes these answers from the same mapper as the -create door, as it already does for a missing database column. No key is added -to the row. The dry run still previews such rows as `ok`, because it checks the -metadata and does not judge `storage.notNull` or uniqueness. diff --git a/.changeset/20701-import-row-schema-drift-door-answer.md b/.changeset/20701-import-row-schema-drift-door-answer.md deleted file mode 100644 index 7bcb5ebedaf..00000000000 --- a/.changeset/20701-import-row-schema-drift-door-answer.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): an import row for a missing database column answers what the create door answers (#20701) - -**`POST /api/v1/data/:object/import` and the async import job.** When an object -declares a field whose database column is missing (the schema has drifted from -the metadata), a committed row that writes that field now fails with -`code: 'INVALID_FIELD'`, `field` set to the field, and the sentence -`POST /api/v1/data/:object` gives for the same key: "The database table of -object 'X' has no column for field 'f'. If the object declares 'f', its database -schema has drifted from the metadata: run 'os migrate' to reconcile." It used to -fail with the database's own code and text (for example `SQLITE_ERROR` and -`table X has no column named f`) and no `field`. The async job's rows, read from -`GET /api/v1/data/import/jobs/:jobId/results`, change the same way. - -The row now classifies a write error through the same mapper as the create door, -and takes that answer when it is `INVALID_FIELD`. The dry run still cannot see a missing column, -because it checks the metadata and not the table, so it previews such a row as -`ok`. diff --git a/.changeset/20722-import-time-fraction.md b/.changeset/20722-import-time-fraction.md deleted file mode 100644 index e598023ef09..00000000000 --- a/.changeset/20722-import-time-fraction.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/rest": minor ---- - -fix(rest)!: `POST /api/v1/data/:object/import` reads a `time` cell by `@objectstack/core`'s one `time` rule, the rule the write door asks, so the `10:00:00.250` that `/export` writes for a `time` with milliseconds re-imports as itself instead of failing its row as `invalid_date`, and a cell the write door refuses is refused with the write door's code, `invalid_time` (#20722) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `/import` accepts in a `time` cell. An ISO 8601 instant whose UTC year has no four-digit spelling (`"9999-12-31T23:00:00-02:00"`, which is UTC year 10000) was stored as its UTC time of day (`01:00:00`). It now fails its row with `invalid_time`, as the write door has refused the same value since #20671. A refused `time` cell's row code also moves from `invalid_date` to `invalid_time`, the code the write door gives for the same value. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). - -The import's `time` reader was a private pattern with no fractional part. A `time` stored with milliseconds (`10:00:00.250`, which the write door accepts and `/export` writes as it is stored) failed its row on re-import with `Clock: "10:00:00.250" is not a valid time`, on every backend, so an export did not re-import. The reader now asks the same rule the write door asks of a written `time`: `isUninterpretableTemporalComparand('time', …)` for the verdict and `temporalStorageForm(…, 'time')` for the stored value. A cell is admitted exactly when the write door admits the same value, and stored as the same wall clock. - -Through the route, the process in America/New_York, on memory, SQLite and PostgreSQL 16 (at `Asia/Shanghai`), before and after: - -| `time` cell | before | now | -|:--|:--|:--| -| `10:00:00.250`, `23:59:59.999` (what `/export` writes) | row failed, `invalid_date` | stored as written | -| `10:00:00.5`, `10:00:00.000` | row failed, `invalid_date` | `10:00:00.500`, `10:00:00` | -| `2026-07-15T10:00:00.250Z`, `2026-07-15 10:00:00.250` | stored `10:00:00`, the fraction dropped | `10:00:00.250`, as the write door stores it | -| `9999-12-31T23:00:00-02:00` (an instant in UTC year 10000) | stored `01:00:00` | row failed, `invalid_time`, as the write door refuses it | -| `10:00Z`, `10:00+08:00`, `07/15/2026 10:00`, `25:00` | row failed, `invalid_date` | row failed, `invalid_time` | -| `10:00`, `10:00:00`, `2026-07-15T18:00:00+08:00` | `10:00:00` | unchanged | -| `2026/7/15 9:00` (the year-first form) | `09:00:00` | unchanged | - -A zone-naive `2026-07-15 24:00` in a `time` cell, refused before, is now read as `00:00:00`: core's rule reads it so, and the write door admits it. A `date` or `datetime` cell keeps `invalid_date`, and the row's sentence is unchanged for every kind. A time of day with a `Z` or an offset stays refused: a `time` carries no zone. The year-first date-time (`2026/7/15 9:00`) is still read as its wall clock; it is the one reading the import has that the write door has not. The dry run reports the same verdicts. - -**Who is affected.** A caller of `POST /api/v1/data/:object/import`, including the dry run, whose file carries such an instant in a `time` column now gets that row refused. A client that matched the row report's `code` for a refused `time` cell now reads `invalid_time`. Rows already stored are not rewritten. - -This supersedes the note in the `@objectstack/objectql` entry for #20671 that the server import turns a `time` cell into `HH:MM:SS`: the import now keeps a non-zero fraction (`HH:MM:SS.fff`). diff --git a/.changeset/20725-subflow-guard-every-door.md b/.changeset/20725-subflow-guard-every-door.md deleted file mode 100644 index 872b634e846..00000000000 --- a/.changeset/20725-subflow-guard-every-door.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -fix(service-automation)!: a packaged flow is never armed onto a disabled packaged subflow on any door, removing a packaged subflow its packaged callers can still reach is refused, and the disable refusal reads a caller's parked runs completely (#20725) - -Clause-②: yes (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. ADR-0126 §7.3 refuses one state: a packaged flow armed while a packaged flow it calls (the `flowName` of a `subflow` or `map` node) is disabled, so that the caller fails at that node on the child's refusal. The activation switch (`POST /api/v1/automation/:name/toggle`) already refused it in both directions. It now holds on every other door too. - -**Arming declines it; registration is never refused.** Creating, republishing, upgrading or hot-reloading a flow, a cold boot, and a trigger registering at `kernel:ready` all arm through one gate. That gate now leaves a packaged flow **unarmed** while a packaged subflow it calls is disabled, by the activation ledger or by its definition's `status` (`obsolete` / `invalid`). The flow still registers, so a boot or an upgrade never fails on an installation's choice: - -- `GET /api/v1/automation/_status` reports it `enabled: true, bound: false`, with a `reason` naming each disabled subflow and the step that re-arms it. The `kernel:bootstrapped` binding audit prints the same reason. -- The engine logs one warning naming each subflow and its remedy (enable it, or publish it with status `active`). -- It is armed the moment its subflow is enabled, through the switch or by republishing the subflow `active`. A flow held back by two subflows is armed when both are on. -- An armed caller is unarmed when its subflow is republished `obsolete` or `invalid`, or when the activation ledger read at boot switches that subflow off. -- In a cycle of flows switched off in the activation ledger, enabling the first one is still accepted, but it stays unarmed until the flow it calls is enabled; then both are armed. -- A flow the customer authored, or a subflow the customer authored, is not judged. - -**Removing a packaged subflow is refused while a packaged caller can still reach it.** `AutomationEngine.unregisterFlow`, and so `DELETE /api/v1/automation/:name`, now refuses with `DELETE_RESTRICTED` / `409` and `subflowCallers`, the same refusal the switch gives on disable. Nothing is removed. The message names each caller and the steps: - -- **An enabled caller** guards: disable it first. -- **A switched-off caller** guards while the subflow itself is still enabled. The removal door cannot read whether that caller still holds a parked run, so it names the switch instead: switch the subflow off (that refusal names each parked run to cancel), then remove it. -- Once the subflow is switched off and every caller is switched off, the removal completes. - -A flow an artifact reload no longer ships (a package upgrade or uninstall, a Studio package publish, a dev reload) is removed through the new `AutomationEngine.withdrawFlow`, which does not take this refusal: the package decided the removal, and the next cold boot would not register the flow either. - -**The disable refusal reads a caller's parked runs completely.** Disabling a packaged subflow under a switched-off caller that holds a parked run was decided from the durable store's deployment-wide list of paused runs, which reads at most 1000 rows. A caller whose run lay beyond them read as holding none, and the disable was accepted. The refusal now asks the store for the named callers' runs, all of them: `SuspendedRunStore` gains an optional `listByFlow(flowNames)` (complete by contract, or an error), and `ObjectStoreSuspendedRunStore.listByFlow` reads the `(flow_name, status)` index page by page to its end. A store without it is read through `list()`. The deployment-wide listing (`listSuspendedRunsDurable`) is unchanged. diff --git a/.changeset/20726-clone-notice-status-switch.md b/.changeset/20726-clone-notice-status-switch.md deleted file mode 100644 index 90e1284cd02..00000000000 --- a/.changeset/20726-clone-notice-status-switch.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): the clone door's notice names the clone's own off-switch, its status (#20726) - -`POST /api/v1/automation/:name/clone` answers a `notice` saying the clone is armed. It told the admin to switch the clone off through `POST /api/v1/automation/NAME/toggle`. A clone carries no package envelope, so it is a flow authored in the deployment, and that switch refuses it: the switch turns packaged flows on and off only. The notice now names the clone's own switch: send its complete definition with `status: 'obsolete'` to `PUT /api/v1/automation/NAME`. It also says that the toggle switches packaged flows only and refuses the clone, whatever flow the clone was copied from. The response shape is unchanged; only the notice text moves. diff --git a/.changeset/20726-toggle-door-docs-client.md b/.changeset/20726-toggle-door-docs-client.md deleted file mode 100644 index da949d8df0e..00000000000 --- a/.changeset/20726-toggle-door-docs-client.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/client': patch ---- - -docs(client): `automation.toggle` says it switches packaged flows only, and names a customer flow's switch (#20726) - -`client.automation.toggle` had no docblock of its own: its one line had drifted above an unrelated member. It now says that it switches packaged flows only, and that a flow authored in the deployment is refused with 409 `RESOURCE_CONFLICT`. Such a flow's switch is its `status`, sent with the complete definition through `automation.update`. This is prose only: the method's signature and behaviour are unchanged. diff --git a/.changeset/20726-toggle-door-docs-spec.md b/.changeset/20726-toggle-door-docs-spec.md deleted file mode 100644 index bb9db6effc9..00000000000 --- a/.changeset/20726-toggle-door-docs-spec.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the Automation API docblock says the toggle door switches packaged flows only, and names a customer flow's switch (#20726) - -The module docblock of `api/automation-api.zod.ts` listed `POST /api/v1/automation/:name/toggle` as "Enable/disable flow". That file ships as source, and its docblock is also the source of the Automation API reference page. The line now reads "Enable/disable a packaged flow". A new paragraph says what a flow authored in the deployment uses instead: its `status`, published with the complete definition through `PUT /api/v1/automation/:name`. The toggle door refuses such a flow with 409 `RESOURCE_CONFLICT`. The `IAutomationService.toggleFlow` docblock, which read "Enable or disable a flow", says the same. This is prose only: no schema, type or export changes. diff --git a/.changeset/20726-toggle-door-packaged-only.md b/.changeset/20726-toggle-door-packaged-only.md deleted file mode 100644 index b62bdc63f06..00000000000 --- a/.changeset/20726-toggle-door-packaged-only.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -fix(service-automation)!: the toggle door refuses a flow no package ships, naming its status switch (#20726) - -Clause-②: no (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. `toggleFlow(name, enabled)` on the automation service, and so `POST /api/v1/automation/:name/toggle` and `client.automation.toggle`, now switches packaged flows only: a flow a code package ships. - -**What was wrong.** The switch records an installation's choice in the packaged-metadata activation ledger (`sys_metadata_activation`, ADR-0126 §7.2), whose rows name the package that ships the flow. For a flow authored in the deployment it wrote a row anyway: - -- A flow with no package id, such as one created through `POST /api/v1/automation` or the clone door, was refused with 400 `VALIDATION_FAILED` "Package is required", naming a field the caller never sent. -- A flow carrying the runtime-row package sentinel or an app package id was accepted, and a ledger row was written for it. That gave it a second off-switch beside its own `status`. -- With no ledger attached, the flip was accepted in process only. - -**What changed.** A flow without package provenance is now refused with `RESOURCE_CONFLICT` / `409`, in both directions and with or without a ledger. The refusal comes before anything is written or changed. The message says the switch turns packaged flows on and off. It names the flow's own switch: its definition's `status`, published with the complete definition through `PUT /api/v1/automation/:name`. `obsolete` switches it off and `active` arms it. The switch never rewrites a definition itself. Packaged flows toggle exactly as before. - -**Migration.** To switch a customer-authored flow off, stop sending `POST /api/v1/automation/NAME/toggle` with `{"enabled": false}`. Instead, send `PUT /api/v1/automation/NAME` with the flow's complete definition and `status: 'obsolete'`, and `status: 'active'` to arm it again. In the SDK, `client.automation.toggle(name, false)` becomes `client.automation.update(name, { ...definition, status: 'obsolete' })`. - -**A customer flow that a ledger row already holds off.** If this switch turned a customer flow off before this release, its ledger row still holds the flow off after the upgrade, and no `status` clears that row. The refusal says so and names the step that completes: clone the flow under a new name through `POST /api/v1/automation/NAME/clone`, which arms the copy, then remove the old one. diff --git a/.changeset/20730-meta-packaged-base-dashboard-view.md b/.changeset/20730-meta-packaged-base-dashboard-view.md deleted file mode 100644 index d3d3735ae28..00000000000 --- a/.changeset/20730-meta-packaged-base-dashboard-view.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): an org's published edit to a packaged dashboard or view is what the `/meta` item and list reads serve, in every locale, instead of the packaged translation of the string it replaced - -An organization may edit a packaged dashboard or view in place and publish the edit. The metadata protocol's reads returned the edit, and `?layers=true` reported it as effective, but `GET /api/v1/meta/dashboard/:name`, `GET /api/v1/meta/dashboard`, `GET /api/v1/meta/view/:name` and `GET /api/v1/meta/view` served the bundle's translation of the string the package shipped. For example, a widget retitled `Total Users (edited)` on the platform's `system_overview` dashboard was served as `Total Users` to an `en` reader and as `用户总数` to a `zh-CN` reader. - -The translators in `@objectstack/spec/system` already let an edited string win over the bundle when they are handed the item as the package shipped it, and the metadata protocol already answers that item (`getPackagedDashboardBase`, `getPackagedViewBase`). The `/meta` reads handed it over for objects only. They now hand it over for dashboards and views too. The change is in the translation step that both `/meta` transports share, the REST server's routes and the runtime's HTTP dispatcher. A view is looked up by its full `.` name. - -What a reader sees now: - -- An edited string is served as written, in every locale. -- A widget or view the org left alone is still translated. -- Resetting the overlay brings back the shipped string and its translation. -- A dashboard or view with no org edit is served exactly as before. - -This fixes what the metadata reads serve, not yet what the console draws. The console built from objectui `db11afd4967c`, this repository's pin when the change was made, looks a dashboard's widget titles and a view's label up in the bundle again in the browser, so it still draws the packaged translation over an edit the server now serves: measured, the `system_overview` board shows `Total Users` / `用户总数` and a `zh-CN` view tab shows `进行中`. - -Nothing to migrate: no key, export or route changed. diff --git a/.changeset/20731-metadata-protocol-packaged-view-base.md b/.changeset/20731-metadata-protocol-packaged-view-base.md deleted file mode 100644 index d6bbc0faa31..00000000000 --- a/.changeset/20731-metadata-protocol-packaged-view-base.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol): `getPackagedViewBase(name)` answers the view a code package ships, before any overlay (#20731) - -`ObjectStackProtocolImplementation` gains `getPackagedViewBase(name)`, the view twin of `getPackagedDashboardBase`. It returns the packaged (code-layer) declaration of a view, which is the `packagedBase` that `translateView` compares a served view against, so that a tenant's published overlay is not overwritten by the packaged translation catalog. - -`name` is the view's registry identity, the qualified `.` that each view of a `defineView` container is registered under and that the overlay row and both reads carry. The bare view key that the catalog uses under its object is not an identity (another object may ship a view with the same key), and it answers `undefined`. - -It reads the artifact registry's code-package entry only, through the same lookup as `getPackagedDashboardBase`. An overlay that was hydrated under the plain registry key can therefore never be returned as the base it is compared against. It returns `undefined` for a view that no code package ships, for an unknown or empty name, and for a registry that cannot answer. A caller treats `undefined` as "no base known", and the catalog applies as before. - -The method is additive. No existing read changes answer, and `getPackagedDashboardBase` answers exactly as before. Nothing is removed or renamed. diff --git a/.changeset/20731-spec-view-catalog-override.md b/.changeset/20731-spec-view-catalog-override.md deleted file mode 100644 index d86b1bc35d4..00000000000 --- a/.changeset/20731-spec-view-catalog-override.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -fix(spec): `translateView` lets an explicit override beat the packaged catalog when it is handed the packaged view (#20731) - -`translateView` (`@objectstack/spec/system`) now follows the rule ADR-0029 D9.2a records for objects, and that `translateDashboard` already follows: an explicit override beats a packaged default. When the caller supplies `packagedBase` (the view as its code package ships it, before any tenant overlay), a catalog string replaces a served string only while that string still equals its packaged counterpart. It is the same comparison, not a second one. - -The comparison is made per string: the view `label` and `description`, each bulk-action def's `label`, `confirmText` and `confirmLabel`, and each of its params' `label`, `help` and `placeholder`. A def or param is matched by the `name` the bundle addresses it by, and one that the packaged view does not carry counts as authored, so its strings keep their values. - -Why: an org overlay on a packaged view (ADR-0126 Regime O) changed the label of the showcase's `showcase_task.in_progress` and published. The metadata protocol's item and list reads served the edit, but a `zh-CN` reader was served `进行中`, the catalog's translation of the label the package shipped. The catalog (`objects.._views.`) translated the packaged view and was applied over the tenant's edit. - -What changes for a caller: - -- `translateView`'s third parameter is typed `TranslateDocumentOptions` (was `ResolveOptions`). That type is `ResolveOptions` plus the optional `packagedBase`, and `translateMetadataDocument` already passed it through. Every existing call compiles unchanged. -- Without `packagedBase` (`undefined` or `null`), the output is byte-identical to before: the catalog applies. No serving layer in this release passes a view base yet, so no served view changes answer with this package alone. -- An edit back to exactly the shipped string is a no-op: the catalog still translates it. A label written as an inline locale map is not an override either; only a string is compared. - -Nothing is removed or renamed, and there is nothing to migrate. diff --git a/.changeset/20740-time-value-zone-less.md b/.changeset/20740-time-value-zone-less.md deleted file mode 100644 index 41868de0ec5..00000000000 --- a/.changeset/20740-time-value-zone-less.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -fix(spec)!: a `time` value carries no zone — `ClockTimeValueSchema` refuses a `Z` or a UTC offset, so a `time` field default, an action param default or a submitted `time` action param with one is refused when it is authored or submitted, not on every insert that falls back to it - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows the `time` stored form (`valueSchemaFor({ type: 'time' })`) to the zone-less wall clock `HH:MM[:SS[.fraction]]` that the record validator already enforces on write (ADR-0053 D-C1). It ships as `minor` under the launch-window convention for accept-set narrowings; the breaking-ness is carried by this banner and the ADR-0087 disposition above. - -Refused now, where they parsed before: - -- `FieldSchema`: a `time` field's literal `defaultValue` with a zone (`'10:00Z'`, `'10:00+08:00'`). Before, it parsed and each insert that fell back to it was refused `400 VALIDATION_FAILED` / `invalid_time` on a field the caller never sent. -- `ActionParamSchema`: a `time` param's literal `defaultValue` with a zone. -- `validateActionParams` (the action dispatcher, ADR-0104 D2): a submitted `time` param value with a zone, now `invalid_shape`. - -## FROM → TO - -| you wrote | write instead | -| --- | --- | -| `defaultValue: '10:00Z'` or `'10:00+00:00'` | `defaultValue: '10:00'` | -| `defaultValue: '10:00+08:00'` | the wall clock you meant, `'10:00'` or `'02:00'`, or a `datetime` field for an instant | - -**The one-line fix:** drop the `Z` or offset from every `time` value, or use a `datetime` field. - -**Stored metadata.** The D2 conversion `time-default-utc-suffix-dropped` (retired from the load path) drops a `Z` or a zero offset from a stored `time` default on a field or on an action param typed `time`, so such a row loads canonical. It leaves a non-zero offset as stored and reports it as a TODO naming the field or param, which `os migrate meta --stored` lists; the row keeps loading, fails the schema wherever it is parsed, and needs the rewrite by hand. The D3 entry `time-default-zone-refused` carries that judgement. - -**Unchanged:** a zone-less wall clock, the `NOW()` token and expression defaults on a `time` field, and every `date` and `datetime` value. The repo census found no shipped `time` default with a zone. diff --git a/.changeset/20745-nested-object-door.md b/.changeset/20745-nested-object-door.md deleted file mode 100644 index 07880d67aef..00000000000 --- a/.changeset/20745-nested-object-door.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: a plain object with no `$` operator beneath a relation field, a structured-JSON field or an undeclared `id` column is refused with `INVALID_FILTER` / 400 at `where`, a per-aggregation `filter` and `having`, on every driver - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a filter may put beneath a relation or JSON-valued field. A plain object with no `$`-operator key — `{ "owner": { "region": "NA" } }` beneath a `lookup`, `{ "meta": { "a": 1 } }` beneath a `json` field, `{}` included — is refused by the engine before any driver is asked. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What an author sees now.** `400 INVALID_FILTER`, naming the field, its declared type, the object's keys (never its values), the position (`where.owner`, `aggregations[1].filter.owner`, `having.owner`) and the route that works, inside the first 500 characters the REST door keeps: - -- **A relation field** — `lookup`, `master_detail`, `user`, `tree`, single or multiple (the nested-relation form, a condition on the related record's own fields). No data-path driver follows a relation: the field stores the related record's id. Filter the related object first, then match the field against the ids it returns — `{ "owner": { "$in": ["ID", "..."] } }` on a single-valued field, `{ "owners": { "$contains": "ID" } }` per id on a multi-valued one (an `$or` of those for several ids; the SQL driver refuses `$in` on a multi-valued column). A dotted path (`"owner.region"`) is no route: it was already refused with `INVALID_FIELD` on every driver. -- **A structured-JSON field** — `json`, `composite`, `repeater`, `record`, `location`, `address`, `vector` (a whole-value match). The drivers share no meaning for it: the in-memory driver compared documents, the SQL driver refused the bind. Test the whole value's presence with `{ "meta": { "$null": false } }`, or store the part you filter on in a field of its own and filter that field. `$contains` is no route: it was already refused over a JSON value on every driver. -- **`id`, `created_at` or `updated_at` absent from the declared field map** — the platform provisions these columns, so they are judged by the type they store (text, datetime), in the scalar-field words: compare with a value or an operator. - -No mechanical rewrite exists, because which related records or which part of the JSON value the caller meant is not in the object; the fix is by hand, as above. - -Measured through `POST /api/v1/data/:object/query`, three rows (owner `u1`, region NA, on `d1` and `d3`): - -| position | filter | before: memory · SQLite · PostgreSQL 16 | now, on all three | -|:--|:--|:--|:--| -| `where` | `{ owner: { region: "NA" } }` on a `lookup`, and its `master_detail`, multiple-lookup, `user` and `tree` twins | no records (`d1`, `d3` were meant) · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400, the engine's words | -| `where` | `{ meta: { a: 1 } }` on a `json` field, and its `address` and `composite` twins | the deep-equal records · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400 | -| `where` | `{ id: { a: 1 } }` | no records · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400 | -| per-aggregation `filter` | `{ owner: { region: "NA" } }` | count 0 on all three | `INVALID_FILTER` / 400 | -| per-aggregation `filter` | `{ meta: { a: 1 } }` | count 1 on all three (the engine's own deep equality) | `INVALID_FILTER` / 400, one answer per filter at every position | -| `having` | `{ owner: { region: "NA" } }` over a lookup groupBy | no group on all three | `INVALID_FILTER` / 400 | -| `where` | route `{ owner: { $in: ["u1"] } }`; `{ owners: { $contains: "u1" } }` | `d1`, `d3` on all three | unchanged | - -**Who is affected.** A caller that sends the nested-relation form or a JSON object comparand to the in-memory driver — a test suite, a local or embedded deployment on `InMemoryDriver`, a flow or hook calling the engine in-process — and read the empty (or deep-equal) answer as a real one; and a per-aggregation `filter` that matched a JSON value by deep equality. On `SqlDriver` the `where` forms were already a 400, now in the engine's words. - -**Supersedes** the "Unchanged" paragraph of the scalar-field refusal entry (`20546-no-operator-object-on-scalar`) for relation and structured-JSON fields: they are judged now, in words of their own. File and media fields (a legacy stored value is an inline object), `formula` (refused one door earlier, `INVALID_FIELD`), any other undeclared key, a `{ $field }` reference and every operator bag are still not judged by this refusal. diff --git a/.changeset/20745-nested-relation-prose.md b/.changeset/20745-nested-relation-prose.md deleted file mode 100644 index 4f7cfbc4a81..00000000000 --- a/.changeset/20745-nested-relation-prose.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -docs(spec): the `FilterCondition` docblock says the query engine refuses the nested-relation form - -`FilterCondition`'s form 4, `{ relation: { field: value } }`, stays in the type and the schema (nothing is narrowed: `FilterConditionSchema` parses it as before), and its docblock now states what the engine answers: `INVALID_FILTER` / 400 on every driver, because no data-path driver follows a relation into the related object. It names the route that works — filter the related object first, then match the relation field against the ids it returns (`$in`, or `$contains` per id on a multi-valued relation). The `QueryFilter` example no longer teaches the form, and the `Filter` nested arm's comment points at the refusal. diff --git a/.changeset/20751-services-strings-state-the-decision.md b/.changeset/20751-services-strings-state-the-decision.md deleted file mode 100644 index cdda60c365e..00000000000 --- a/.changeset/20751-services-strings-state-the-decision.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/plugin-webhooks': patch -'@objectstack/service-messaging': patch ---- - -Auth, webhook and outbound-delivery refusals, warnings and field help no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings these three packages show to operators, administrators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `@objectstack/plugin-auth`: an unrecognised audience posture is refused because it must not fall through to a more permissive posture than the one intended; the `ObjectQL` adapter's case-insensitive warning says the `$ieq` operator is deliberately deferred until there is demonstrated pull for it; the `internal`-column refusal says the column is withheld from every ordinary read and recovered only through the engine's accessor; the 2FA re-enrollment errors say a re-enrolled TOTP secret may be live at sign-in without having been confirmed; the walled-owner boot warning says a declared owner is stamped verified only when an operator-provisioned path creates the account; the OTP send-budget lines name the budget without a number. -- `@objectstack/plugin-webhooks`: the parked-event record says the event is recorded rather than delivered unsigned (or without its authored headers) and rather than discarded without a trace; the redeliver refusals say a delivery that cannot be signed is refused rather than sent unsigned; the zero-trigger warning says the `api` trigger was removed because nothing could fire it; the seed and legacy-migration warnings say a credential is never stored in cleartext instead and that a failed migration leaves it cleartext in `definition_json`. -- `@objectstack/service-messaging`: the `sys_http_delivery` field help for `attempts` (in every shipped locale) says a parked row is not redeliverable because it carries no signature; the `headers_json` and `error` help and the outbox refusals drop their citations; the notification `ack()` refusal says cancelling a pending row is not part of the outbox contract until a live consumer needs it. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20752-cli-strings-state-the-decision.md b/.changeset/20752-cli-strings-state-the-decision.md deleted file mode 100644 index 06ce75e88a8..00000000000 --- a/.changeset/20752-cli-strings-state-the-decision.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/types': patch ---- - -CLI help, warnings and refusals no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Several lines the CLI prints sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `os build` / `os validate`: the provider preflight step now reads "Checking that every required capability has a provider installable in this edition...", and the undeclared-key header reads "Undeclared authoring keys (N) — dropped at load; reported here, never refused". -- `os doctor`: the retired `referenceFilters` row now says the key was removed from FieldSchema as a key no runtime read; the `NODE_ENV` and config-load rows drop their citations. -- `os serve`: the no-auth refusal says anonymous data access is always denied with no setting that turns that off; the organizations remedies drop their citations. -- `os meta resync`, `os db clean` and the `os migrate duplicates` / `multi-value-columns` / `recorded-by` / `summary-nulls` descriptions, the `os dev --restart` flag help, the `os storage orphans` closing line and the storage-driver refusals each say what was decided instead of citing it. -- `os serve`'s unknown-hostname 404 page spells its three short grey colours in six hex digits; they render the same. -- `@objectstack/types`: the host importer's undeclared-package message says the fallback resolves from the caller once `fallbackImport` is passed, and drops the citation beside "Being merely REACHABLE is not enough". - -Text only: no exit code, error code, flag, field or control flow moves. A script that matches the old CLI text (for example the "Checking capability providers" step line) needs the new spelling. diff --git a/.changeset/20752-rest-strings-state-the-decision.md b/.changeset/20752-rest-strings-state-the-decision.md deleted file mode 100644 index 56ccced32f6..00000000000 --- a/.changeset/20752-rest-strings-state-the-decision.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -REST refusals, warnings and the served OpenAPI text no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -A few strings `@objectstack/rest` sends to callers and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `POST /data/:object/import` with a named mapping that declares a `javascript` transform: the `UNSUPPORTED_TRANSFORM` message now says the import path does not execute it (there is no server-side sandbox), so the import is refused rather than run with that transform skipped. -- `GET /openapi.json`: the built-in section's response description says the section is built from the routes this server actually mounts and that payload schemas are deliberately not invented; the request-body description says the document leaves the route-specific shape undescribed rather than invent one. -- The boot warning for a config that still sets the retired `api.requireAuth` drops its citation; it already says the key was removed and anonymous access to object data is always denied. -- `/discovery`'s `capabilities.transactionalBatch.description` cites ADR-0034 alone. - -Text only: no status, error code, field, route or control flow moves. A client that matches the old message text (for example the tracker-number suffix the `UNSUPPORTED_TRANSFORM` message used to end with) needs the new spelling. diff --git a/.changeset/20752-runtime-strings-state-the-decision.md b/.changeset/20752-runtime-strings-state-the-decision.md deleted file mode 100644 index 58aa0ab84d8..00000000000 --- a/.changeset/20752-runtime-strings-state-the-decision.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -Runtime refusals, boot errors and warnings no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Strings `@objectstack/runtime` shows to callers, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- The enablement refusal (`POST /actions/_activation/:object/:action`) adds that the switch is not scoped to the caller's organization, which is why `manage_metadata` gates it. -- The doubled post-success navigation warning (`[action-contract]`) says the contract refuses a pair of destinations rather than ranking them, and that "declared `onSuccess` wins" is the console renderer's interim precedence, not a contract. -- The legacy database notice says `dev`, `start` and `migrate` now share one default database file. -- The `BodyRunner` warning for a `log` capability with no logger says the capability writes only to the factory's logger, never to `console`. -- The seed tenancy handoff warning says what a failure leaves behind: seed and API writes on separate autonumber counters until the next boot's migration repairs it. -- The auth forwarder's sanitised-500 log line says the client's message was withheld unconditionally and that this line is where the original error is read. -- The `StandaloneStack` guard for a driver kind with no dispatch arm says falling through to SQLite would hand the caller an engine they never selected. -- The `StandaloneStack` refusals for an unsupported or URL-less database driver, the declarative-endpoint hints, the `cacheTtlSeconds` warning and the two other `BodyRunner` warnings drop their citations; each already said what it refuses and why. - -Text only: no status, error code, field, route, export or control flow moves. A log filter or test that matched the old text (for example a `See #NNNN` suffix) needs the new spelling. diff --git a/.changeset/20752-verify-plugin-strings-state-the-decision.md b/.changeset/20752-verify-plugin-strings-state-the-decision.md deleted file mode 100644 index 19ec3b6a8d6..00000000000 --- a/.changeset/20752-verify-plugin-strings-state-the-decision.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/verify': patch -'@objectstack/plugin-dev': patch -'@objectstack/plugin-hono-server': patch ---- - -The `verify --rls` report and messages, the dev plugin's tenancy and no-auth messages and the Hono server's no-API warning no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Strings these three packages show to operators, and print in verification reports, sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `@objectstack/verify`, the `objectstack verify --rls` report: the header reads `=== objectstack verify (RLS / cross-owner by-id-write invariant) — ===`, and the position-persona line reads `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed`. -- `@objectstack/verify`, an `rls-hole` verdict's detail: it says the by-id write bypassed RLS, and that a caller that cannot read a record must not be able to write it. -- `@objectstack/verify`, the refusal when the RLS probe persona cannot be provisioned (no ObjectQL engine): it says a by-id write that bypasses RLS is what becomes unreachable. The matching position-persona refusal drops its citation. -- `@objectstack/verify`, the records the probe writes: the probe permission set's row-level-security policy description, the probe `sys_permission_set` row's description and the position persona's `sys_user_position` reason drop their citations. Each already said what it is for. -- `@objectstack/verify`, the `bootStack` refusal for `multiTenant: true` when the app does not declare `@objectstack/organizations`: the citation beside "a package merely reachable through NODE_PATH or a hoisted workspace store is not accepted" goes. -- `@objectstack/plugin-dev`, the `REST API NOT enabled` warning for a stack that mounts no auth: it says anonymous access to object data is always denied, with no setting that turns that off. -- `@objectstack/plugin-dev`, the two refusals for an `OrganizationsPlugin` that refused to be constructed or failed to initialize: they drop their citations. Each already says `OS_ALLOW_DEGRADED_TENANCY` covers only an absent multi-org runtime, not a present one that declined. -- `@objectstack/plugin-hono-server`, the boot warning for a server with no data or discovery API mounted: it drops its citation. It already says the plugin is a transport adapter that serves neither. - -Text only: no status, error code, exit code, route, field, export, verdict or count moves. A log filter or script that matched the old text (for example the report header's `RLS / #NNNN` spelling) needs the new spelling. diff --git a/.changeset/20758-page-header-breadcrumb-retired.md b/.changeset/20758-page-header-breadcrumb-retired.md deleted file mode 100644 index 472260f57e7..00000000000 --- a/.changeset/20758-page-header-breadcrumb-retired.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: retire a page header's `breadcrumb` switch — no renderer ever drew a trail for it (#20758) - -**BREAKING** — `breadcrumb` on a `page:header` component (`PageHeaderProps`) is retired, with its `true` default: no renderer ever drew a trail for it. objectui drew an empty slot that nothing filled, and the console draws the navigation trail once, in the app shell's header. Delete the key, whether it was `true` or `false`. The shell's trail is unchanged. - -Clause-②: no (narrowing) - -Measured before removal: objectui's `PageHeaderRenderer` reads the key only to draw an empty `div[data-page-breadcrumb-slot]`, and nothing fills it. The one producer is objectui's Studio page-block inspector ("Show breadcrumb"), so stored pages may carry either value. The one in-repo author found was the published `objectstack-ui` skill's record-page example. No example app authors it. The `nav:breadcrumb` component type is not part of this retirement: the Studio page palette still offers it. - -## FROM → TO - -| you wrote (17.5 and earlier) | write instead | -| --- | --- | -| `{ type: 'page:header', properties: { title, breadcrumb: true } }` | `{ type: 'page:header', properties: { title } }` | -| `{ type: 'page:header', properties: { title, breadcrumb: false } }` | `{ type: 'page:header', properties: { title } }` | - -**The one-line fix:** delete `breadcrumb` from every `page:header`'s `properties`. - -**What an author who still writes it sees.** A page is never refused for it. A page component's `properties` is an open bag, so `definePage()`, `defineStack({ pages })` and the page write door accept the page as before. `os validate` / `os build` / `os lint` report the key as a warning at `properties.breadcrumb`, with the prescription: - -> `page:header` property `breadcrumb` was removed in @objectstack/spec 17 (ADR-0087 D2) — no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, and the navigation trail is drawn once, by the app shell's header. Delete the key, whether it was `true` or `false`; the shell's trail is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. - -A typed `PageHeaderProps` input fails `tsc` at the key. - -## The retirement kit - -- **A `retiredKey()` tombstone** on `PageHeaderProps`, a `strictObject`, beside the `icon` that row lost at 17. `RETIRED_KEYS_BY_MAJOR[18]`: `ui/PageHeaderProps:breadcrumb`. No retired-default residue stage is owed: the `true` default was never written into a built artifact, because a page parses its component `properties` as an open bag and only the advisory props lint reads this row. -- **The D2 conversion `page-header-breadcrumb-removed`** (protocol 18, retired from the load path) deletes the key from every `page:header`, `true` and `false` alike, with one notice per header. It reaches headers in regions, nested in a container's `children`, and in a slotted page's named slots. A stored `page` row or a built artifact that carries the key loads through the rehydration seams, which replay it. -- **The D3 entry `page-header-breadcrumb-retired`**: a header that said `false` reads as absent after the strip, so it shows the empty slot's spacing again until the renderer stops drawing the slot. -- **No deprecation window**, per the project's startup-stage posture. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. - - diff --git a/.changeset/20760-bucket-key-four-digit-year.md b/.changeset/20760-bucket-key-four-digit-year.md deleted file mode 100644 index 2328550179b..00000000000 --- a/.changeset/20760-bucket-key-four-digit-year.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/service-analytics': patch ---- - -A date-bucket key spells its year with four digits at every granularity, as the SQL drivers' bucket expressions do, so the in-memory and pushed-down paths key a day in 0001..0999 alike and a drill-down from such a key finds its range. - -A `date` value names a year from 0001 to 9999, so these keys are reachable through a `date` field and through a stored `datetime` row. For 0050-06-15, `strftime('%Y-%m')` on SQLite and `to_char(…, 'YYYY-MM')` on PostgreSQL answer `0050-06`, while `bucketDateKey` answered `50-06`: the same `groupBy` keyed the same rows differently depending on which path ran it. - -- **`@objectstack/core` `bucketDateKey`** pads the year to four digits: `0050`, `0050-Q2`, `0050-06`, `0050-06-15`, and the ISO week key `0050-W24` (early January 0050 is `0049-W52`, its ISO week-year). The engine's in-memory `groupBy` and the memory cube face delegate to it, so both now answer the drivers' key. A year from 1000 to 9999 is spelled as before. -- **`@objectstack/core` `bucketKeyToCalendarRange`** reads exactly what `bucketDateKey` writes. Its week arm checked a key against the unpadded label, so a padded key such as `0050-W01` answered `null`; it now answers `{ start: '0050-01-03', end: '0050-01-10' }`. An unpadded key (`50-06`, `49-W52`) is not a bucket key and still answers `null`. -- **`@objectstack/service-analytics`** mints the `compareTo` alignment key through `bucketDateKey` instead of spelling it locally, so a comparison row in 0001..0999 merges onto its bucket (`0050-06`) instead of being appended under `50-06`. diff --git a/.changeset/20761-flow-provenance-server-held.md b/.changeset/20761-flow-provenance-server-held.md deleted file mode 100644 index 2516292e272..00000000000 --- a/.changeset/20761-flow-provenance-server-held.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/service-automation': minor -'@objectstack/runtime': patch ---- - -fix(automation): which flows are packaged is the package loader's fact, never the flow definition's own, and every flow written through an authoring door is authored in the deployment (#20761) - -Clause-②: yes (widening) - -A flow counts as packaged only when a managed package's loader registered it (ADR-0126 §2, ADR-0131 D6). Before this change, a flow definition written through an authoring door could carry a code package's provenance, and the automation engine then treated that flow as the package's. - -- **The automation engine reads the loader's set.** The ADR-0126 §7.3 subflow guards, the arming gate, the activation switch and the package an activation row names now come from the packages the loader registered. The provenance a flow definition carries is kept for display only. `AutomationEngine` gains `setPackagedFlowSource(reader)` and `packagedFlowOwner(name)`, and the package exports the `PackagedFlowSource` type. `AutomationServicePlugin` attaches the reader for you: it asks the metadata protocol when the engine needs the answer. An engine with no reader attached treats no flow as packaged. -- **One authoring rule for flows.** `ObjectStackProtocolImplementation` gains two methods. `packagedArtifactOwner({ type, name })` names the package whose loader registered an item. `tenantAuthoredWriteRefusal({ type, name, item, packageId? })` is the rule every flow write door asks: the automation create, update and clone doors, and the metadata door's flow write. - - A write to a name a package ships is refused as a locked base. The answer is `packagedBaseRefusal`'s own (`403 NOT_OVERRIDABLE`), so sending a shipped flow's definition back is refused. - - A definition that claims a code package's provenance for a name no package ships is refused with `422 INVALID_METADATA`, and nothing is written. Before, the automation doors kept the claim and the metadata door removed it without saying so. - - A customer flow's definition sent back as it was read is accepted as before. That includes a stored flow bound to one of your own packages, whose read carries that binding. - - `packagedBaseRefusal` also takes an optional `packageId`, the base a save names. -- **The metadata door's other types are unchanged.** Only flows are judged by this rule. Migrating stored rows and duplicating a package are not affected either. -- **A clone is saved.** `POST /automation/:name/clone` now writes its copy as a stored flow of the deployment, through the metadata protocol's save, with no package provenance. The copy reads back on the metadata door and is still there after a restart. Before, it lived only in the running engine and was gone after a restart. If the save fails, the clone is withdrawn and the failure is returned. - -**If a write of yours is now refused with `422 INVALID_METADATA`:** remove the package provenance from the flow definition and send it again. To customize a packaged flow, clone it under a new name. diff --git a/.changeset/20768-first-boot-migration-gate-read.md b/.changeset/20768-first-boot-migration-gate-read.md deleted file mode 100644 index e8a3888cbb8..00000000000 --- a/.changeset/20768-first-boot-migration-gate-read.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -fix(driver-sql): the first boot of a new database no longer prints a `DATABASE_ERROR` for `sys_migration` (#20768) - -On the first boot of a new database, the SQL driver printed this line once, on its warn channel (stderr by default): - -```text -[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... no such table: sys_migration -``` - -Nothing was wrong. At the start of its first schema sync, before it creates any table, the driver asks whether this deployment's file columns have moved (the ADR-0104 media-arm resolver). The resolver the engine supplies answers by reading `sys_migration`. On a new database that table does not exist yet, so the read is refused and the answer is "not moved", which is correct for an empty store. - -The driver now asks that question inside an async scope. Inside it, a read refused because its own target table does not exist goes to the logger's `debug` channel instead of `warn`. The default logger has no `debug`, so the line is not printed. The logger shape gains an optional `debug`. The refusal is still thrown to the resolver, and the resolver's answer is the same as before. - -What still warns: - -- every other refusal inside that scope, such as a malformed statement on a table that exists, or a missing table named by another relation (a view over a dropped table); -- a missing table read anywhere else, as before. - -The missing-table check is the shared `isMissingTableError` from `@objectstack/types`, which `@objectstack/metadata/errors` re-exports. There is nothing to migrate. diff --git a/.changeset/20771-wall-clock-one-rule.md b/.changeset/20771-wall-clock-one-rule.md deleted file mode 100644 index c290b0538b7..00000000000 --- a/.changeset/20771-wall-clock-one-rule.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/core': patch ---- - -`isUninterpretableTemporalComparand` reads a bare wall clock on a `time` column by the spec's `ClockTimeValueSchema` (`@objectstack/spec/data`), not by a private copy of it (#20771) - -Clause-②: no - -The wall-clock half of core's `time` rule (`HH:MM[:SS[.fraction]]`, hours 00 to 23, minutes and seconds 00 to 59, no time zone) was spelled twice: once as the spec's `ClockTimeValueSchema`, the stored form of a `time` value, and once as a private regex in `@objectstack/core`. The two admitted the same strings, but nothing tied them together, so an edit to either one changed one side only. Core now asks the spec schema. Every caller of `isUninterpretableTemporalComparand('time', …)` therefore answers from the rule the spec's `time` default gate uses: the engine's temporal-comparand door, the analytics comparand check, the record validator's `time` arm and the import's `time` coercion. - -Unchanged: - -- Every string gets the verdict it got before. Measured over 8,655,360 generated strings: 8,640 read by both the old regex and the schema, the rest refused by both, 0 answered differently. -- An instant, a number or a `Date` on a `time` column is judged as before. diff --git a/.changeset/20773-etl-entry-syncconfig-schedule-deleted-in-17.md b/.changeset/20773-etl-entry-syncconfig-schedule-deleted-in-17.md deleted file mode 100644 index 8fbc5fa519c..00000000000 --- a/.changeset/20773-etl-entry-syncconfig-schedule-deleted-in-17.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): two ADR-0087 migration entries state what the tree does — `etl-pipeline-layer-retired` dates the `syncConfig.schedule` deletion to `@objectstack/spec` 17, and `driver-sql-unresolvable-where-column-refused` names the remote `aggregate()` refusals - -Clause-②: no - -**`etl-pipeline-layer-retired` (protocol 17).** The entry explains that connector-attached -`syncConfig` has no reader outside `packages/spec`, and cites the same measurement that removed -`syncConfig.schedule`. Its `replacement` text said that key was retired "in 18". It was deleted -in `@objectstack/spec` 17 under ADR-0049 (first released in 17.5.0), as the note at the deleted -position in `integration/connector.zod.ts` already says. The sentence now reads "the same -measurement that deleted `syncConfig.schedule` in @objectstack/spec 17 under ADR-0049". - -**`driver-sql-unresolvable-where-column-refused` (protocol 18).** The entry named only a `where` -column on `find()` / `findOne()` / `count()` and `INVALID_FILTER` / 400. On the remote face of -`TursoDriver`, the `aggregate()` door answered `[]` for a missing column or a missing table. It -now refuses as the local face does: `INVALID_FILTER` / 400 for a `where` column the table lacks, `INVALID_FIELD` / 400 for a -`groupBy` or aggregation column the table lacks, and `DATABASE_ERROR` / 500 for an object whose -table is absent. The entry's `surface` now names that door and those codes. Its remedy adds -grouping and aggregating, and running schema sync so the object's table exists. Its acceptance -criterion now also covers a report or dashboard that groups by, or aggregates over, a name the -object has no column for. - -Text only: no entry id, conversion or matching logic changes, and `os migrate meta` rewrites -exactly what it rewrote before. The generated migration registry, `spec-changes.json` and the -protocol upgrade guide carry the corrected text. diff --git a/.changeset/20783-groupby-structured-json-refused.md b/.changeset/20783-groupby-structured-json-refused.md deleted file mode 100644 index 77c31eee744..00000000000 --- a/.changeset/20783-groupby-structured-json-refused.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: a `groupBy` on a structured-JSON field is refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `aggregate` accepts as a grouping target. A `groupBy` entry that names a declared field of the structured-JSON class (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) is refused by the engine before any driver is asked. Both entry spellings are judged, the field name and the `{ field }` object, a `dateGranularity` bucket included. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, or `groupBy[0].field` for the object form), the field and its declared type, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own. The thrown error carries `field`, `fields`, `object` and `param: 'groupBy'`. - -**Why a refusal.** The drivers share no meaning for a JSON document as a group key. Measured through `POST /api/v1/data/:object/query` over three rows with different documents under the grouped field: the in-memory driver answered 200 with one group holding every row, SQLite answered 200 with one group per serialized document, and PostgreSQL answered 500 `DATABASE_ERROR`. A `vector` field split the same three ways, and a date bucket over a `json` field answered one `null` bucket on memory and SQLite and 500 on PostgreSQL. No producer that groups by a structured-JSON field was found (no dataset, cube, view grouping or `groupBy` in the example apps names one), so no meaning is defined for it here. - -**Who is affected.** A caller of `engine.aggregate` or of the REST query door that grouped by such a field on the in-memory driver or on SQLite and read the merged or per-serialization groups as real ones. On PostgreSQL the same query was already a 500. The analytics service's aggregate path (a cube query the native-SQL strategy declines, such as a time dimension with a granularity, or any cube query on the in-memory driver) reaches the engine and answers this refusal too. - -**Unchanged.** A `groupBy` on any other type (`text`, `number`, a `multiple: true` select, a file field), a structured-JSON field as an AGGREGATED column (`count`, `count_distinct`, `min`, `max`), and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. diff --git a/.changeset/20802-dotted-relation-route.md b/.changeset/20802-dotted-relation-route.md deleted file mode 100644 index eceb7262af3..00000000000 --- a/.changeset/20802-dotted-relation-route.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -fix(metadata-protocol): a dotted relation filter path names the nested-relation form as the route - -A filter key such as `account.industry` — a dotted path through a relation field — is still refused with `INVALID_FIELD` / 400 at the query parameter door. Its words no longer say a filter reaches only the object's own columns, which stopped being true when the engine began serving the nested-relation form in `where`: they now name that form, `{ "account": { "industry": VALUE } }`, beside the denormalise remedy, in the same words as the engine's own refusal. diff --git a/.changeset/20802-nested-relation-filter-served.md b/.changeset/20802-nested-relation-filter-served.md deleted file mode 100644 index 0546c007566..00000000000 --- a/.changeset/20802-nested-relation-filter-served.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -feat(objectql): the nested-relation filter `{ relation: { field: value } }` is served in `where`, lowered at the engine's filter seam — the drivers receive `$in` / `$contains` and are unchanged - -Clause-②: yes (widening) - -A condition on a related record's own fields, written beneath a relation field of the queried object — `{ "account": { "industry": "tech" } }` beneath a `lookup` — is now answered by the engine in `where`, on every verb that takes one (`find`, `findOne`, `count`, `aggregate`, `update`, `delete`) and by `judgeFilter`. It was refused with `INVALID_FILTER` / 400 until now; this supersedes the relation-field paragraph of the pending `20745-nested-object-door` entry. - -**How it is answered.** The engine reads the related object with the condition, then matches the relation field against the ids that read returns, and the drivers receive only that: `{ "account": { "$in": [ids] } }` on a single-valued relation, and on a multi-valued one (`multiple: true`) an `$or` of one `$contains` per id, so it matches on any member. The relation types are `lookup`, `master_detail`, `user` and `tree`. It composes as written inside `$and` / `$or` / `$not`, and the `FilterArray` sugar lowers to it too. No related record matching selects no rows; under `$not`, a record whose relation is empty satisfies the negation. - -**As the caller.** The related read is the engine's own `find` on the related object with the caller's execution context, so that object's access check, row scope and field permissions apply exactly as they do to a direct read of it. A condition on a field the caller cannot read is refused by the same check that refuses a direct filter on it (`PERMISSION_DENIED` / 403, naming the field), never answered with an empty list; a related record the caller cannot see matches no condition. - -**Bounded.** At most `RELATION_FILTER_ID_CAP` (1,000, exported) related ids feed one condition. A condition matching more is refused with `INVALID_FILTER` / 400, naming the cap, the related object and the two-step route — never run over a cut-off list. - -**Still refused, in the engine's words (`INVALID_FILTER` / 400, before any read):** a second level (a relation condition beneath the related object's own relation field, or a dotted key inside the condition), a key the related object does not declare, an empty condition `{}`, and a related object that is not registered. An aggregation's own `filter` and `having` keep refusing the form, and their words now name `where` as the place it is served. The dotted spelling `{ "account.industry": "tech" }` stays refused with `INVALID_FIELD` / 400, and its words now name the nested form to write instead. The structured-JSON and scalar-field refusals are unchanged. - -Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 (owner `u1`, region NA, on `d1` and `d3`; `d4` has no owner): - -| `where` | before | now | -|:--|:--|:--| -| `{ owner: { region: "NA" } }` on a `lookup`, and its `master_detail` and multiple-lookup twins | `INVALID_FILTER` / 400 | `d1`, `d3` | -| `{ parent: { title: "a" } }` on a `tree` field | `INVALID_FILTER` / 400 | `d2`, `d3` | -| `{ $not: { owner: { region: "NA" } } }` | `INVALID_FILTER` / 400 | `d2`, `d4` | -| `{ owner: { region: "APAC" } }` (no owner matches) | `INVALID_FILTER` / 400 | no rows | - -SQLite, PostgreSQL and the in-memory driver match the element of a multi-valued relation, so an id that is a substring of another stored id (`u1` inside `u10`) does not match it. diff --git a/.changeset/20802-nested-relation-prose.md b/.changeset/20802-nested-relation-prose.md deleted file mode 100644 index 30c30a9cd7a..00000000000 --- a/.changeset/20802-nested-relation-prose.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -docs(spec): the `FilterCondition` docblock says the query engine serves the nested-relation form in `where` - -`FilterCondition`'s form 4, `{ relation: { field: value } }`, now states the served semantics: the engine reads the related object with the condition as the caller (its row scope and field permissions apply), matches the relation field against the ids it returns (`$in`, or any member on a multi-valued relation), reaches one level, and refuses a condition matching more related records than its cap rather than truncating. The `QueryFilter` example shows the form again, and the `Filter` nested arm's comment says the engine serves one level. The type and the schema are unchanged. diff --git a/.changeset/20805-formula-write-strip.md b/.changeset/20805-formula-write-strip.md deleted file mode 100644 index 5e42c0d54af..00000000000 --- a/.changeset/20805-formula-write-strip.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/objectql': minor -'@objectstack/service-automation': patch ---- - -A caller-supplied value for a `formula` field is stripped on every engine write path, in every context, and reported through `droppedFields` / `onFieldsDropped` under a new `reason`, `computed`; and `ObjectQL.validate` runs the write's own field doors, so a dry run built on it predicts what the write will do (#20805). - -Clause-②: yes (narrowing) - - - -**BREAKING**: `ObjectQL.validate` now refuses a row that carries a key the object does not declare, exactly as `insert` and `update` refuse it: the call throws `INVALID_FIELD` / 400 naming the field. It used to answer `valid: true` for that row while the write it previews refused it, so the protocol's `validateData` and the import dry run built on it said "ok" for rows the commit then failed. It ships as `minor` under the launch-window convention; the widening half is the new `reason` arm. - -**A formula value is stripped, never refused.** A `formula` field is computed on every read and no driver has a column for it, so a full read returns the key and a record written back carries it: a form save, a flow's `update_record`, a `GET` then `PUT`. The key used to reach the driver, and the driver decided. On SQL drivers the whole write failed with the driver's own error (`SqliteError` "table … has no column named …", with no `status` and no `field`; the REST door relabelled it `400 INVALID_FIELD` "Unknown field" for a field the object declares). On the in-memory driver the value was stored as a shadow column nothing reads. Now the engine takes the value out before the defaults, the hooks and the other strips, completes the write, and reports one `{ reason: 'computed' }` event per call. That holds on `insert` (one row or a batch), `insertMany`, and `update` by id and by predicate, on every driver, and in every context, `isSystem` included: there is no column for any caller's value to land in. Measured on SQLite and the in-memory driver through `protocol.createData`, `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and `engine.update`: each now answers success with `droppedFields: [{ fields: ['doubled'], reason: 'computed' }]`, the stored row carries no such key, and the read still returns the computed value. - -- **`computed` is not `readonly`.** `isSystem` exempts the static `readonly` strip and does not exempt this one. A `formula` field also declared `readonly: true` is reported once, as `computed`. -- **`strictReadonlyWrites` refuses it.** That option's coverage is derived from what `onFieldsDropped` reports, so a caller that passes it and sends a formula value now gets `ERR_READONLY_FIELD_REJECTED` with a `computed` drop in `drops`, and nothing is written, `isSystem` included. The refusal message names the reason and its remedy; a refusal without a `computed` drop reads exactly as before. -- **Hooks are handed the payload that will be stored.** A `beforeInsert` / `beforeUpdate` hook no longer sees the formula key in `ctx.input.data`; `ctx.submitted` on update still carries the caller's submission as sent. -- **Consumers of `DroppedFieldsEvent['reason']` must handle `computed`.** The contract requires a branch on `reason` to be exhaustive. In this release the strict refusal message (`@objectstack/objectql`) and the flow `create_record` / `update_record` step warning (`@objectstack/service-automation`) word it. - -**What `validate` runs now.** Before judging a row, `ObjectQL.validate` runs the write's own doors, by the same functions the write calls: the declared-field door (the refusal above), the computed-field strip, and the caller-write strips, under the write's `isSystem` gate (on `insert` mode the runtime-owned strip and the static `readonly` strip with its re-default; on `update` mode the static `readonly` strip, where a supplied `id` is the address the write binds and is never judged). What the write would drop is reported through a new optional `onFieldsDropped` listener on `validate`'s options, in the same events the write emits. One consequence for verdicts: a reference field declared static `readonly` is now stripped in the preview as it is on the write, so a validation rule that reads through it answers the same on both. - -**Unchanged.** A `summary` field keeps its column: a caller-supplied roll-up value is still stored as sent and overwritten by the next write of a child record. The REST layer's own handling of a missing column (schema drift) is unchanged. diff --git a/.changeset/20807-analytics-json-dimension-refused.md b/.changeset/20807-analytics-json-dimension-refused.md deleted file mode 100644 index 7645f894e8a..00000000000 --- a/.changeset/20807-analytics-json-dimension-refused.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: a cube or dataset dimension on a structured-JSON field is refused with `INVALID_FIELD` / 400 at the analytics door, before any SQL is built - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the analytics query doors accept as a dimension. A cube dimension, or a dataset dimension, whose column is a declared field of the structured-JSON class (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) is refused before either strategy builds a statement, when it groups the result: a `dimensions` entry, or a `timeDimensions` entry with a `granularity`. The column is judged where it is declared: on the cube's object, or, for a dotted path such as a dataset dimension over `account.hq`, on the object the cube's declared join for that path names. It holds on `POST /api/v1/analytics/query`, on its dry run `POST /api/v1/analytics/sql`, and on `POST /api/v1/analytics/dataset/query`, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What an author sees now.** `400 INVALID_FIELD`, naming the member as the request wrote it (the cube dimension, or the dataset dimension), the column it groups by, the object and the column's declared type, saying the query was not run, and naming the route: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own. The thrown error carries `member`, `param` (`dimensions` or `timeDimensions`), `cube`, `field` and `object`. - -**Why a refusal.** A JSON document is no group key the SQL dialects share. Measured through `POST /api/v1/analytics/query` over three rows with a different document each, on the service `AnalyticsServicePlugin` composes over a real engine: SQLite answered 200 with one group per serialized document, and PostgreSQL 16 answered 500 `DATABASE_ERROR`. A dataset dimension over a joined object's `json` field answered the same two ways through `POST /api/v1/analytics/dataset/query`. The native-SQL strategy compiled the `GROUP BY` itself, so the engine's own refusal of a structured-JSON `groupBy` never saw the query; the engine-aggregate strategy did reach that refusal, but named the engine's `groupBy[0]` position rather than the member the caller wrote. The class is `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, the one the engine's refusal reads. No producer groups by such a field: no cube or dataset dimension in the example apps names one. - -**Who is affected.** A dashboard, report or caller that grouped an analytics query by a structured-JSON field on SQLite and read one group per serialized document as real groups. On PostgreSQL the same query was already a 500. - -**Unchanged.** A dimension on any other type; a `timeDimensions` entry with no `granularity`, which bounds a range and groups nothing; measures (this door judges only the members that group); a dotted dimension path the cube declares no join for, whose object is not a declaration; a member naming a column the object does not have, which keeps its existing `INVALID_FIELD` answer first; and a host that wires no `sourceFieldMeta`, where the column's type cannot be read. diff --git a/.changeset/20808-json-stored-group-distinct-refused.md b/.changeset/20808-json-stored-group-distinct-refused.md deleted file mode 100644 index c4b90cf36cc..00000000000 --- a/.changeset/20808-json-stored-group-distinct-refused.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@objectstack/objectql": minor -"@objectstack/spec": minor -"@objectstack/lint": patch -"@objectstack/service-analytics": patch ---- - -fix(objectql,spec)!: a `groupBy` on a multi-value field and a `count_distinct` on a JSON-stored field are refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver, and the aggregate × field-type table stops accepting `count_distinct` over the JSON-stored types - -Clause-②: no (narrowing) - - - -**BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, in two positions, on every driver and for every caller that reaches the engine (the REST query door, a flow or hook, and the analytics strategy that lowers a cube query onto `engine.aggregate`). Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -- A `groupBy` entry that names a **multi-value** field: an inherently-multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. Both entry spellings are judged, the field name and the `{ field }` object. -- A `count_distinct` aggregation over a **JSON-stored** field: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently-multi option type, or a multi-capable field declared `multiple: true`. - -**BREAKING** (`@objectstack/spec`): `AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct` no longer lists the ten JSON-stored types (the structured-JSON seven and `multiselect`, `checkboxes`, `tags`), so `isAggregateCompatibleWithFieldType('count_distinct', type)` answers `false` for them. Every reader of the table refuses those pairs now: the dataset-measure lint rule (`measure-aggregate-field-type-refused`, run by `os validate` and at a runtime dataset save), the analytics dataset compile leg (`400 DATASET_INVALID`), and the engine door above. The `count` row is unchanged. - -**What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, `groupBy[0].field`, or `aggregations[0].field`), the field and its declaration, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps. For a multi-value field the route is to filter by one member: `where` with `$contains` on the field, one query per member. For a structured-JSON field it is to store the part you count in a field of its own, or to count rows with `count`. The thrown error carries `field`, `fields`, `object` and `param` (`groupBy` or `aggregations`). - -**Why a refusal.** Every SQL driver stores these values in a JSON column, and the drivers share no meaning for one as a group key or a distinct key. Measured through `POST /api/v1/data/:object/query` over three rows: grouping by any of the eight multi-value declarations answered one group per array on the in-memory driver, one group per serialized array on SQLite, and 500 `DATABASE_ERROR` on PostgreSQL 16. `count_distinct` over any structured-JSON or multi-value field answered 3 on the in-memory driver (equal values counted apart), 2 on SQLite (serialized text compared), and 500 on PostgreSQL (no equality operator for `json`). No example app and no published stack groups by a multi-value field or counts one distinct, so no meaning is defined for either here. - -**What to write instead.** A dataset measure or a query that counted a JSON-stored field distinct: use `count` over it, or store the scalar part you meant to count in a field of its own and `count_distinct` that field. A grouping by a multi-value field: filter by each member with `$contains` and count. - -**Who is affected.** A caller that grouped by a multi-value field, or counted a JSON-stored field distinct, on the in-memory driver or on SQLite and read the answer as a real one; on PostgreSQL both were already a 500. A dataset whose measure pairs `count_distinct` with a JSON-stored field is refused by the lint rule and the compile leg. - -**Unchanged.** (Two shapes the structured-JSON `groupBy` entry of this same release lists as unchanged are narrowed here: a `multiple: true` select as a group key, and `count_distinct` over a structured-JSON field. This entry is the later word on both.) A `groupBy` or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; the `having`, filter and sort positions; and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. - -`@objectstack/lint`: the dataset-measure refusal's hint no longer says `count_distinct` accepts every type. - -`@objectstack/service-analytics`: the dataset compile leg's refusal of a `count_distinct` measure over a JSON-stored field says why it diverges (the drivers compare the values for equality three ways) and prescribes `count`, or a scalar field for the part being counted; its other refusals no longer say `count_distinct` accepts every type. diff --git a/.changeset/20809-guidance-slot-docblocks.md b/.changeset/20809-guidance-slot-docblocks.md deleted file mode 100644 index 9886ddce229..00000000000 --- a/.changeset/20809-guidance-slot-docblocks.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the `strictObject` `guidance` option and the `ToolSchema` guidance table no longer describe `guidance` rows as tombstones for retired keys - -Clause-②: no - -`StrictObjectOptions.guidance` (in the published declarations) and the docblock above -`TOOL_RETIRED_KEY_GUIDANCE` in `src/ai/tool.zod.ts` (shipped as source) called the slot a -place for "tombstones for retired keys". A tombstone is `retiredKey()` in the shape, which -keeps the key declared, and a `guidance` row for a declared key never fires. The docblocks -now say what the slot is for: prescriptions for keys the shape does not declare — -wrong-layer pointers, and the upgrade for a spelling removed from the shape. - -Text only: no schema, no guidance entry, no prescription and no parse behaviour changes. diff --git a/.changeset/20810-analytics-seam-lowering.md b/.changeset/20810-analytics-seam-lowering.md deleted file mode 100644 index 9ad1394e3c3..00000000000 --- a/.changeset/20810-analytics-seam-lowering.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -The analytics seams now run the one shared filter lowering (`lowerFilterCondition`, `@objectstack/spec/data`) that ADR-0053 D-D1, as amended, places at every seam that runs the shared comparand doors: after the doors and after filter-token resolution, so each face compiles one lowered condition — the `$between` split, the whole-day upper bound on a bare `YYYY-MM-DD`, the last supported day, and the NULL-polarity guards. - -Clause-②: yes - -**The read scope (`compileScopedFilterToSql`).** The scope is lowered at the compiler's entry, right after its placeholders resolve. It reads each column's declared type from the `declaredValueShape` option both of its consumers already pass, so the whole-day rule rewrites a declared `datetime` column and nothing else; with no declarations handed in, no column is read as `datetime`. Corrected answers, each now the rows `SqlDriver.find` returns for the same filter: - -- a bare-day `$lte` on a declared `datetime` column kept only the rows before that day and dropped the day itself. Rows at 10:00Z on 07-27, 07-28 and 07-29 under `{ signed_at: { $lte: '2026-07-28' } }` answered 07-27 alone; they now answer 07-27 and 07-28, compiled as `< '2026-07-29'`. This is the NativeSQL statement's read scope and the `/analytics/sql` echo's. -- a bare-day `$between` on a declared `datetime` column answered no row for a one-day range, and now answers that day's rows. -- a `{today}` (or any date-macro) upper bound is widened as the day it resolves to. - -An RLS `using` bound already reached the read scope lowered by the RLS compile seam, and answers as before. A declared `date` column compiles byte-identical to before. - -**The analytics `where` and draft-preview door.** The condition the door admits is lowered before either face reads it. The `where` → tree face (both strategies) reads each member's declared column type through the host's declared-type hook: a bare-day `$lte` on a `datetime` member now reaches the engine as `$lt` the next day, and the `/analytics/sql` echo prints that half-open bound — the statement the engine runs, where it used to print `<=` the named day. Rows are unchanged on every strategy. A nested-relation filter (`{ account: { region: 'NA' } }`) is spelled as the dotted member it has always compiled to before the lowering reads it, so a guard it adds under `$not` names that member. - -The draft preview (`queryDataset` with `previewDrafts`) now evaluates `$null` — the one operator the lowering emits that it did not — so a drafted chart filtered on `{ field: { $null: true } }` is answered instead of refused `INVALID_FILTER` / 400; `$exists` and `$empty` stay refused. Corrected answers: a row with no value now satisfies `$ne`, `$nin` and the negation of an equality even when the comparand is the text `"null"` or `"undefined"`, which this face used to compare as text against the missing value — the answer every data driver gives. Its bare-day bounds answer as before. - -Compiled SQL for `$ne`, `$nin`, `$notContains` and a `$not` operand now carries the lowering's NULL guard around each face's own copy of it: the same rows, a longer statement, until those copies are deleted. diff --git a/.changeset/20810-memory-cube-face-door.md b/.changeset/20810-memory-cube-face-door.md deleted file mode 100644 index 68eec306f9e..00000000000 --- a/.changeset/20810-memory-cube-face-door.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/driver-memory': minor ---- - -`MemoryAnalyticsService` (the in-memory analytics cube face) now runs the two shared filter comparand doors every other analytics face runs, then the shared filter lowering of ADR-0053 D-D1 (as amended), before it compiles a query's `where`. It also compiles `$or` and `$null`, the two parts of the lowering's output it could not. - -Clause-②: yes (narrowing) - - - -**BREAKING**: `query()` and `generateSql()` now refuse, with `INVALID_FILTER` / 400, filter comparands they used to answer. Each is refused the same way by every other analytics face and by the query engine, so a filter that worked here worked nowhere else. Measured on a fixture where `d` is `'v1'`, `'v2'`, `null` and absent: - -- `undefined` in any comparand position — `{ d: undefined }` and `{ d: { $eq: undefined } }` answered the no-value rows, `{ d: { $ne: undefined } }` the valued ones, `{ d: { $in: ['v1', undefined] } }` row `v1`; -- a `null` member of `$in` / `$nin` (`{ d: { $in: ['v1', null] } }` answered `v1` and both no-value rows), and a `null` under an ordering operator (`{ d: { $gt: null } }` answered no row); -- a scalar where `$in` / `$nin` takes a list (`{ d: { $in: 'v1' } }`); -- a plain object, a `Map` or a binary value as a comparand — `{ d: { $ne: { a: 1 } } }` and `{ d: new Map() }` answered EVERY row. - -The fix is to write the comparand you mean: `null` or `{ $null: true }` for "has no value", a list for `$in` / `$nin` (and `{ $null: true }` in a `$or` for "one of these, or no value"), a scalar for an ordering operator. - -Corrected answers, each now what the live query path (`find()`) returns: - -- a bigint comparand within 2^53 (`{ n: { $gt: 2n } }`) is read as its number and answered; beyond 2^53 it is refused `INVALID_FILTER` / 400. Both used to fail with an uncoded error. -- `$between` is answered as its two bounds, with a bare-day maximum widened to the whole day before it is converted to the field's storage form; it was refused. -- `$null` (true: no value; false: has a value) and `$or` (a `{}` branch is TRUE, `$or: []` is FALSE) are compiled on both exits; they were refused. `$not`, `$startsWith`, `$endsWith` and `$empty` stay refused. `ANALYTICS_FILTER_CAPABILITIES` names `$null` and the `$or` combinator accordingly. - -The `generateSql()` echo and the `query()` pipeline dump for `$ne`, `$nin` and `$notContains` now show the lowering's NULL escape around this face's own guard — `(d IS NULL OR (d IS NULL OR d != 'v1'))` — the same rows as before. diff --git a/.changeset/20819-regime-c-refusal-path.md b/.changeset/20819-regime-c-refusal-path.md deleted file mode 100644 index c656250f7eb..00000000000 --- a/.changeset/20819-regime-c-refusal-path.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): the refusal of an in-place edit or removal of a packaged flow names the clone and the on/off switch, not a redeploy or `OS_METADATA_WRITABLE` (#20819) - -Clause-②: no - -A write or removal that targets a flow shipped by a code package, and does not name that package, is refused with `403 NOT_OVERRIDABLE`. That covers `PUT /api/v1/meta/flow/:name` without `?package=`, `DELETE /api/v1/meta/flow/:name`, `PUT` and `DELETE /api/v1/automation/:name`, and `POST /api/v1/automation` onto a packaged flow's name. The refusal used to say "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch", and cited ADR-0005. The administrator of an installed package can do neither. - -The refusal now names the two paths ADR-0126 sanctions for a packaged flow, and cites ADR-0126: - -- clone it under a new name to customize it: `POST /api/v1/automation/:name/clone` with `{ name, label }`; -- or switch it off: `POST /api/v1/automation/:name/toggle` with `{ enabled: false }`. Where one install serves several organizations, only the platform operator can use the switch. - -The status, the code and which writes are refused are unchanged. `OS_METADATA_WRITABLE=flow` still opens the lock as before; the refusal just no longer suggests it. Every other metadata type's refusal reads exactly as before. A write that names the shipping package with `?package=` is refused with `403 ITEM_LOCKED` by a separate limb, which this change leaves as it was. diff --git a/.changeset/20821-plan-deferred-ddl-reads.md b/.changeset/20821-plan-deferred-ddl-reads.md deleted file mode 100644 index 4bf127ac919..00000000000 --- a/.changeset/20821-plan-deferred-ddl-reads.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -fix(driver-sql): `os migrate plan` on a database that does not exist yet no longer prints `DATABASE_ERROR` for the tables whose DDL it deferred (#20821) - -`os migrate plan` (and the boot of `os migrate apply`) runs with the SQL driver's DDL deferred: the driver records every table as pending `create_table` and creates none of them. The same boot then reads `sys_metadata`, `sys_metadata_activation` and `sys_migration`. On a new database those tables do not exist yet, so each read was refused, and each refusal printed a line like this on the driver's warn channel (stderr by default): - -```text -[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_metadata' (SQLITE_ERROR) ... no such table: sys_metadata -``` - -Nothing was wrong: every reader already answers from the refusal, and the plan lists the same tables as pending creates. A dry run on a new database printed six of these lines. - -The driver now sends such a refusal to the logger's `debug` channel instead of `warn`, when all three hold: - -- this driver has DDL deferred; -- the refused statement targets a table whose DDL this driver deferred; -- the shared `isMissingTableError` predicate from `@objectstack/types` recognises the refusal as that table being missing. - -The default logger has no `debug`, so the line is not printed. The refusal is still thrown to the caller with the same envelope (`DATABASE_ERROR`, status 500), and the plan's output is unchanged. - -What still warns: - -- every other refusal on a deferred driver, such as a malformed statement on a table that exists; -- a missing table that the driver did not defer, such as a table nothing in the boot declares; -- every refusal once the deferred DDL has been applied, or on a driver that never deferred any. - -There is nothing to migrate. diff --git a/.changeset/20822-driver-memory-face-copies.md b/.changeset/20822-driver-memory-face-copies.md deleted file mode 100644 index c9c01d972a3..00000000000 --- a/.changeset/20822-driver-memory-face-copies.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/driver-memory': patch ---- - -refactor(driver-memory): the cube face's own whole-day bound and the in-memory reference matcher are deleted; no answer a caller gets moves (#5930 step 4, #20822) - -Clause-②: no - -- **`MemoryAnalyticsService` (the cube face).** Its `where` door has run the shared `lowerFilterCondition` (`@objectstack/spec/data`) since #5930 step 3, on every column. A bare-day `$lte` therefore reaches the `lte` row already lowered: as `$lt` the next day, or as `$null: false` on `9999-12-31`. The row's own copy of that rule is deleted, and the `lte` row now compiles the comparison it is handed on both exits. The rows `query()` returns and the SQL `generateSql()` echoes are unchanged. An explicit `dateRange` end still widens a bare day through its own window arm (ADR-0053 D-D1 item 8). -- **The reference matcher (`memory-matcher.ts`, `match()`) is retired** (ruling D6 on #5930). No production code called it and the package never exported it: the published `dist` exports are the same 33 names before and after. `InMemoryDriver` keeps `getValueByPath`, the one helper it imported from that module. The matcher's tests now assert the live query path (`InMemoryDriver.find`), the shared filter shape gate, or the spec predicate the matcher evaluated. diff --git a/.changeset/20822-driver-mongodb-face-copy.md b/.changeset/20822-driver-mongodb-face-copy.md deleted file mode 100644 index 8775284b7bc..00000000000 --- a/.changeset/20822-driver-mongodb-face-copy.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/driver-mongodb': patch ---- - -fix(driver-mongodb): `MongoDBDriver` compiles the whole-day comparison it is handed — its own copy of the bare-day upper bound is deleted (ADR-0053 D-D1 items 5, 7 and 9, #20822) - -Clause-②: no - -- **What is deleted.** `translateFilter` no longer widens a bare `YYYY-MM-DD` `$lte` to `$lt` the next day, no longer widens a `$between` maximum the same way, and no longer turns `$lte '9999-12-31'` into `$ne: null` or drops a `$between` maximum on that day. Every verb that translates a `where` inherits it: `find`, `findOne`, `count`, `updateMany`, `deleteMany`, `explain`, and the `$match` stage of `aggregate` / `buildAggregationPipeline`. -- **A read through the engine or the RLS compile seam is unchanged.** The seam hands the driver a filter the shared `lowerFilterCondition` (`@objectstack/spec/data`) has already rewritten on the declared `datetime` columns: `$lt` the next day, a split `$between`, `$null: false` on the last supported day. The deleted copy gave the same answer on that input. A `date` column answers as before, because its stored calendar-day text orders the same either way. -- **Two answers converge on what `SqlDriver` returns** (ADR-0053 D-D1 item 7's scope). On a registered object, a bare-day `$lte` or `$between` maximum is now compared as written on a column that is not declared `datetime`: a `text` column holding ISO instant text, or a column the object does not declare. This driver used to widen it to the whole day. -- **A caller that passes no seam gets the comparison it wrote** (item 5): a `MongoDBDriver` verb or `translateFilter` called directly. A bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, and `$lte '9999-12-31'` compares against that midnight. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. -- No exported name changes. diff --git a/.changeset/20822-driver-sql-turso-copies.md b/.changeset/20822-driver-sql-turso-copies.md deleted file mode 100644 index 2f120a2797a..00000000000 --- a/.changeset/20822-driver-sql-turso-copies.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/driver-sql': minor -'@objectstack/driver-sqlite-wasm': patch -'@objectstack/driver-turso': patch -'@objectstack/plugin-security': patch -'@objectstack/spec': patch ---- - -fix(driver-sql, driver-turso, plugin-security, spec)!: `SqlDriver` and `TursoDriver` compile the filter they are handed — their copies of the whole-day bound and of the NULL-safe `$not` rewrite are deleted, and the RLS compile seam lowers type-blind when it cannot read the declared types (ADR-0053 D-D1 items 5, 7 and 9, #20822) - -Clause-②: no (narrowing) - - - -**BREAKING**: `SqlDriver` in `@objectstack/driver-sql` loses three `protected` methods: `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite`. They were the driver's copy of the whole-day bound, which the shared lowering now applies once, at the seams, before a driver sees the filter. A subclass of `SqlDriver` that calls one of them, or overrides one with the `override` modifier, no longer compiles (TS2339, TS4113). That includes a subclass of `SqliteWasmDriver` or `TursoDriver`, which extend `SqlDriver`. A subclass that re-declares one without `override` still compiles, but the driver never calls it, so the rule it carried stops applying. It ships as `minor` under the launch-window convention. The class's public methods are unchanged. - -FROM → TO: a `SqlDriver` subclass that called `this.calendarDayUpperBoundRewrite(table, field, op, value)`, `this.calendarDayBetweenRewrite(table, field, value)` or `this.calendarDayExclusiveUpperBound(table, field, value)`, or overrode one of them, lowers the filter with `lowerFilterCondition` from `@objectstack/spec/data` instead, before the driver compiles it: `lowerFilterCondition(where, { isDatetimeColumn })`, where `isDatetimeColumn` answers which columns get the whole-day bound. - -**Supersedes two sentences of this release's shared-lowering entry** (`lowerFilterCondition`, #5930), which this change makes false: - -- "A guard without that set treats no column as `datetime`." Now: when the RLS compile seam has no field guard, or one without a `datetime` set, it cannot read which columns are `datetime`, so it applies the whole-day rule to every column (the `@objectstack/plugin-security` entry below). -- "Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input." Now: `SqlDriver`, `SqliteWasmDriver` and `TursoDriver` keep no copy of the whole-day bound or of the NULL-safe `$not` rewrite. A read through the engine or the RLS compile seam gets the lowered answer, and a call on the driver itself gets the comparison it wrote (the `@objectstack/driver-sql` and `@objectstack/driver-turso` entries below). - -- **`@objectstack/plugin-security` — an RLS policy compiled with no field guard is lowered type-blind.** The RLS compile seam runs the shared `lowerFilterCondition` on every compiled `using` and `check` filter. When the security plugin could not resolve the object's declared fields (no field guard), or a caller of `RLSCompiler.compileFilter` passes a guard without a `datetime` set, the seam cannot read which columns are `datetime`, and it now applies the whole-day rule to every column (a bare-day upper bound becomes `$lt` the next day), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the type. It used to read no column as `datetime`, which left the bound to each driver's own copy of the rule. Visible on a `using` policy such as `record.signed_on <= '2026-01-05'` on such an object: every row of that day is kept on every driver, including `InMemoryDriver`, which had compared it as written since its own copy was deleted. A guard with a `datetime` set is unchanged, and so are the NULL-polarity guards. -- **`@objectstack/driver-sql` — `SqlDriver` keeps no copy of the rules the seams apply.** Deleted: the whole-day rewrite of a bare-day `$lte` and of a `$between` maximum on a `datetime` column, on the plain and the legacy-normalised column paths, including the last supported day (the protected methods `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite` are removed from the class); and the NULL-safe rewrite of a `$not` operand (`nullSafeNegationOperand` and its polarity tables, module-private). A read through the engine or the RLS compile seam is unchanged: the seam hands the driver a filter the shared lowering has already rewritten, and the deleted copies gave the same answer on that input. A caller that passes no seam — `find`, `findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` or `findWithWindowFunctions` called on the driver itself — now gets the comparison it wrote: a bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, `$lte '9999-12-31'` compares against that midnight, and a `$not` is SQL's three-valued negation, so a row whose compared column is NULL is not returned by it. `$ne`, `$nin` and `$notContains` keep their NULL-safe form, which this emitter spells for the operator itself. The refusal of an `undefined` comparand (`INVALID_FILTER` / 400) is kept: without it some positions would answer instead of refusing. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. A subclass that called or overrode one of the three removed methods: see **BREAKING** above. -- **`@objectstack/driver-sqlite-wasm` — `SqliteWasmDriver` inherits the `SqlDriver` change above**, with the same answers on a seamed read and on a direct call. -- **`@objectstack/driver-turso` — both faces of `TursoDriver` compile the filter they are handed.** Local and replica mode inherit the `SqlDriver` change. Remote mode: `toRemoteFilter` no longer widens a bare-day `$lte` or a `$between` maximum (it still splits a two-bound `$between` into the `$gte` / `$lte` pair the remote transport compiles, both ends inclusive, and still converts each comparand to storage form), and `RemoteTransport` no longer rewrites a `$not` operand (its copy of the polarity tables is deleted). The two faces still answer every filter alike, on a seamed read and on a direct call. The remote transport keeps its refusal of an `undefined` comparand, worded as `driver-sql`'s, so both faces refuse it in one sentence. The same one line keeps the seam's reading on a direct call. -- **`@objectstack/spec` — the ADR-0087 ledger records the removal.** The protocol-18 step of `MIGRATIONS_BY_MAJOR` gains the semantic entry `driver-sql-calendar-day-methods-removed`, which names the three removed methods with their replacement and acceptance criteria. Every upgrade channel that projects protocol 18 carries it. `spec-changes.json` and the generated upgrade guide stop at the current protocol, 17, so neither changes in this release. A subclass that re-declares one of the methods without `override` still compiles and is never called, so the ledger, not the compiler, is the notice that reaches it. diff --git a/.changeset/20822-f3-route-then-delete.md b/.changeset/20822-f3-route-then-delete.md deleted file mode 100644 index 8de990234ad..00000000000 --- a/.changeset/20822-f3-route-then-delete.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata': patch -'@objectstack/objectql': patch -'@objectstack/driver-memory': patch ---- - -fix: the whole-day bound on a bare `YYYY-MM-DD` upper bound is applied at the seams only — `DatabaseLoader.queryHistory` in driver mode becomes one, the engine seam lowers type-blind for an object with no field map, and `InMemoryDriver` drops its own copy (ADR-0053 D-D1 items 5 and 7, #20822) - -Clause-②: no - -- **`@objectstack/metadata` — `DatabaseLoader.queryHistory` in driver mode lowers its own filter.** With a raw `IDataDriver` (`MetadataManager.setDatabaseDriver`) the history filter reaches the driver without passing any seam. The loader now runs the shared `lowerFilterCondition` (`@objectstack/spec/data`) on it, typed by the history object it syncs: `until: 'YYYY-MM-DD'` reads `recorded_at < next day`, so every version recorded on that day is kept on every driver, and an instant `until` is kept as written. Engine mode is unchanged (the engine's own `where` seam lowers it). Before this, the whole day was kept only by each driver's own copy of the rule; with `@objectstack/driver-memory`'s copy deleted below, `until` = today would have gone from every version of the day to none. -- **`@objectstack/objectql` — an object with no field map is lowered type-blind.** The engine's `where` seam (on `find`, `findOne`, `count`, `update`, `delete` and `aggregate`'s `where` / `aggregations[i].filter`) reads the object's declared field map and rewrites a declared `datetime` column only. For an object the registry does not hold there is no declaration to read, and the seam now applies the whole-day rules to every column (a bare-day `$lte` becomes `$lt` the next day, a `$between` splits), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the declared type. It used to leave such an object to each driver's own copy. Visible on `SqlDriver`: a bare-day `$lte` on a non-`datetime` column of an unregistered object that holds ISO instant text now keeps the whole day; a `datetime` or `date` column answers as before. An object with a field map is unchanged. -- **`@objectstack/driver-memory` — `InMemoryDriver` compiles the comparison it is handed.** Its four copies of the whole-day rule are deleted (the `$lte` and `$between` arms of the filter translator, the `<=` and `between` arms of the AST-node translator). A read through the engine hands it a `where` the engine's seam has already lowered, so on that path a declared `datetime` column keeps the whole named day, and a declared `date` column answers as before. A row-level security `using` filter is not lowered by the engine's seam: the security middleware ANDs it into the query's `where` after that seam has run, and only the RLS compile seam lowers it, rewriting just the columns its field guard declares `datetime`. Two answers converge on what `SqlDriver` already returns (ADR-0053 D-D1 item 7's scope): on a registered object, a bare-day `$lte` / `$between` on a declared `text` column holding ISO instant text, or on a column the object does not declare, is now compared as written, where this driver used to widen it to the whole day. One path narrows outside those two: an RLS `using` policy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, is compiled with no field guard, so the RLS compile seam reads no column as `datetime` and the bound reaches this driver as written, where this driver used to widen it to the whole day; that holds until #20822 group 2 makes the RLS compile seam type-blind when it has no guard. A direct `find()` that passed no seam gets the comparison it wrote (item 5); lower the filter with `lowerFilterCondition` first to keep the whole-day reading. diff --git a/.changeset/20822-having-contains-membership.md b/.changeset/20822-having-contains-membership.md deleted file mode 100644 index 5ac58171ed9..00000000000 --- a/.changeset/20822-having-contains-membership.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/driver-mongodb': patch -'@objectstack/formula': patch -'@objectstack/spec': patch ---- - -fix(objectql,driver-mongodb,formula): the `having` and per-aggregation evaluator compiles the whole-day comparison it is handed, and `$contains` asks membership on a JSON-stored field in `MongoDBDriver` and in `matchesFilterCondition` (ADR-0053 D-D1 items 5 and 9; the `FILTER_OPERATORS` `$contains` contract, #20822) - -Clause-②: no - -- **`@objectstack/objectql`: the aggregate evaluator's own whole-day copy is deleted.** The walker behind `having` and `aggregations[i].filter` no longer widens a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, on a `datetime` column to the whole day, and no longer drops the bound on `9999-12-31`. Through `engine.aggregate` nothing changes: the engine's seam lowers both positions with the shared `lowerFilterCondition` (`@objectstack/spec/data`) before the walker runs, by the object's declared `datetime` fields for the per-aggregation `filter` and by the aggregated column's type for `having`. A caller that passes no seam gets the comparison it wrote: `applyInMemoryAggregation(rows, ast, tz, fields)` called directly now counts `{ at: { $lte: '2026-02-01' } }` against that day's midnight. To keep the seam's reading on a direct call, lower each `filter` first with `lowerFilterCondition(filter, { isDatetimeColumn })`. -- **`@objectstack/driver-mongodb`: `$contains` / `$notContains` ask membership on a declared JSON-stored field.** On a field `syncSchema` recorded as `multiple: true`, a multi-option type (`tags`, `multiselect`, `checkboxes`) or a JSON type, `translateFilter` (every verb, and the aggregation `$match`) now emits an array-only `$elemMatch` over the members the comparand names, with the candidate rule the SQL dialects bind (`jsonMembershipCandidates`, `@objectstack/core`): `'1'` names the string `'1'` or the number `1`, `'true'` the string or `true`. It used to emit a `$regex`, which MongoDB applies to each element, so `{ owners: { $contains: 'u1' } }` matched a stored `['u10']` and `{ tags: { $contains: 'red' } }` a stored `['redwood']`. `$notContains` is the exact complement, and still admits a row with no value. A scalar column, and a field whose declaration the driver does not hold (an object never synced, a standalone `translateFilter` call), keep the substring `$regex`. -- **`@objectstack/formula`: `matchesFilterCondition` asks membership of a JSON-stored column.** When the caller supplies `options.fields` and it names the column, the declaration decides: membership on a JSON-stored column, substring on any other. Otherwise the stored value decides: an array asks membership, anything else substring. A stored array used to fail `$contains` and pass `$notContains` whatever it held. - - **The RLS write check, which evaluates a policy with this function, moves with it.** Under a `check` such as `record.tags.contains('x')` on a multi-valued field, a write whose post-image holds `['x']` (a row the same policy's read shows) is now admitted; it was refused `PERMISSION_DENIED` / 403. `['xy']` stays refused, and the read hides it. - - A scalar written to a declared multi-valued field is judged as written, before the write door wraps it in a list. So `tags: 'xy'`, which the check used to admit while the read hides the stored `['xy']`, is now refused 403. And `tags: 'x'` is now refused 403 too, although the read shows the stored `['x']`. Send the list, `tags: ['x']`. -- **`@objectstack/spec`: docblock only, in the shipped `src/data/filter.zod.ts`.** The three pointers to the deleted `SqlDriver.calendarDayUpperBoundRewrite` / `calendarDayBetweenRewrite` now name the shared `lowerFilterCondition` at the seams, and the `FILTER_OPERATORS` `$contains` implementation-status list gains `driver-mongodb` and `formula`. No schema, type or export changes. -- No exported name changes. diff --git a/.changeset/20825-cli-picklist-extend.md b/.changeset/20825-cli-picklist-extend.md deleted file mode 100644 index 26849b5db08..00000000000 --- a/.changeset/20825-cli-picklist-extend.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -feat(cli)!: `objectstack validate` and `objectstack build` refuse a `picklistExtensions` entry whose `extend` names no picklist the stack declares (#20825) - -Clause-②: no (narrowing — `objectstack validate` / `objectstack build` newly refuse a `picklistExtensions[].extend` that names no picklist the stack declares; nothing is accepted that was refused before) - - - -**BREAKING** — an accept-set narrowing on two authoring commands, shipped as -`minor` under the launch-window convention. A stack with a `picklistExtensions` -entry whose `extend` names no picklist the stack declares — `extend: 'industy'` -beside a `picklists: [{ name: 'industry', … }]` — used to pass `objectstack -validate` and `objectstack build` (which wrote the artifact). Both now exit 1 and -name the extension and the list it names (`picklist-reference-unknown`, the rule a -field's dangling `picklist` already gets). -**One-line fix:** correct `extend` to the picklist the entry adds options to, declare -the list it names (`picklists: [{ name, label, options }]`, or a `*.picklist.ts` file -the stack imports), or remove the entry. - -**Which extensions are judged.** The ones the load path registers: the top-level -`picklistExtensions` of a one-package stack, or each `packages[]` entry's own. An -`extend` resolves against every picklist the stack declares, including one a sibling -package in the same artifact owns. - -**A list from a package outside the stack is reported, not refused.** When the -package declaring the extension lists a `manifest.dependencies` entry the stack does -not carry, the list may live there, and these commands cannot read it. That -extension is an `info` notice (`picklist-reference-unverified`) in `warnings` and on -the console, naming the extension, the list and the dependencies — never a failure, -not even under `--strict`. diff --git a/.changeset/20825-cli-picklist-kind.md b/.changeset/20825-cli-picklist-kind.md deleted file mode 100644 index e66bb45bbcc..00000000000 --- a/.changeset/20825-cli-picklist-kind.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@objectstack/cli': minor -'@objectstack/lint': patch ---- - -feat(cli)!: `objectstack validate` and `objectstack build` refuse a field whose `picklist` names no picklist the stack declares, and lint R8 counts `picklist` as an options source (#20825) - -Clause-②: no (narrowing — `objectstack validate` / `objectstack build` newly refuse a field `picklist` that names no picklist the stack declares; the R8 change removes a false-positive warning and widens no accept set of its own) - - - -**BREAKING** — an accept-set narrowing on two authoring commands, shipped as -`minor` under the launch-window convention. A stack with a select field whose -`picklist` names no picklist the stack declares — `picklist: 'industy'` beside a -`picklists: [{ name: 'industry', … }]` — used to pass `objectstack validate` and -`objectstack build` (which wrote the artifact). Both now exit 1 and name the field -and the list it names (`picklist-reference-unknown`). -**One-line fix:** correct `picklist` to a list the stack declares, or declare the -list it names (`picklists: [{ name, label, options }]`, or a `*.picklist.ts` file the -stack imports). - -**Which references are judged.** The ones the load path registers: the top-level -`objects` and `objectExtensions` of a one-package stack, or each `packages[]` -entry's own. A reference resolves against every picklist the stack declares, -including one a sibling package in the same artifact owns. - -**A list from a package outside the stack is reported, not refused.** When the -package declaring the field lists a `manifest.dependencies` entry the stack does not -carry, the list may live there, and these commands cannot read it. That reference is -an `info` notice (`picklist-reference-unverified`) in `warnings` and on the console, -naming the field, the list and the dependencies — never a failure, not even under -`--strict`. - -**Lint R8 (`field/select-missing-options`)** no longer reports a select, multiselect -or radio field that names a `picklist`: the picklist is its options source. The -warning it used to give pointed at `options`, which a field naming a `picklist` -cannot add — the field schema refuses the two together. A select with neither still -warns, and its fix now names both sources as alternatives. diff --git a/.changeset/20831-object-block-grouping-typed.md b/.changeset/20831-object-block-grouping-typed.md deleted file mode 100644 index 4e18daaa821..00000000000 --- a/.changeset/20831-object-block-grouping-typed.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -The `grouping` prop of the `object-grid` and `object-kanban` page blocks is now judged by the list view's own `GroupingConfigSchema`, so a padded grouping field name or a wrong-shaped value is refused on these blocks exactly as it is on `list-view` (#20831). - -Clause-②: yes (narrowing) - - - -**BREAKING**: `ComponentPropsMap['object-grid'].grouping` and `ComponentPropsMap['object-kanban'].grouping` were `z.unknown()`, so any value parsed. Both now take `GroupingConfigSchema` by reference: `{ fields: [{ field, order?, collapsed? }, ...] }`, at least one entry, each `field` the stored name with no leading or trailing whitespace. Both renderers already read exactly that shape: the grid groups by every `grouping.fields[i].field` and reads `order` / `collapsed`, and the kanban board takes `grouping.fields[0].field` as its swimlane field when no `swimlaneField` is authored. A value outside it validated green before and rendered one `(empty)` group on the grid, or one swimlane holding every card on the board, with no error. - -What is refused now, and the one-line fix for each: - -| Authored `grouping` | Refused as | Write instead | -| --- | --- | --- | -| `{ fields: [{ field: ' business_unit ' }] }` | `custom` at `grouping.fields.0.field`, naming the received value | `{ fields: [{ field: 'business_unit' }] }` | -| `'business_unit'` (a bare string) | `invalid_type` at `grouping` | `{ fields: [{ field: 'business_unit' }] }` | -| `42`, `true`, or any other non-object | `invalid_type` at `grouping` | delete the key; it never grouped anything | -| `{ fields: [] }` | `too_small` at `grouping.fields` | delete the key | -| `{ fields: [...], showCounts: true }` (an undeclared key) | `unrecognized_keys` at `grouping` | delete the undeclared key | - -On `object-kanban`, an authored `swimlaneField` still wins over `grouping`; when both are set, deleting `grouping` is the whole migration. A fully-spelled grouping parses byte-identically; a short entry `{ field }` parses clean and gains the list view's defaults (`order: 'asc'`, `collapsed: false`), which is how the grid already read it. - -Where it surfaces: the component-props gate reports a refused value as a `component-props-invalid` finding at the offending path on `os validate`, `os build` and `os lint` (advisory, as every finding of that gate is). A page component's `properties` are not parsed on the metadata save path, so a stored page keeps loading and rendering as it does today until its source is fixed; the ADR-0087 semantic entry `ui-object-block-grouping-config-typed` carries the same table for `os migrate meta`. - -The published JSON Schemas for `ObjectGridProps` and `ObjectKanbanProps` now describe the `grouping` shape; the non-padded field-name rule is a runtime refinement the JSON Schema does not express, recorded for both as `grouping.fields.element.field` in `dropped-refinements.baseline.json`, beside the same site on every list-view schema. diff --git a/.changeset/20833-activity-parent-read-gate.md b/.changeset/20833-activity-parent-read-gate.md deleted file mode 100644 index 5e015c2e829..00000000000 --- a/.changeset/20833-activity-parent-read-gate.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/plugin-audit': minor ---- - -fix(plugin-audit)!: an engine read of `sys_activity` returns only the rows whose parent record the caller can read, the same way an engine read of `sys_comment` is narrowed - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what an engine read of `sys_activity` returns to a caller that is not system context. It ships as `minor` under the repo's launch-window convention for narrowings. - -**What changes.** `AuditPlugin` now mounts a read gate on `sys_activity`, beside the one `sys_comment` already has. It is an engine middleware, so it narrows what passes through the engine: `find`, `findOne`, `count` and `aggregate`, which on the generic data doors are the list, its `total`, the by-id read and both query shapes. There a row is returned only when the caller can read the record it is about. That answer is the one the comment gate asks: the caller's own engine read of the parent record, so the parent object's sharing, RLS and object-level permissions decide. Parent reads are batched, one per parent object. - -**Rows that are left out**, failing closed exactly as the comment gate does for its threads: - -- a row about a record the caller cannot read; -- a row about a record that no longer exists; -- a row that names no record, or names an object the engine does not know; -- a row that names `sys_activity` itself. - -**Unchanged.** A caller who can read every record (an admin) keeps every row about a record that exists. System-context reads, including the audit writer's own, are not narrowed. The object, its fields and what the CRUD mirror writes are unchanged, and nothing stored is rewritten. diff --git a/.changeset/20841-blueprint-nav-label-inherits.md b/.changeset/20841-blueprint-nav-label-inherits.md deleted file mode 100644 index c14da600ddc..00000000000 --- a/.changeset/20841-blueprint-nav-label-inherits.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `BlueprintNavItemSchema.label` says an absent label is inherited at render time, not defaulted by the expander - -Clause-②: no - -The `label` describe on a blueprint nav item read "defaults to the target label/name". An -expander or an AI author that follows "defaults" copies the target's label into the entry, and -the entry then stops following a rename of that target. The runtime nav entry's `label` has -meant something else since it became optional: absent, the entry inherits the CURRENT label of -what it opens at render time; present, it renders verbatim. The blueprint describe now says -exactly that, and tells the author not to copy the target's label in as a default. - -Describe text only: the key stays `z.string().optional()`, so the schema accepts and refuses -the same blueprints. The reference page `content/docs/references/ai/solution-blueprint.mdx` -is regenerated from it. diff --git a/.changeset/20844-resolved-token-year-range.md b/.changeset/20844-resolved-token-year-range.md deleted file mode 100644 index 53107d3356a..00000000000 --- a/.changeset/20844-resolved-token-year-range.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/objectql': minor ---- - -fix(core,objectql)!: a relative-date placeholder that resolves outside its field's years is refused `INVALID_FILTER` / 400, naming the placeholder and the year it resolved to, instead of reaching the driver and answering the wrong rows - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the engine answers for a filter carrying a relative-date placeholder. A date macro is resolved after the temporal-comparand door, which steps around a placeholder, so the year range that door asks of a literal never saw the value one resolved to. It does now, through the same function, core's `isOutsideTemporalYearRange`, by the column's kind: a `date` takes the years 0001 to 9999 and a `datetime` 1000 to 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What is refused now.** A date macro whose resolved value falls outside its column's years, on a declared `date` or `datetime` field (or, on a `time` field, one that resolves to an instant whose UTC year has no four-digit spelling), at `where` (on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`), at a per-aggregation `filter`, at `having` (by the aggregated column's kind), and through `judgeFilter`. On REST that is `POST /api/v1/data/:object/query` and every other door that reads through the engine. Measured before this on InMemoryDriver and SqlDriver on SQLite, over a `datetime` field with a row in 2026 and a row in 1500: - -- `$gt {8000_years_from_now}` answered both rows, and the right answer was none; -- `$lt {2027_years_ago}` answered the 1500 row, because the resolver spelled year -1 as `-1-10-01` and that text was read as a day in 2001, and the right answer was none; -- `$lt {1977_years_ago}` resolved to year 49, below the `datetime` floor of 1000, which now applies to a resolved placeholder as it does to a literal; -- on a `time` field, `$gt {8000_years_from_now}` answered every row: the `time` rule keeps no time of day from an instant whose UTC year has no four-digit spelling, so it compared as text. Such a placeholder is refused now in the words a literal of that instant gets. - -**What an author sees.** The refusal names the field, the placeholder as written, its position, the value it resolved to and that value's year, in the temporal-comparand door's words for the year class: `filter on 'opened_at' compares a declared datetime field against "{8000_years_from_now}" at where.opened_at.$gt, a relative-date placeholder that resolved to "+010026-10-01" (the year 10026), an instant whose UTC year falls outside the years 1000 to 9999 …`. It ends by asking for a placeholder whose offset lands inside those years. - -**The resolver's spelling** (`@objectstack/core`). A date macro that lands on a day outside 0001..9999 now resolves to that day in the expanded-year form of ECMAScript's date time string format, `+010026-10-01` or `-000001-10-01` (year 0 is `0000-10-01`). It used to take the storage rule's unpadded spelling, `10026-10-01` or `-1-10-01`, which `Date.parse` reads through the host's legacy parser in the host's zone, so a day in year -1 read as one in 2001 and could not be judged. Every consumer of `resolveFilterToken` and `resolveFilterTokens` sees the new spelling for such a day only. A day inside 0001..9999 and a sub-day placeholder's instant are spelled as before. - -**Unchanged.** A placeholder that resolves inside its column's years answers as before; a `date` keeps the years 0001 to 0999, which a `datetime` refuses, and a `time` field reads the time of day of any instant with a four-digit year, year 0 included. A placeholder on a column with no temporal kind (text, number) and a context placeholder such as `{current_user_id}` are not judged by this range. Every literal comparand answers as before. diff --git a/.changeset/20846-temporal-range-message.md b/.changeset/20846-temporal-range-message.md deleted file mode 100644 index 45cb9d7abc6..00000000000 --- a/.changeset/20846-temporal-range-message.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/spec': patch -'@objectstack/objectql': patch -'@objectstack/rest': patch ---- - -fix(objectql,rest): a `date` or `datetime` value refused for its year says so — "must be a date in the years 0001 to 9999" / "must be a datetime whose UTC year falls in the years 1000 to 9999" — instead of "must be a valid date (ISO-8601)", which was false for a value such as `0500-07-15T10:00:00Z` (#20846) - -Clause-②: yes (widening) — one new export on `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`. No value's verdict moves and no wire key moves: the field code stays `invalid_date` and its `constraint` stays `{ type }`. - -`POST` / `PATCH /api/v1/data/:object` and each row of `POST /api/v1/data/:object/import` -refuse a `date` outside the years 0001 to 9999 and a `datetime` whose UTC year falls -outside 1000 to 9999. When the value itself is readable — an ISO 8601 string such as -`0500-07-15T10:00:00Z` or `+010000-01-01`, or a `Date` — the refusal's message now -names the kind's years. An author who read "not valid ISO" rewrote the spelling, and no -spelling of that year is admitted. - -- `@objectstack/spec`: the validation message catalog gains `invalid_date_range` and - `invalid_datetime_range` in `en`, `zh-CN`, `ja-JP` and `es-ES`. They are two more - sentences of the `invalid_date` code, never a wire value. The years are the template - parameters `{{firstYear}}` / `{{lastYear}}`. A deployment that overrides a message - under `validation.field.invalid_date` or `validation.field.invalid_datetime` does not - cover these values. To override their text, define - `validation.field.invalid_date_range` / `validation.field.invalid_datetime_range`. -- `@objectstack/core`: `SUPPORTED_TEMPORAL_YEARS` (`{ date: { first: 1, last: 9999 }, - datetime: { first: 1000, last: 9999 } }`, frozen) is the range - `isOutsideTemporalYearRange` judges by. It is exported so a refusal names the range - from the source the doors use, never a copy of its numbers. -- `@objectstack/objectql` and `@objectstack/rest`: the record validator and the import's - cell reader choose the range sentence for such a value. An import cell with more than - four year digits (`+010000-01-01`) is refused by the import's reader. It used to read - "is not a valid date" and now gets the same range sentence as the write door. - -**What is not affected.** Which values are refused is unchanged, and so is the refusal's -code (`invalid_date`) and `constraint`. A value that is not readable keeps its sentence: -"must be a valid date (ISO-8601)" at the write door, `"…" is not a valid date` at the import. -So does a number, which is never a written `date` or `datetime`. diff --git a/.changeset/20859-cube-filter-array-lowered.md b/.changeset/20859-cube-filter-array-lowered.md deleted file mode 100644 index e6d39287f0c..00000000000 --- a/.changeset/20859-cube-filter-array-lowered.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@objectstack/driver-memory': minor ---- - -`MemoryAnalyticsService` lowers the `FilterArray` spelling of `where` instead of dropping it - -Clause-②: no (narrowing) - - - -An array `where` such as `[['stage', '=', 'won']]` used to skip every filter step of the -analytics (cube) face: `query()` aggregated every row and `generateSql()` echoed no `WHERE`, -while the object spelling `{ stage: 'won' }` answered its rows. The array is now lowered by -`@objectstack/spec`'s `isFilterAST` / `parseFilterAST` — the lowering the analytics `where` door -and the engine already apply — so both spellings answer the same rows and echo the same `WHERE` -on both exits. `[]` still means no filter. - -**BREAKING**: `query()` and `generateSql()` now refuse, with `INVALID_FILTER` / 400, a `where` -array that is not a filter — one `isFilterAST` rejects. Each such array used to answer EVERY row -and echo no `WHERE`; the analytics `where` door and the query engine refuse the same shapes. -Measured on a fixture where `d` is `'v1'`, `'v2'`, `null` and absent, each of these answered all -four rows: - -- an infix join, `[['d', '=', 'v1'], 'or', ['d', '=', 'v2']]`; -- an operator outside the filter-array vocabulary, `[['d', 'sounds_like', 'v1']]`; -- a list of scalars, `[1, 2, 3]`; -- a cube-style entry list, `[{ member: 'd', operator: 'equals', values: ['v1'] }]`. - -An array that does lower but carries a comparand or operator the face refuses in its object -spelling (`[['d', 'in', 'v1']]`, `[['d', 'starts_with', 'v']]`) is now refused as that object -spelling is; it too used to answer every row. - -The fix is to write the filter you mean: the prefix form `['or', condA, condB]` for an infix -join, an operator from the filter-array vocabulary, or the `FilterCondition` object. diff --git a/.changeset/20860-page-size-entry-advisory.md b/.changeset/20860-page-size-entry-advisory.md deleted file mode 100644 index 6e5a779be1f..00000000000 --- a/.changeset/20860-page-size-entry-advisory.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the `ui-object-grid-page-size-positive-integer-refused` migration entry states what the platform does with a stored `object-grid` page size of `0` — the page saves and loads, and the finding is advisory on the CLI - -Clause-②: no - -**`ui-object-grid-page-size-positive-integer-refused` (protocol 18).** The entry's acceptance -criterion said that a stored page whose `object-grid` node carries `pageSize: 0` "is refused on -its next authoring-path save with a per-key issue at `pagination.pageSize`". Nothing on the -metadata save path judges a page component's `properties`. `PageComponentSchema.properties` is an -open record, and the one judge of the `ComponentPropsMap` row is the component-props gate, an -advisory rule that runs on the CLI only. Measured through `saveMetaItem`, the call behind -`PUT /api/v1/meta/page`: such a page saves (`success: true`, and the result carries no `advisories` member), is stored as an -active row and reads back with `pageSize: 0` intact. On the same page, `os validate`, `os build` -and `os lint` each report one advisory `component-props-invalid` finding at -`properties.pagination.pageSize` and no error. The criterion now says exactly that. It also names -the `pageSizeOptions` entry and the flat `pageSize` shorthand, which are reported the same way at -their own paths. - -Text only: no entry id, conversion or matching logic changes, and `os migrate meta` rewrites -exactly what it rewrote before. The generated migration registry carries the corrected text. The -sibling entry `object-grid-default-filters-rule-array` already stated the advisory-only outcome -and is unchanged. diff --git a/.changeset/20861-host-sign-in-handoff.md b/.changeset/20861-host-sign-in-handoff.md deleted file mode 100644 index 671f40cbd3d..00000000000 --- a/.changeset/20861-host-sign-in-handoff.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/plugin-auth': minor ---- - -feat(plugin-auth): a host declares its own sign-in handoff route with `hostSignInHandoff`, and the `no_sign_in_account_at_boot` boot report stops calling that deployment a dead end (#20861) - -Clause-②: yes (widening) - -`AuthPluginOptions` gains one option, `hostSignInHandoff?: boolean` (default -`false`). The HOST that constructs the plugin sets it when it signs people in -through a handoff route of its own: a route that is not a login-page provider -and that creates the session without writing a `sys_account` row. A hosted -kernel whose owner signs in through the control plane is the case it is for. -That owner can still sign in when the login page shows no platform sign-in -button: - -```ts -new AuthPlugin({ /* … */ hostSignInHandoff: true }); -``` - -With it declared, human `sys_user` rows and zero `sys_account` rows are that -deployment's normal state. The boot report then logs the shape at `debug` and -names `hostSignInHandoff` as the reason. It no longer logs an `error` saying -nobody can sign in. The option's only reader is that boot report. - -- It is a declaration, not a detection. The option is the only way to set it: - there is no environment variable or setting. Nothing infers it from an - environment's name, from a control plane's platform-SSO flag, or from missing - rows. -- The login page is not changed. `getPublicConfig()` returns the same value with - or without the option, and no provider is registered. -- Set it only where the host really serves such a route. On a deployment with - no such route, the option turns the error for a deployment nobody can sign in - to into a quiet `debug` line. -- A deployment that does not declare it gets the same report as before. That - includes every self-hosted deployment with no delegated sign-in path. diff --git a/.changeset/20862-automation-doors-persist.md b/.changeset/20862-automation-doors-persist.md deleted file mode 100644 index 65325839616..00000000000 --- a/.changeset/20862-automation-doors-persist.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/runtime': minor ---- - -fix(runtime)!: the /automation create and update doors save the flow as a tenant row, so what they answer 200 for survives a restart, and the removal door deletes that row too (#20862) - -Clause-②: no (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. `POST /api/v1/automation` and `PUT /api/v1/automation/:name` now refuse a definition the metadata store refuses, which they used to register and answer `200` for. `DELETE /api/v1/automation/:name` now relays a store's refusal to delete the flow's row. - -**What changed.** The create and update doors registered a flow in the automation engine and wrote no metadata row. The next boot binds flows from the stored metadata, so a flow created through `POST /automation` was gone after a restart, and an update through `PUT /automation/:name` to a flow stored through `/meta` lost to the stored definition. Both doors now save the definition through the metadata protocol's own `saveMetaItem`: the save `PUT /api/v1/meta/flow/:name` uses, and the one the clone door (`POST /automation/:name/clone`) already used. The row is env-wide and live (`active`), so the flow reads back on `/meta` and survives a cold boot. The three doors share one path. - -- **Engine first, store second.** The engine's registration is still the first check. Its refusal is answered as before (`400 VALIDATION_FAILED`), and nothing is saved. -- **A save the store refuses is relayed with its own code and status, and leaves no registration behind.** A create is withdrawn from the engine. An update puts back the definition the engine held, so a refused update does not take the flow down. -- **`DELETE /automation/:name` deletes the tenant row too**, through `deleteMetaItem`, so a flow created through the door does not come back at the next boot. The engine's own removal refusal (`DELETE_RESTRICTED` / `409`) is still raised before the store is touched. A name with no stored row is removed as before. A delete the store refuses puts the definition back and relays the refusal. -- **Unchanged:** the locked-base refusal on a packaged flow's name (`403 NOT_OVERRIDABLE`) and the refusal of a definition claiming a package's provenance (`422 INVALID_METADATA`) still answer first. A composition with no metadata protocol keeps the engine-only registration and removal it always had. - -**Newly refused, because the metadata store refuses them** (measured on the showcase): - -- A flow name with a leading underscore. `FlowSchema` admits it and the metadata item-name grammar does not, so the door answers `400 INVALID_REQUEST`. Rename the flow to a name that starts with a letter. -- A definition a gating runtime publish rule refuses, such as a default edge that also carries a condition (`flow-default-edge-with-condition`). The door answers `422 INVALID_METADATA` with the rule's finding. Fix the definition as the finding says. - -Such a flow could never be stored, so before this change it ran only until the next restart. diff --git a/.changeset/20863-orphan-package-binding-refused.md b/.changeset/20863-orphan-package-binding-refused.md deleted file mode 100644 index ef74b91497f..00000000000 --- a/.changeset/20863-orphan-package-binding-refused.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol)!: a flow saved through the metadata door naming, as its base, a package this deployment has not installed is refused, instead of being stored bound to a package that does not exist (#20863) - -Clause-②: no (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. `PUT /api/v1/meta/flow/:name` answered `200` and stored the flow live when the save named, as the package the flow belongs to, a package id this deployment has never installed. It now answers `422 WRITABLE_PACKAGE_REQUIRED`, and nothing is written, served or registered. - -**What changed.** The one authoring rule every flow write door asks (`tenantAuthoredWriteRefusal`) now also judges the base a save names. After the locked-base refusal and before the provenance check, a named base must be a package the registry holds as installed: a code package, an installed package, or a tenant's own writable base created through the package door. That is the same registry read the metadata write path already resolves a base against, so no second list of packages is kept. The refusal does not depend on what the definition carries: a save with no provenance of its own and a save whose provenance names that same missing package were both stored before, and both are refused now. It applies on a single-kernel host and on an environment kernel alike. - -- The code is `WRITABLE_PACKAGE_REQUIRED` / `422`, the one ADR-0070 D1 already uses for a runtime create whose base is missing or read-only. The refusal names the package id the save sent. -- **Unchanged:** a flow saved without naming a package; a flow saved into an installed package; the locked-base refusal of a shipped flow, which still answers first; every other metadata type, whose saves keep their old handling; the `/automation` create, update and clone doors, which name no package; and the server-stated rewrites of stored rows (the stored-metadata migration and package duplication), which the rule does not judge. - -**What to send instead.** Save the flow into a package this deployment has installed (the package list shows them, and a base that does not exist yet is created first through the package door), or save the flow without naming a package. diff --git a/.changeset/20864-precedence-loader-set.md b/.changeset/20864-precedence-loader-set.md deleted file mode 100644 index 5398ed011db..00000000000 --- a/.changeset/20864-precedence-loader-set.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -fix(automation): boot-time flow precedence takes which same-named flow is the packaged one from the package loader's set, not from the flow definitions' own provenance (#20864) - -Clause-②: yes (widening) - -When several flow definitions share one name at startup, the automation plugin arms one of them and shadows the rest. Which contender counts as the packaged one is now the answer of the set of flows a managed package's loader registered. That is the same answer the ADR-0126 §7.3 subflow guards, the arming gate and the activation switch read since #20761. The package provenance a flow definition carries is kept for display only. - -- `resolveFlowPrecedence(items, logger?, packagedFlowOwner?)` and `describeFlowContender(item, packagedFlowOwner?)` take the reader as a new optional last argument, typed `PackagedFlowSource` (the reader `AutomationEngine.setPackagedFlowSource` takes). `AutomationServicePlugin` passes the engine's own `packagedFlowOwner` for you. -- A definition that claims a package's provenance for a name no package loaded ranks as a flow of the deployment. The shadowing record (`getShadowedFlows()`, and the startup warnings) no longer names that package as its source. -- With no reader, no contender is packaged. That is the engine's own answer when no reader is attached. -- Two contenders that both rank as the deployment's keep the order they were listed in. The package id orders packaged contenders only, as before. -- A startup whose registry the package loader and the stored-flow hydration filled arms the same flows as before: those entries already agree with the loader's set. - -**If you call `resolveFlowPrecedence` or `describeFlowContender` yourself:** pass the loader's-set reader as the last argument, for example `(name) => engine.packagedFlowOwner(name)`. Without it no contender ranks as packaged. diff --git a/.changeset/20867-analytics-year-keys.md b/.changeset/20867-analytics-year-keys.md deleted file mode 100644 index 9ca1736e81e..00000000000 --- a/.changeset/20867-analytics-year-keys.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -A dataset's date dimension reads the bucket key `@objectstack/core`'s `bucketDateKey` writes, with its year in four digits, and a draft preview keys a row the way the same dataset does once published. - -- **Dimension labels (`queryDataset`).** A date dimension's grouped key is labelled as written. The year key `0050` was labelled `1970` (read as epoch seconds, because the year check admitted only 1000..9999), and a month or day key lost its padding (`0050-06` became `50-06`, `0050-06-15` became `50-06-15`). A raw date value is relabelled with the year in four digits too. A year from 1000 to 9999 is labelled as before. -- **Draft preview (`queryDataset` with `previewDrafts`).** Drafted seed rows are keyed by `bucketDateKey` itself, the key the published path's grouping writes. For 0050-06-15 the preview answered `50`, `50-Q2`, `50-06` and `50-06-15`; it now answers `0050`, `0050-Q2`, `0050-06` and `0050-06-15`. A `week` bucket is now the ISO week label (`2026-W25`), no longer the Monday's date (`2026-06-15`), so a weekly `compareTo` in the preview merges each comparison row onto its week, as the published path does. An epoch-milliseconds value is bucketed by its instant (it was the empty bucket), and a `Date` in 0001..0999 by its own year (a `Date` in 0050 keyed `1950`). diff --git a/.changeset/20869-cross-class-refusal-remedy-first.md b/.changeset/20869-cross-class-refusal-remedy-first.md deleted file mode 100644 index e7aafb60640..00000000000 --- a/.changeset/20869-cross-class-refusal-remedy-first.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/formula': patch -'@objectstack/plugin-security': patch ---- - -fix(formula,plugin-security): the refusal of a field-to-field comparison across comparison classes now leads with its remedy, so the remedy reaches REST callers (#20869) - -Clause-②: no - -A row-level policy that compares two fields of no shared comparison class (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object) is refused with `INVALID_FILTER` / 400. The REST door keeps a 4xx message under 500 characters by cutting it to its first 499 characters plus an ellipsis. Both messages for this refusal put the remedy last, so the remedy was always cut off, and a caller read the diagnosis but never the fix: - -- The record matcher's message (`@objectstack/formula`, raised by the RLS write check on an insert or update through `/data`) was 972 characters, with the remedy starting at character 825. -- The explain engine's message (`@objectstack/plugin-security`, answered by `GET` / `POST /api/v1/security/explain`) put the remedy after the policy names and the diagnostic. Those have no length limit, so the message was 601 characters with a short policy name and longer with longer names. - -Both messages now start with the remedy. It is the same sentence as before and has only moved: - -- The record matcher's message is 494 characters and reaches the wire whole. In order it says: the remedy; that the two columns share no class, and which classes exist; why the comparison is refused; and why the columns are not named. It still names no column, operator or policy; the server log names them. -- The explain engine's message starts with the remedy, then names the policy and both columns, then gives the reason. Whatever the names' length, the remedy sits in the first 125 characters. With long names the REST door may cut the reason at the end. - -Unchanged: the error code (`INVALID_FILTER`), the status (400), which comparisons are refused, the refusal a find answers with (driver-sql's read refusal, 383 characters, which already reached the wire whole), and every other refusal. diff --git a/.changeset/20870-page-requires-load-and-promote.md b/.changeset/20870-page-requires-load-and-promote.md deleted file mode 100644 index 72f9983483e..00000000000 --- a/.changeset/20870-page-requires-load-and-promote.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -A stored page whose `requires` names a plugin the deployment's console does not load is reported when the page loads, and a draft → active promotion re-stamps an html page's `requires` with the save door's own computation (ADR-0080 §5: `requires` is validated at save and load, and derived from the source). - -Clause-②: no - -**At load.** The boot hydration of stored metadata (`loadMetaFromDb`) prints one `warn` line for each stored page whose `requires` lists a namespace no component in the deployment's SDUI component manifest carries, naming the page and every such namespace under the marker `[page_requires_plugin_absent]`. It is a report, never a refusal: the page has already loaded when the line is printed, and it is served. The manifest is read through the same `SDUI_MANIFEST_SERVICE` key the save door reads; `os serve` registers it before any plugin initialises, so it is there when stored pages load. A host that registers no manifest judges no page at load, exactly as it judges none at save. - -**At promotion.** `publishMetaItem` and `publishPackageDrafts` now store the promoted body with the `requires` that an active save of the same body would store, computed against the manifest registered at the moment of the publish. Before, the draft's `requires` was carried into the active row as it was. A draft saved before the host had a manifest reached `active` with no `requires`, and an agreeing list kept the draft's own spelling (its order and any repeats). Nothing is newly refused: wherever the runtime authoring gate runs, it already refused a draft whose source does not compile, or whose `requires` disagrees with its source, and it still does. A host with no manifest promotes the draft as written. - -`SysMetadataRepository.promoteDraft` takes an optional `deriveActiveBody(draftBody)` that derives the active row's body from the draft row it promotes. When it is omitted, the draft body is promoted unchanged, as before. diff --git a/.changeset/20873-aggregation-filter-array-membership.md b/.changeset/20873-aggregation-filter-array-membership.md deleted file mode 100644 index e619c6f5e3a..00000000000 --- a/.changeset/20873-aggregation-filter-array-membership.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a per-aggregation `filter` with `$contains` / `$notContains` on a multi-valued field counts the rows the same `where` finds (#20873) - -Clause-②: no - -`engine.aggregate({ aggregations: [{ …, filter }] })` — and so `POST /api/v1/data/:object/query` -with a per-aggregation `filter` — evaluates that filter in the engine, not in the driver. Its -`$contains` arm failed every value that was not a string, so on a `multiple: true` lookup, -`multiselect`, `checkboxes` or `tags` field a stored array never matched: -`{ owners: { $contains: 'u1' } }` counted 0 on every driver where the same condition as a `where` -found 2 rows, and `$notContains` counted every row, the rows holding the member included. - -On a declared JSON-stored field (a multi-valued field, or a structured-JSON type) both operators -now ask MEMBERSHIP, the reading `FILTER_OPERATORS`' `$contains` docblock declares and `where` gives -on every SQL dialect: `'u1'` is a member of `['u1', 'u2']` and not of `['u10']`, and a member stored -as a number or boolean is named by its text (`'1'` finds `[1, 2]`). `$notContains` is the exact -complement, and a row with no value still satisfies it. A scalar text field keeps the substring -test, unchanged, and so does `having`. - -No query that was refused now answers, and none that answered is refused: only the count of a -per-aggregation `filter` on a multi-valued field moves, to the `where` count. diff --git a/.changeset/20874-memory-contains-membership.md b/.changeset/20874-memory-contains-membership.md deleted file mode 100644 index f4401ab9c9a..00000000000 --- a/.changeset/20874-memory-contains-membership.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@objectstack/driver-memory": minor ---- - -fix(driver-memory): `$contains` / `$notContains` on a multi-valued or JSON-stored field answer by membership, as the SQL drivers do - -Clause-②: yes (widening) — one new public method on the exported `InMemoryDriver` class, `filterContainsTest`; its return type `MemoryContainsTest` is not re-exported from the package entry. No accepted filter key or operator is added: `$contains` and `$notContains` keep their declared shape. - -On a field whose declaration makes it JSON-stored (`multiple: true` on a `lookup`, `user`, `select`, `radio`, `file` or `image` field, a `multiselect`, `checkboxes` or `tags` field, or a structured type such as `json`), the in-memory driver now answers `{ field: { $contains: v } }` by whole-element membership: some element of the stored array equals `v`. It used to match each element by substring, so `u1` matched a row storing `['u10']` and `'red'` matched a row storing `['redwood']`. A number member answered nothing: `{ nums: { $contains: '1' } }` missed `[1, 2]`. `$notContains` is the exact complement, and a row with no value still satisfies it. A scalar text column keeps the case-exact substring test. - -`driver-sql` gives the same answer on SQLite, PostgreSQL and MySQL; the two drivers were measured over the same fixture. The answer holds on every face of this driver: - -- `find()` and `count()`, in both filter spellings; -- the nested-relation filter on a multi-valued relation, which the engine lowers to one `$contains` per related id; -- `MemoryAnalyticsService`'s query, and its SQL echo, which now renders SQLite's `json_each` membership construct for such a column. - -The comparand is still a string. A number or boolean member is named by its text: `'1'` matches the stored number `1` (and `'1.50'` the number `1.5`), `'true'` matches the boolean `true`, and `'null'` matches a `null` member. A field the driver holds no declaration for, such as a field on an object never passed through `syncSchema`, keeps the substring reading. - -New: `InMemoryDriver.filterContainsTest(object, field, value)` returns the one test every face above lowers `$contains` to. It is a narrow seam for the analytics face, beside `filterSubstringPattern` and `filterComparandStorageForm`. The added public method is why this entry is `minor`. - -**If your tests relied on the old answer:** on the in-memory driver, a filter that matched an id by prefix or a tag by substring now returns only the member rows. That is what SQL already returned in production. Write `$contains` with the whole member value. diff --git a/.changeset/20887-analytics-nested-relation-engine-answer.md b/.changeset/20887-analytics-nested-relation-engine-answer.md deleted file mode 100644 index ede9946ef36..00000000000 --- a/.changeset/20887-analytics-nested-relation-engine-answer.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: the nested-relation filter `{ relation: { field: value } }` gets the engine's answer on every analytics face — the related object read as the caller, capped - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what the analytics query doors answer for the nested-relation filter form — a plain object with no `$` key beneath a field, `{ owner: { region: 'NA' } }` — on the native-SQL path, and widens it everywhere else. It holds on `POST /api/v1/analytics/query`, on `POST /api/v1/analytics/dataset/query`, and on their dry run `POST /api/v1/analytics/sql`, on every SQL driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What an author sees now.** The same answer `find()` gives for the same filter. The data engine reads the related object with the condition as the caller — that object's row scope and field permissions apply — and matches the relation against the ids it returns: `$in` on a single-valued relation, any member on a multi-valued one. It is the one rule, in the engine; the analytics layer holds no copy of it. - -- A condition on a field of the related object the caller cannot read is refused with `403 PERMISSION_DENIED`, naming the field — never answered. -- A condition matching more than 1,000 related records is refused with `400 INVALID_FILTER`, naming the two-step route — never run over a cut-off list. -- At a measure's own `filter` the form is refused with `400 INVALID_FILTER`, as the engine refuses it at an aggregation's own `filter`: put the condition in the query's `where`. -- `POST /api/v1/analytics/sql` refuses a `where` carrying the form with `400 INVALID_FILTER`: no statement it could print reproduces a read of the related object as the caller. The query itself is answered by `POST /api/v1/analytics/query`. - -**Why.** Measured on the base before the field-level gate (#20917) and the relationship-path admission (#20933) landed, over one fixture with the real security layer (a related field the caller may not read, a related row scope, 1,001 matching related records). The native-SQL strategy flattened the form to a dotted member and joined the related table: through a dataset that `include`d the relationship it answered rows for a condition on a field the caller cannot read, answered a match past the engine's cap, and counted a measure filter carrying the form; without the declared join it named a table that does not exist (500), and a multi-valued relation was refused. The engine-aggregate strategy refused the form as a cross-object filter (400). The engine serves the form since the nested-relation filter landed in `where`. - -**How.** The native-SQL strategy declines a query in which the form appears in the `where`, the dataset's own `filter` or a requested measure's `filter`, so the query runs on the engine-aggregate path, which hands the form to the engine as written. - -**A read scope carrying the form.** Unchanged in outcome: where a read scope is compiled to SQL (`compileScopedFilterToSql`, on the native-SQL path and in both SQL echoes) it is still refused fail-closed with `500 READ_SCOPE_COMPILE_FAILED`, the policy withheld — that compile holds no data engine to read the related object with. Its words now name the route that serves the form. On the engine-aggregate path the scope reaches the engine as written, and the engine serves it as the caller, as before. - -**Who is affected.** A dashboard, dataset or caller that wrote the nested form in an analytics filter on a SQL driver and read the joined answer: a condition on a related field the caller may not read, a match past 1,000 related records, a measure filter carrying the form, or a query that needs the native-SQL strategy for another part (a cross-object measure, a multi-hop dimension), which the engine-aggregate path refuses in its own words. - -**Unchanged.** The dotted cube member (`{ 'owner.region': 'NA' }`), a traversal through the cube's declared join; an empty object beneath a field (`{ owner: {} }`), still refused as a field constraint with no operator; every filter without the form. diff --git a/.changeset/20889-analytics-native-measure-number.md b/.changeset/20889-analytics-native-measure-number.md deleted file mode 100644 index 0260ecfa236..00000000000 --- a/.changeset/20889-analytics-native-measure-number.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/driver-sql': patch -'@objectstack/service-analytics': patch ---- - -fix: the analytics native-SQL path answers a measure its response declares `number` as a number on every dialect, presented by the one rule `driver-sql`'s `aggregate()` applies, which `@objectstack/core` now exports as `AGGREGATE_ANSWER_KIND` and `presentAsNumber` (#20889) - -Clause-②: yes (widening) - -**New exports.** `@objectstack/core` exports two names, moved here unchanged -from `@objectstack/driver-sql`, which now imports them instead of keeping them -private: - -- `AGGREGATE_ANSWER_KIND`: what each declared aggregate function answers. - `count`, `count_distinct`, `sum` and `avg` answer `'number'`; `min` and `max` - answer `'column'`, a value of the aggregated column. -- `presentAsNumber(value)`: the `'number'` presentation. A string `Number()` - reads as a number becomes that number. Any other value is returned as given: - a number, `null`, a boolean, empty or blank text, or text that reads as NaN. - -**What changed.** On PostgreSQL, `POST /api/v1/analytics/query` and -`POST /api/v1/analytics/dataset/query` answered through `NativeSQLStrategy` -returned count, count_distinct, sum, avg, and min / max over a numeric column -as strings, such as `count: "2"` and -`sum: "500.000000000000000000000000000000"`, while `fields[]` declared -`number`. A dataset's `row_count` did the same, and a measure-scoped count -mixed `"1"` with the number `0` in one column. SQLite answered numbers. The -strategy now presents each measure column by its declared aggregate function, -through the same table and presenter as `SqlDriver.aggregate()`. `min` / `max` -are presented only when their column is declared numeric, so `max` over a text -column, every dimension, and expression measures keep the value the database -returned. - -**Precision.** The answer is one JS number, the policy `driver-sql`'s -`aggregate()` already applies. A total that needs more digits than a double -holds, such as `9007199254740993`, answers the nearest double -(`9007199254740992`), which is also what SQLite and the engine path answer. - -**What did not move.** `@objectstack/driver-sql`'s behaviour is unchanged: its -`aggregate()` reads the same table, and its read presenter calls the same -function. The answers on SQLite are byte-identical. The arithmetic of the -analytics native statement did not change either. On PostgreSQL its `sum` and -`avg` still add exact decimals, so `0.1 + 0.2` answers `0.3` where the engine -path answers `0.30000000000000004`. diff --git a/.changeset/20890-dataset-dimension-json-stored-refused.md b/.changeset/20890-dataset-dimension-json-stored-refused.md deleted file mode 100644 index 46095ee43ba..00000000000 --- a/.changeset/20890-dataset-dimension-json-stored-refused.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -fix(lint)!: `os validate`, `os build` and `os lint` refuse a dataset dimension over a JSON-stored field, the group key the analytics door already refuses at query time - -Clause-②: yes (narrowing) - - - -**BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail, and so can a runtime dataset save (Studio, REST `/meta`, MCP), which runs the same rule. A dataset dimension is a group key, and the analytics door refuses a query that groups by a JSON-stored column with `400 INVALID_FIELD` before any SQL is built, so such a dimension could be declared but never served. The new rule `dimension-json-stored-field-refused` (gating, `error`) refuses it where the author writes it. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is removed. The package entry exports the new id as `DIMENSION_JSON_STORED_FIELD_REFUSED`, beside `MEASURE_AGGREGATE_FIELD_TYPE_REFUSED`, and the `rule` member of `DatasetMeasureAggregateFinding` gains it. - -**What is refused.** A dimension whose `field` resolves, on the dataset's object or across its join chain, to a field declared with a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) or a multi-value declaration (`multiselect`, `checkboxes`, `tags`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`). The two classes are `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES` and `isMultiValueField`, the predicates the analytics door reads. - -**What an author sees now.** The finding names the dataset, the dimension, the field, the object that declares it and its declaration, and says the analytics door refuses every query that groups by it. It names the route: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own; for a multi-value field, filter by one member with `$contains` in a record query, one query per member. It is located at `datasets[N].dimensions[M].field`, name-keyed on the runtime wire. - -**Unchanged.** A dimension over any other field, a single-value `select` or `lookup` included; a dimension whose field does not resolve (`dataset-field-unknown` reports that) or declares no type; a dataset over an object this stack does not define; measures, filters and every other position. A cube dimension (`analyticsCubes`) is not judged: no authoring rule reads cubes. diff --git a/.changeset/20890-dataset-distinct-multiple-refused.md b/.changeset/20890-dataset-distinct-multiple-refused.md deleted file mode 100644 index fb261ac5371..00000000000 --- a/.changeset/20890-dataset-distinct-multiple-refused.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -fix(lint)!: a dataset `count_distinct` measure over a field declared `multiple: true` is refused by `measure-aggregate-field-type-refused`, as the compile leg and the engine already refuse it - -Clause-②: yes (narrowing) - - - -**BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail, and so can a runtime dataset save, which runs the same rule. `measure-aggregate-field-type-refused` reads the field's declaration, not its type alone: `count_distinct` over a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true` is refused, because that field is a list stored as JSON and no two backends compare such values for equality alike. The dataset compile leg already answers the pair `400 DATASET_INVALID`, and the engine's `count_distinct` door answers it `400 INVALID_FIELD`. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What an author sees now.** The finding names the measure, the field, the object and the declaration with its flag (`select` with `multiple: true`), and says the aggregate accepts its row's types, none of them with `multiple: true`. The hint names the aggregates the declaration does accept, read from the same predicate: `count`. - -**What to write instead.** `count` over the field, or `count_distinct` over a field that stores one scalar value. To count the records holding one member, filter by it with `$contains` in a record query. - -**Unchanged.** `count_distinct` over the same types without the flag; `count` over any field; every other aggregate, whose rows accept no multi-capable type and whose verdicts therefore do not move; and the skips the rule already had. diff --git a/.changeset/20892-package-visibility-default-org.md b/.changeset/20892-package-visibility-default-org.md deleted file mode 100644 index d52c986853f..00000000000 --- a/.changeset/20892-package-visibility-default-org.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): `PackageSchema.visibility` defaults to `org` (was `private`), the create-time default every publish path already produced - -Clause-②: yes - -`PackageSchema` (`@objectstack/spec/marketplace`) filled an omitted `visibility` with -`private`, but no path that creates a package ever reached that value: the cloud control -plane gives a new package `org` when the create request omits the key, and -`os package publish` used to send `org` itself. The declared default now matches what the -runtime does: `org`, which makes a package published from one environment installable in -the owner organization's other environments. - -- **What changes:** `PackageSchema.parse(row)` on a row with no `visibility` now returns - `visibility: 'org'`. A row that names `private`, `org` or `marketplace` is read exactly - as before, and any other value is still refused. -- **What does not change:** the accepted values, and `CreatePackageRequestSchema.visibility`, - which stays optional with no default. A create request that omits the key reaches the - control plane without it, and the control plane's default applies. -- **If you relied on the old default:** pass `visibility: 'private'` explicitly. diff --git a/.changeset/20892-plugin-publish-visibility-when-asked.md b/.changeset/20892-plugin-publish-visibility-when-asked.md deleted file mode 100644 index 1d3e60e3fc2..00000000000 --- a/.changeset/20892-plugin-publish-visibility-when-asked.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os plugin publish` sends `visibility` only when `--visibility` is passed, so re-publishing no longer moves a `marketplace` plugin to `private` - -The `--visibility` flag of `os plugin publish` defaulted to `private`, and the CLI always -sent it in the package upsert (`POST /api/v1/cloud/packages`). That upsert is also the -re-publish path, and the control plane updates an existing package's visibility whenever -the request carries one. So re-publishing a new version of a `marketplace` plugin without -repeating `--visibility marketplace` silently set it to `private`. `os package publish` -already works this way; both commands now behave the same. - -The flag no longer has a default, and an omitted flag is an omitted key: - -- **Re-publish without the flag:** the package keeps its current visibility. -- **First publish without the flag:** the control plane applies its own default (`org` on - ObjectStack Cloud), the default `PackageSchema.visibility` declares. Before this change - `os plugin publish` sent `private` here; pass `--visibility private` to keep that. -- **With the flag:** unchanged. `--visibility private|org|marketplace` is sent and applied. - -The publish summary gains a `Visibility` line showing the control plane's answer. When the -control plane does not report it and no flag was given, the line says -`not reported by the control plane`. The "Re-run with --submit" hint now follows that -answer too, so it also fires when a `marketplace` plugin is re-published without the flag. diff --git a/.changeset/20892-publish-visibility-when-asked.md b/.changeset/20892-publish-visibility-when-asked.md deleted file mode 100644 index 8d9b1680c77..00000000000 --- a/.changeset/20892-publish-visibility-when-asked.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os package publish` sends `visibility` only when `--visibility` is passed, so re-publishing no longer moves a `marketplace` package to `org` - -Clause-②: no - -The `--visibility` flag defaulted to `org`, and the CLI always sent it in the package -upsert (`POST /api/v1/cloud/packages`). That upsert is also the re-publish path, and -the control plane updates an existing package's visibility whenever the request carries -one. So re-publishing a new version of a `marketplace` package without repeating -`--visibility marketplace` silently set it to `org` and took it out of the marketplace. - -The flag no longer has a default, and an omitted flag is an omitted key: - -- **Re-publish without the flag:** the package keeps its current visibility. -- **First publish without the flag:** the control plane applies its own default - (`org` on ObjectStack Cloud), so a new package gets the same visibility as before. -- **With the flag:** unchanged. `--visibility private|org|marketplace` is sent and - applied, as before. - -The publish summary's `Visibility` line shows the control plane's answer. When the -control plane does not report it and no flag was given, the line says -`not reported by the control plane`. The "visibility is marketplace but the version is -still draft" hint now follows that answer too, so it also fires when a `marketplace` -package is re-published without the flag. The `--submit` help text now states its -precondition as the package's visibility being `marketplace` (already stored, or set -with `--visibility marketplace`), since the flag is no longer sent on every publish. diff --git a/.changeset/20896-template-import-door.md b/.changeset/20896-template-import-door.md deleted file mode 100644 index 976726e5066..00000000000 --- a/.changeset/20896-template-import-door.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the import template (`GET /api/v1/data/:object/export?template=true`) is gated by the import door's permissions, not the export's - -Clause-②: no - -The template carries no records — only the columns the caller may write, one -example row and an instructions sheet — so it answers to whoever may import, and -the export permission (`allowExport`) neither admits nor refuses it: - -- A caller with the create permission on the object gets the template, with or - without `allowExport`. -- A caller without the create permission gets `403 PERMISSION_DENIED`, with or - without `allowExport`. -- An object whose `enable.apiMethods` exposes neither `create` nor `update` - answers `405 OBJECT_API_METHOD_NOT_ALLOWED`, as `POST /api/v1/data/:object/import` - does. An object that exposes `create` without `list` serves the template. -- Without `template=true` the export is unchanged: the same two export checks - and the same bytes. - -To let a role download the template, grant it create on the object. diff --git a/.changeset/20897-exists-non-boolean-refused.md b/.changeset/20897-exists-non-boolean-refused.md deleted file mode 100644 index b590d2745bf..00000000000 --- a/.changeset/20897-exists-non-boolean-refused.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/driver-memory': minor -'@objectstack/driver-mongodb': minor ---- - -fix(driver-memory, driver-mongodb)!: a non-boolean `$exists` comparand is refused with `INVALID_FILTER` / 400, as `$null`'s is, instead of selecting the rows with no value (#20897) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the in-memory driver and the MongoDB driver accept in a filter. A `$exists` comparand that is not a boolean (a string, a number, `null`, `undefined`, an object) is now refused with `INVALID_FILTER` / 400, where these two drivers used to answer it. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -`FieldOperatorsSchema` declares `$exists` as a boolean, and `driver-sql`, `driver-sqlite-wasm` and both Turso transports already refused any other comparand. The in-memory driver and the MongoDB driver did not: they read `$exists` as `value === true`, so every other value asked for the rows with NO value. `{ stage: { $exists: "yes" } }` and `{ stage: { $exists: 1 } }` returned the rows without a stage, the opposite of what was written. `0`, `null` and the string `"false"` landed on that same side by the same default, not because anything read them. The in-memory driver's analytics face read the same flag by truthiness and answered the valued rows for the same filter, so that driver gave two different answers. - -**What an author sees now.** `400 INVALID_FILTER` with `driver-sql`'s message, beginning `Operator "$exists" on field "FIELD" requires a boolean comparand (true or false).` and naming the position (`filter.stage.$exists`). On the in-memory driver the refusal covers `find`, `findOne`, `count`, `aggregate`, `updateMany`, `deleteMany` and the analytics face (`query()` and `generateSql()`). There, an `undefined` or object comparand is refused first by that face's comparand-type check, also `INVALID_FILTER` / 400, in its own words. A refused write changes nothing. - -**What to write instead.** Write the boolean itself. `"$exists": true` matches rows whose field has a value, and `"$exists": false` matches rows whose field has none. - -**Who is affected.** A caller that sent a non-boolean `$exists` to `InMemoryDriver` or `MongoDBDriver` (a test suite, a local or embedded deployment, a flow or hook calling the engine in-process) and read the answer as a real one. On `SqlDriver` the same filter was already a 400. - -**Unchanged.** `$exists: true` and `$exists: false` answer exactly as before. The aggregation `filter` and `having` positions, which the engine evaluates itself after the driver, are not changed by this entry. diff --git a/.changeset/20901-form-view-subform-columns-canonicalized.md b/.changeset/20901-form-view-subform-columns-canonicalized.md deleted file mode 100644 index 5bcdb0a440f..00000000000 --- a/.changeset/20901-form-view-subform-columns-canonicalized.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): a stored form view whose subform grid columns use the `field` spelling is respelled to `name` on the way in, as a relationship field's `inlineColumns` already are (#20901) - -**`@objectstack/spec`** - -- **New ADR-0087 conversion `form-view-subform-columns-canonicalized` (protocol 18, retired from the authoring path).** A form view's `subforms[].columns` accepted any value through 17.5.0 and now takes the inline grid column contract, which refuses `{ field: 'x' }` with the prescription naming `name`. The conversion rewrites that entry as `{ name: 'x' }`, every other key kept, wherever a form view travels as data at rest: a stored `view` row (its `form`, each `formViews` entry, a form view item's `config`, a flattened form overlay), an assembled manifest's `viewItems`, and `os migrate meta --from 17`, which lists the edit. A built artifact whose declared protocol floor is 17.5.0 or lower is converted too, not refused. An entry that already carries `name` is left alone, including one that carries both `field` and `name`: the parse names both keys, and the author picks one. It is the same respelling `field-column-lists-canonicalized` applies to a relationship field's `inlineColumns`, and both entries run one shared rule. -- **Authored sources are unchanged:** `defineStack` and `objectstack validate` do not replay a retired conversion, so a source that writes `field` on a subform column is still refused with the prescription. Write `name`. -- **Two step-18 migration entries now read true.** `inline-grid-column-currency-scale-refused` no longer says a column declaring no `type` keeps its `scale`: over a `currency` field of the child object, `defineStack` refuses it, under `inline-grid-column-identity-only-currency-scale-refused`, which the entry now names. `form-view-subform-columns-closed` names this conversion as the one mechanical edit on its carrier. diff --git a/.changeset/20901-inline-grid-column-carriers.md b/.changeset/20901-inline-grid-column-carriers.md deleted file mode 100644 index 10cfedb1c33..00000000000 --- a/.changeset/20901-inline-grid-column-carriers.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: a form view's subform columns are the inline grid column contract, and a column that declares no `type` is judged as the type it renders (#20901) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on two published authoring surfaces, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. The console's master-detail grid reads one column shape from two carriers: a relationship field's `inlineColumns` and a form view's `subforms[].columns`. Only the first was judged, and only by the type a column declares. - -**`@objectstack/spec`** - -- **`FormViewSchema.subforms[].columns`** now references `InlineGridColumnSchema`, the strict, name-keyed column a relationship field's `inlineColumns` already takes. It was `z.array(z.any())`, so every column published clean, including a key the grid never reads and a key the other carrier refuses. Every rule the column schema holds now applies on the form view too, with its own message: an unknown key is named; the retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`; a column without `name` is refused; `scale` on a column declaring `type: 'currency'` is refused with the currency ruling's remedy. This reaches `view.form` and every `view.formViews` entry, wherever a view is parsed against the spec: `defineStack`, `objectstack validate`, and the `view` metadata type's registered schema (`ViewMetadataSchema`). -- **`defineStack`'s cross-reference check** now judges a column that declares no `type` as the type it renders. The console fills such a column's type from the child field, so an identity-only column over a `currency` field renders as a currency column. The check resolves the child field, re-parses the column with that type through `InlineGridColumnSchema`, and reports that schema's own refusal (`STACK_CROSS_REFERENCE_INVALID`, 422). Today that means `scale` on an identity-only column over a `currency` child field. Both carriers are walked: `inlineColumns` resolves against the object that owns the relationship field, and `subforms[].columns` against the subform's `childObject`. A child object the stack does not declare, or a column naming no field of it, is not judged. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `subforms: [{ childObject: 'invoice_line', columns: [{ field: 'quantity' }] }]` | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'quantity' }] }]` | -| `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency', scale: 2 }] }]` | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency' }] }]` | -| `columns: [{ name: 'amount', scale: 2 }]` where `amount` is a `currency` field of the child object (either carrier) | `columns: [{ name: 'amount' }]` | -| a column carrying a key the column schema does not declare | the column without that key | - -The one-line fix: write each form-view subform column as `{ name, … }` using only the keys a relationship field's `inlineColumns` accepts, and delete `scale` from any column that renders as a currency column, whether it declares `type: 'currency'` or takes it from a `currency` child field. Nothing replaces `scale` there: the currency's ISO 4217 minor unit decides the displayed decimals. - -## Who is affected, measured - -On `origin/main` `cb4c31dd52`: zero authored `subforms` in the repository, and one authored `inlineColumns` block (the showcase invoice, seven identity-only columns, none carrying `scale`). No example, template or test fixture outside this change's own pins changes verdict. Deployed metadata was not measured. diff --git a/.changeset/20901-step18-rationale-form-view-subform-columns.md b/.changeset/20901-step18-rationale-form-view-subform-columns.md deleted file mode 100644 index fe1eeab3164..00000000000 --- a/.changeset/20901-step18-rationale-form-view-subform-columns.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): protocol 18's migration rationale now covers the form view's inline grid columns and the identity-only currency `scale` refusal (#20901) - -**`@objectstack/spec`** - -- **`MIGRATIONS_BY_MAJOR[18].rationale` gains one fragment, `form-view-subform-columns-closed`.** It is the paragraph `os migrate meta --step` shows for the protocol 17 → 18 hop. The new sentences say that a form view's `subforms[].columns` now takes the strict `InlineGridColumnSchema` a relationship field's `inlineColumns` takes, that the conversion `form-view-subform-columns-canonicalized` respells a `{ field }` column as `{ name }` in stored rows and assembled artifacts while an author writing `field` is refused, and that `defineStack` refuses `scale` on a column that declares no `type` when its `name` is a `currency` field of a child object declared in the same stack (`inline-grid-column-identity-only-currency-scale-refused`). -- Text only: no schema, conversion or migration entry changes, and `conversionIds` and `semantic` for step 18 are unchanged. diff --git a/.changeset/20910-regime-c-action-permission-paths.md b/.changeset/20910-regime-c-action-permission-paths.md deleted file mode 100644 index 730eae421d1..00000000000 --- a/.changeset/20910-regime-c-action-permission-paths.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): every refusal of an in-place edit of a packaged flow, action or permission set names that type's own sanctioned path, at every door, and a packaged action's removal names one too, not a redeploy or `OS_METADATA_WRITABLE` (#20910) - -Clause-②: no - -ADR-0126 puts `flow`, `action` and `permission` in Regime C. The packaged base is locked, and the refusal names the sanctioned path. Until now only a flow's package-less refusal did, and only at one of the three places that refuse such a write. The other places prescribed "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE …" or "Set OS_METADATA_WRITABLE to enable additional types at runtime". A packaged action's removal named no path at all. - -There is now one regime table, and each type's row names only the primitives that type has: - -- a packaged flow: clone it under a new name with `POST /api/v1/automation/:name/clone` and `{ name, label }`, or switch it off with `POST /api/v1/automation/:name/toggle` and `{ enabled: false }`; -- a packaged action: switch it off with `POST /api/v1/actions/_activation/:object/:action` and `{ enabled: false }` (`:object` is `global` for an object-less action). No clone is named, because cloning an action is not a sanctioned path; -- a packaged permission set: clone it under a new name, with the "Clone" action on the permission set or `POST /api/v1/data/sys_permission_set` with a new name. This is the same wording the permission-set lock in `@objectstack/plugin-security` already uses. - -The switches are operator-only where one install serves several organizations. Every refusal cites ADR-0126. All three places that refuse such a write read the same table: - -- **`403 NOT_OVERRIDABLE` on an environment-scoped kernel.** This covers `PUT /api/v1/meta/:type/:name` without `?package=`, and for a flow or an action `DELETE` too. -- **`403 NOT_OVERRIDABLE` where the `/meta` protocol is not environment-scoped**, for example the default local `pnpm dev` boot. The metadata repository refuses that write one layer down, and now with the same sentence. -- **`403 ITEM_LOCKED` for a write that names the read-only package with `?package=`, while `OS_METADATA_WRITABLE` is not set for the type.** The sentence now opens "Cannot overlay 'TYPE' in package 'ID': that package is read-only, and its packaged base is locked against in-place edits." and then names the path. - -A packaged flow's package-less sentence is byte-for-byte unchanged. Statuses, codes, `lockSource`, `packageId`, `docs` and which writes are refused are unchanged too. `OS_METADATA_WRITABLE` still opens the lock for a write that names no package. The `ITEM_LOCKED` refusal given while the variable IS set reads exactly as before. Removing a permission set's overlay row is still allowed, as repair. Every type with no declared regime reads exactly as before, at every door. diff --git a/.changeset/20912-analytics-multi-value-distinct-refused.md b/.changeset/20912-analytics-multi-value-distinct-refused.md deleted file mode 100644 index 387497e9e85..00000000000 --- a/.changeset/20912-analytics-multi-value-distinct-refused.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: a grouped dimension on a multi-value field and a `count_distinct` measure over a JSON-stored field are refused with `INVALID_FIELD` / 400 at the analytics door, before any SQL is built; a dataset `count_distinct` measure over a field declared `multiple: true` is refused at compile time - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the analytics query doors accept, in two positions, and what the dataset compiler accepts, in one. It holds on `POST /api/v1/analytics/query`, on its dry run `POST /api/v1/analytics/sql` and on `POST /api/v1/analytics/dataset/query`, on every driver and on both strategies. It ships as `minor` under the launch-window convention for accept-set narrowings. - -- A cube or dataset dimension whose column is a **multi-value** field, when it groups the result (a `dimensions` entry, or a `timeDimensions` entry with a `granularity`): an inherently multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. The same types without the flag are served. -- A `measures` entry that resolves to a `count_distinct` measure whose column is **JSON-stored**: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently multi option type, or a multi-capable field declared `multiple: true`. An authored cube measure, a suffix-inferred one (`tags_count_distinct`) and a compiled dataset's are judged alike; a `count` measure is not judged. -- A dataset measure that pairs `count_distinct` with a base-object field declared `multiple: true` is refused `400 DATASET_INVALID` when the dataset compiles, at registration and on the request door, beside the type row that already refused `tags`. - -The column is judged where it is declared: on the cube's object, or, for a dotted path, on the object the cube's declared join names. - -**What an author sees now.** `400 INVALID_FIELD`, naming the member as the request wrote it, the column, the object, the declaration (`select with multiple: true`), saying the query was not run, and naming the route. For a multi-value field the route is a record query on the declaring object filtered by one member with `$contains`, one query per member. For a structured-JSON field it is to store the scalar part in a field of its own. The thrown error carries `member`, `param` (`dimensions`, `timeDimensions` or `measures`), `cube`, `field` and `object`. The dataset compile refusal names the measure, the field and its declaration with `multiple: true`. - -**Why a refusal.** The engine's aggregate door already refuses both shapes, and the native-SQL strategy compiled its own statement and never reached it. Measured through this service as `AnalyticsServicePlugin` composes it over a real engine: a dimension on a `tags`, `multiselect` or `multiple: true` select answered 200 with one group per serialized array on SQLite and 500 `DATABASE_ERROR` on PostgreSQL 16; an inferred `count_distinct` over a `json`, `tags` or `multiple: true` select field answered 2, 3 and 2 on SQLite and 500 on PostgreSQL; a dataset `count_distinct` over the `multiple: true` select registered, then answered 2 on SQLite and 500 on PostgreSQL. The engine-aggregate strategy answered 400 for every one of these, under the engine's position (`groupBy[0]`, `aggregations[0].field`) rather than the member the caller wrote. The predicates are `@objectstack/spec/data`'s, the ones the engine's doors read: `isMultiValueField`, and the aggregate × field-type table's `count_distinct` row. - -**Your fix (a host calling `compileDataset` directly).** `DatasetCompileOptions` no longer has `declaredFieldType`. Pass `declaredValueShape` instead: the same read of the field's metadata, answering `{ type, multiple }` (the type, and `multiple === true`) rather than the type alone, or `undefined` when nothing answers. A host that passes neither compiles exactly as before, with no aggregate × field-type refusal at all. `AnalyticsService` and `AnalyticsServicePlugin` wire it themselves from `sourceFieldMeta`. - -**Who is affected.** A dashboard, report or caller that grouped by a multi-value field, or counted a JSON-stored field distinct, through the native-SQL strategy on SQLite and read the serialized arrays or the text-compared count as real answers. On PostgreSQL the same queries were already a 500. A dataset that pairs `count_distinct` with a `multiple: true` field no longer registers. - -**Unchanged.** A dimension or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; a member naming a column the object does not have, which keeps its existing `INVALID_FIELD` answer first; a dotted path the cube declares no join for; a measure whose `sql` is an expression; and a host that wires no `sourceFieldMeta`, where the declaration cannot be read. diff --git a/.changeset/20913-flow-stored-row-shipped-name.md b/.changeset/20913-flow-stored-row-shipped-name.md deleted file mode 100644 index 76f4cbb3927..00000000000 --- a/.changeset/20913-flow-stored-row-shipped-name.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): a stored flow under a name a managed package ships is no longer registered or listed as the package's flow (#20913) - -Clause-②: no - -`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Two places still treated a stored flow of a shipped name as an overlay of the package's flow: - -- The startup hydration registered the stored flow carrying the package's provenance, so the automation engine could not tell it apart from the package's own flow. It is now registered as the tenant-authored row it is. -- The flattened flow list served the stored flow in the package's place, marked as the package's. That list is `GET /api/v1/meta/flow` and the execution view the automation engine binds flows from. For a name a managed package ships, the list now serves the package's flow. - -The stored flow is not deleted, rewritten or refused. It stays in the store, and the automation engine reports it as a shadowed definition at startup. Every other metadata type, and every flow name no managed package ships, is listed as before. The by-name read, `GET /api/v1/meta/flow/:name`, is not changed. diff --git a/.changeset/20913-flow-sync-one-precedence.md b/.changeset/20913-flow-sync-one-precedence.md deleted file mode 100644 index f10938dc5c4..00000000000 --- a/.changeset/20913-flow-sync-one-precedence.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(automation): for a flow name a managed package ships, every startup step arms the package's flow, and a stored flow of that name is reported as shadowed (#20913) - -Clause-②: no - -A startup arms flows in two steps: the boot pull from the metadata registry, then a second bind at `kernel:ready` from the metadata protocol's flow list. The second step registered every flow the list held, with no precedence at all. So for a name a managed package ships, a stored flow of the same name could be armed after the boot pull had armed the package's flow. The stored definition then ran, while `getShadowedFlows()` and the startup warnings said the package's flow was armed, and named both contenders as the package. - -- Both startup steps, and the re-bind on `metadata:reloaded`, now resolve same-named flows through one precedence decision: `resolveFlowPrecedence`, with the engine's reader over the package loader's set. -- Within a name a managed package ships, the package's flow is armed and a stored flow of that name is shadowed. A managed package's flow is sealed (ADR-0126 §2): it is customized by cloning it under a new name or by switching it off. This replaces the earlier direction, in which a flow authored in the deployment won over the packaged flow of the same name. -- The shadowing record and the startup warnings name the stored flow as a runtime-authored row, not as the package. The collision warning says which rule armed the flow. -- Names no managed package ships are unchanged: flows authored in the deployment keep the order they were listed in, and two packages shipping one name still resolve by package id. - -**If you call `resolveFlowPrecedence` yourself:** within a name the reader says a package ships, the packaged contender now wins over a contender authored in the deployment. diff --git a/.changeset/20914-aggregate-door-whole-table.md b/.changeset/20914-aggregate-door-whole-table.md deleted file mode 100644 index fa0548626fd..00000000000 --- a/.changeset/20914-aggregate-door-whole-table.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@objectstack/objectql": minor -"@objectstack/spec": patch ---- - -fix(objectql)!: the engine's `aggregate` asks the aggregate × field-type table for every aggregation over a declared field, so `min` / `max` / `avg` over a type the table refuses answer `INVALID_FIELD` / 400 on every driver instead of one answer per driver - -Clause-②: no (narrowing) - - - -**BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, on every driver and for every caller that reaches the engine — the REST query door, a flow or hook, a roll-up summary's recompute, and the analytics strategy that lowers a cube query onto `engine.aggregate`. Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -FROM → TO, per aggregation `{ function, field }` naming a declared field: - -- `min` / `max` over a type outside the numeric, temporal and boolean classes — the structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), the multi-option types (`multiselect`, `checkboxes`, `tags`), the string family (`text`, `email`, `url`, `phone`, …), the option and reference types (`select`, `radio`, `lookup`, `master_detail`, `tree`, `user`), `autonumber`, the file family and `formula` — and over any `select`, `lookup`, `user`, `file` or `image` declared `multiple: true`: FROM whatever the driver answered (a document or an array in memory, the serialized text on SQLite, a 500 on PostgreSQL for a JSON-stored field; a collation-dependent string for a text field) TO `400 INVALID_FIELD`. -- `avg` over a type outside the numeric and boolean classes — a `date`, `datetime` or `time` field included: FROM `null` in memory, a coerced number on SQLite (the average YEAR for a datetime), a 500 on PostgreSQL, TO `400 INVALID_FIELD`. -- `count_distinct` is unchanged: it was already refused over the JSON-stored types, in the same words. - -**What an author sees now.** `400 INVALID_FIELD`, naming the position (`aggregations[0].field`), what the function does and the field with its declaration (`takes the max of 'meta', a declared json field — a structured-JSON value`), saying the query was not run, and naming the types the function accepts, read off the table, inside the first 500 characters the REST door keeps. The thrown error carries `field`, `fields` (every offending aggregation), `object` and `param` (`aggregations`). - -**Why a refusal.** `AGGREGATE_FIELD_TYPE_COMPATIBILITY` already declares which pairs every backend answers the same way, and the dataset compile and lint legs refuse the rest; the engine door asked only its `count_distinct` row. Measured through `engine.aggregate` over two rows: `max` over a `json` field answered `{ a: 1 }` in memory, the string `'{"b":1}'` on SQLite and 500 `DATABASE_ERROR` on PostgreSQL 16 (`function max(json) does not exist`); a `tags` field and a `multiple: true` select or lookup split the same way; `avg` over a `datetime` answered `null`, `2026` and a 500. One query, three answers. - -**What to write instead.** Aggregate a field of a type the function accepts — for `min` / `max`: `number`, `currency`, `percent`, `rating`, `slider`, `progress`, `summary`, `date`, `datetime`, `time`, `boolean` or `toggle`; for `avg`: the same minus the temporal three. A question that was counting in disguise is `count` (or `count_distinct` over a scalar-stored field). A first or last record by a text value is a sort on a list, not an aggregate. A quantity stored as text or JSON belongs in a numeric or temporal field of its own, aggregated there. - -**Who is affected.** A caller that asked `min` / `max` / `avg` of such a field on the in-memory driver or SQLite and read the answer as a real one; on PostgreSQL a JSON-stored field was already a 500. Metadata that lowers onto `engine.aggregate` takes the same verdict at run time: a roll-up summary (`summaryOperations`) whose `min` / `max` / `avg` names such a child field records a failed recompute, a grouped list view's server-side header summary is refused, and a chart or metric component's `aggregate` over such a field is refused. No example app and no published stack authors such a pair. - -**Not judged yet: `sum`.** The `sum` row of the table is held back at this door: a published stack authors a `sum` column summary over a `formula` field, a pair the table refuses, so that row awaits its own decision. `sum` over any field reaches the driver as before. - -**Unchanged.** Every pair the table accepts; `count` over any field, a JSON-stored one included; an aggregation that names no field; an undeclared name or a relationship path, which this door does not judge (the REST door answers an unknown name `INVALID_FIELD` before the engine is reached); a field whose declared type is outside `FieldType`. The structured-JSON `groupBy` entry of this same release lists a structured-JSON field as an aggregated `min` / `max` column as unchanged; this entry is the later word on that shape. - -`@objectstack/spec`: the TSDoc of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` no longer says the engine's `aggregate` door reads only the `count_distinct` row. The table itself is unchanged. diff --git a/.changeset/20914-release-sum-row.md b/.changeset/20914-release-sum-row.md deleted file mode 100644 index 055089bdfa8..00000000000 --- a/.changeset/20914-release-sum-row.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@objectstack/objectql": minor -"@objectstack/spec": patch ---- - -fix(objectql)!: the engine's `aggregate` judges the `sum` row of the aggregate × field-type table too, so `sum` over a type the table refuses answers `INVALID_FIELD` / 400 on every driver instead of `0` in memory and on SQLite and a 500 on PostgreSQL - -Clause-②: no (narrowing) - - - -**BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, on every driver and for every caller that reaches the engine — the REST query door, a flow or hook, a roll-up summary's recompute, and the analytics strategy that lowers a cube query onto `engine.aggregate`. Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -This completes the change of this same release that made the engine's `aggregate` ask the table for `min`, `max` and `avg`, and that held the `sum` row back. Its paragraph "Not judged yet: `sum`" is superseded: this entry is the later word, and the door now asks every row of the table. - -FROM → TO, per aggregation `{ function: 'sum', field }` naming a declared field: - -- `sum` over a type outside `number`, `currency`, `rating`, `slider`, `progress`, `summary`, `boolean` and `toggle` — a `percent` (a rate does not add), the temporal types (`date`, `datetime`, `time`), the string family (`text`, `email`, `url`, `phone`, …), the option and reference types (`select`, `radio`, `lookup`, `master_detail`, `tree`, `user`), `autonumber`, the file family, the structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), the multi-option types (`multiselect`, `checkboxes`, `tags`) and `formula` — and over any `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`: FROM whatever the driver answered TO `400 INVALID_FIELD`. Measured through `engine.aggregate` over two rows: a `json`, `text`, `select` or `tags` field summed to `0` in memory and on SQLite and answered 500 `DATABASE_ERROR` on PostgreSQL 16 (`function sum(json) does not exist`); a `datetime` field summed to `0` in memory, to the years added on SQLite and a 500 on PostgreSQL; a `formula` field summed to `0` in memory and was already refused `400 INVALID_FIELD` by both SQL drivers, which have no column for it; a `percent` field added the rates on all three. - -**What an author sees now.** `400 INVALID_FIELD`, naming the position (`aggregations[0].field`), what the function does and the field with its declaration (`sums 'meta', a declared json field — a structured-JSON value`), saying the query was not run, and naming the types `sum` accepts, read off the table, inside the first 500 characters the REST door keeps. The thrown error carries `field`, `fields` (every offending aggregation), `object` and `param` (`aggregations`). - -**What to write instead.** Sum a field of a type `sum` accepts: `number`, `currency`, `rating`, `slider`, `progress`, `summary`, `boolean` or `toggle`. A rate stored as a `percent` is averaged (`avg` accepts it), or the quantity it is a rate of is summed. A value computed by a `formula` is stored in a numeric field of its own when it must be summed on the server. A question that was counting in disguise is `count`. - -**Who is affected.** A caller that asked `sum` of such a field on the in-memory driver or SQLite and read the `0` as a real total, and a caller that summed a `percent` field on any driver. On PostgreSQL the other measured pairs were already refused (a 500, or a 400 for a `formula`), and on SQLite so was a `formula`. Metadata that lowers onto `engine.aggregate` takes the same verdict at run time: a roll-up summary (`summaryOperations`) whose `sum` names such a child field records a failed recompute, a grouped list view's server-side header summary is refused, and a chart or metric component's `sum` over such a field is refused. No example app authors such a pair. One published stack authors a `sum` list-column summary over a `formula` field; that summary is computed client-side and does not reach `engine.aggregate`. - -**Unchanged.** Every pair the table accepts, `sum` over the eight types above included; `count` over any field; an aggregation that names no field; an undeclared name or a relationship path, which this door does not judge (the REST door answers an unknown name `INVALID_FIELD` before the engine is reached); a field whose declared type is outside `FieldType`. - -**A correction to the earlier entry of this release.** It listed the multi-capable types declared `multiple: true` as `select`, `lookup`, `user`, `file` or `image`; the list is `select`, `radio`, `lookup`, `user`, `file` and `image` (`MULTI_CAPABLE_TYPES`). A `radio` declared `multiple: true` was refused by `min` / `max` / `avg` there all the same, by its type's own row, and it is refused by `sum` here. - -`@objectstack/spec`: the TSDoc of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` states that the engine's `aggregate` door asks every row of the table, where it said "the other rows" while one was held, and names `radio` among the multi-capable types. The table and the predicate are unchanged. diff --git a/.changeset/20917-analytics-field-permission-gate.md b/.changeset/20917-analytics-field-permission-gate.md deleted file mode 100644 index 8ec24209245..00000000000 --- a/.changeset/20917-analytics-field-permission-gate.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: every analytics face answers the engine's field-level read refusal, whichever strategy serves the cube: a field the caller may not read is judged before either strategy runs (#20917) - -Clause-②: yes (narrowing) - - - -**BREAKING for analytics queries that read a field the caller may not read: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube.** - -**What changed.** `POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql` -and `POST /api/v1/analytics/dataset/query` now judge every field a query reads -against the caller's field-level read permissions before a strategy is chosen: -dimensions, measures, time dimensions, filter members, order keys, members -joined through a relationship, and a dataset's own and its requested measures' -filters. A member of an authored cube is judged by the field it resolves to, -not by its name in the cube. A field the caller may not read answers -`403 PERMISSION_DENIED`, in the words the engine uses for the same field. The -native-SQL strategy, the one a SQL driver serves first, answered such queries; -the ObjectQL strategy already refused them on `POST /api/v1/analytics/query` -and `POST /api/v1/analytics/dataset/query`, as the data API did, but printed -the statement on `POST /api/v1/analytics/sql`. - -**What is not affected.** A query that reads only fields the caller may read -answers as before. A system context, and a caller with no permission sets, are -unaffected, as on the data API. A host read scope (row-level policy) may still -name fields the caller cannot read. A deployment with no security service applies -no field-level check, as on the data API. A member of an authored cube whose `sql` -is an expression is not attributed to a field. - -**New hook.** `AnalyticsServiceConfig.getReadableFields(object, context)` supplies -the reader. `AnalyticsServicePlugin` wires it to the `security` service's -`getReadableFields`; a host that constructs `AnalyticsService` itself passes its -own, and without one no field-level check applies. - -**If a widget stopped answering for some users,** it reads a field those users -may not read. Grant that field's read permission to the users who need it, or -build the widget on fields they can read. diff --git a/.changeset/20918-objectql-not-multivalue-served.md b/.changeset/20918-objectql-not-multivalue-served.md deleted file mode 100644 index 2e3011bb3df..00000000000 --- a/.changeset/20918-objectql-not-multivalue-served.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics): on the engine-aggregate path, a `$not`, `$notContains` or null test over a multi-valued lookup now gets the engine's rows instead of `400 INVALID_FILTER` (#20918) - -Clause-②: yes - -**What changed.** `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query`, when served by the engine-aggregate strategy (a query with a granularity, or a host with no raw SQL), used to refuse these filters on a SQL driver when the field is a multi-valued lookup (or any other JSON-stored multi-value field). The engine's `find()` and the native-SQL strategy answered them: - -- `{ $not: { owners: { $contains: 'u1' } } }`, and any `$not` whose operand tests such a field; -- `{ owners: { $notContains: 'u1' } }`; -- `{ owners: { $null: false } }`, `{ owners: { $exists: true } }`, `{ owners: { $null: true } }`, and the same tests in a dataset measure's own `filter`. - -Each now answers the rows the native-SQL strategy answers. Where `engine.find()` serves the same filter, those are its rows too. - -**Why.** The analytics `where` door adds a NULL test beside each leaf of a `$not` operand, so that a row holding no value is still answered by the negation. It adds a NULL alternative to the negative-polarity operators too. The engine-aggregate strategy passed both tests to the engine as `{ $ne: null }` and the bare `{ field: null }`. `driver-sql` refuses both spellings over a JSON column, so the whole filter was refused. They now reach the engine as `{ $null: false }` and `{ $null: true }`. The engine's own filter lowering writes the same tests in those spellings for every driver, and `driver-sql` applies them on a JSON column. - -**Unchanged.** Every filter on a single-valued field gets the same rows as before. The native-SQL strategy and the `POST /api/v1/analytics/sql` echo are unchanged: they compile their own SQL. A read scope is unchanged too. - -**One difference from the engine remains.** `{ owners: { $ne: null } }` and the bare `{ owners: null }` are null tests at the analytics door. Both strategies now answer them, the native strategy as before. `engine.find()` refuses them, because `driver-sql` reads `$ne` and the bare equality as value comparisons on a JSON column. `{ $null: false }` / `{ $null: true }` is the spelling both read the same way. diff --git a/.changeset/20919-core-import-runner.md b/.changeset/20919-core-import-runner.md deleted file mode 100644 index 8f6514144e2..00000000000 --- a/.changeset/20919-core-import-runner.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/core': minor ---- - -feat(core): the bulk-import runner, its row coercion, the mapping apply and the field-meta map now live in `@objectstack/core`, beside `bulkWrite` (#20919) - -`runImport` (with `sanitizeRowError` and its option/result types), the cell -coercion (`coerceRow`, `coerceFieldValue`, `parseDateCell`, `parseNumberCell`, -`parseBooleanCell`, `matchOption`, `splitMulti`, `isBlank`), the `mapping` -artifact pipeline (`applyMappingToRows`, `refuseUnknownMappingTargets`, -`MappingArtifactLike`, `MappingFailure`, `ApplyMappingOptions`) and the field -metadata map (`buildFieldMetaMap`, `ExportFieldMeta`) are exported from -`@objectstack/core`. They moved here unchanged from `@objectstack/rest` so the -connector sync executor in `@objectstack/service-automation` writes through the -same runner as the HTTP import door without depending on the HTTP layer. Nothing -to change for consumers: `@objectstack/rest` re-exports every name it exported -before. diff --git a/.changeset/20919-rest-re-exports.md b/.changeset/20919-rest-re-exports.md deleted file mode 100644 index e913d49fa3c..00000000000 --- a/.changeset/20919-rest-re-exports.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -refactor(rest): the import runner, coercion, mapping apply, field-meta map and error classification moved to `@objectstack/core` / `@objectstack/types`; `rest` re-exports them (#20919) - -`runImport`, `coerceRow`, `buildFieldMetaMap` and their types (from the package -index), and `mapDataError` with its sibling classification exports, are now -re-exported from their new homes — byte-identical code, the same names, the same -behaviour at both import routes and at `plugin-auth`'s identity import. Nothing to -change for consumers. diff --git a/.changeset/20919-service-automation-connector-pull.md b/.changeset/20919-service-automation-connector-pull.md deleted file mode 100644 index 07ca7315292..00000000000 --- a/.changeset/20919-service-automation-connector-pull.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -feat(service-automation): the connector sync executor pulls a `mapping`'s `connectorSource` and writes it through the import runner (#20919) - -`AutomationServicePlugin.pullConnectorSource({ mapping, context })` (and the -exported `pullConnectorSource(deps, opts)`) reads the mapping through the -protocol's `getMetaItem`, resolves `connectorSource.connector` to a declared -(`connectors[]`) `rest` or `openapi` instance, makes ONE call to its read action, -takes the array at `recordsPath` (default `body`), projects it through the -mapping's `fieldMapping` and writes it with `@objectstack/core`'s `runImport` — -the import door's coercion, `mode` / `upsertKey` matching and per-row verdicts, -with the door's defaults for every knob `connectorSource` does not declare. - -- **Watermark, read from the target.** For `connectorSource.watermark`, the - starting point sent as `query[watermark.param]` is the highest value already - stored in the target field a `fieldMapping` entry copies `watermark.field` onto - (transform `none`). Nothing else stores it. -- **One response per pull.** The connector's paging is not followed. -- **Loud refusals,** each a `ConnectorPullError` (`code`, `status`, `reason`) - raised before anything is written: a plugin-registered or unregistered - connector, a degraded instance, a provider other than `rest` / `openapi`, an - undeclared action, `update` / `upsert` with an empty `upsertKey`, a - `javascript` transform, an unmapped `watermark.field`, an `ok: false` answer, - a non-array at `recordsPath` and a non-object record. -- **Nothing schedules a pull** — a `job` will drive it; the caller supplies the - execution context. - -The plugin now records the provider of each declared connector instance it -materializes, which the executor reads. diff --git a/.changeset/20919-spec-connector-source-live.md b/.changeset/20919-spec-connector-source-live.md deleted file mode 100644 index 68c7459d98f..00000000000 --- a/.changeset/20919-spec-connector-source-live.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): `mapping.connectorSource` is pulled when a job drives it — the describes say where the watermark is read from and that a pull reads one response (#20919) - -The `connectorSource` describe no longer says the pull is not executed: the -connector sync executor in `@objectstack/service-automation` reads the binding, -and nothing schedules a pull until the `job` stage lands. `watermark.field` now -states that the next pull's starting point is read from the TARGET field a -`fieldMapping` entry copies it onto (an unmapped one is refused at pull time), and -`watermark` states the one-response limit: the connector's paging is not followed, -so a paged endpoint yields its first page only. The liveness ledger's -`connectorSource` rows are `live`, with no author warning: that nothing schedules a -pull yet is said on the key's description. The retired `connector.syncConfig` -prescription and the `connector-sync-keys-retired` upgrade entry say the same, and -the entry's acceptance criterion no longer claims the connector is validated at -authoring. -No key, value or default changed. diff --git a/.changeset/20919-types-data-error-classification.md b/.changeset/20919-types-data-error-classification.md deleted file mode 100644 index 17346e7c477..00000000000 --- a/.changeset/20919-types-data-error-classification.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/types': minor ---- - -feat(types): the data-error classification table (`mapDataError` and its judgements) is exported from `@objectstack/types` (#20919) - -The classification half of `@objectstack/rest`'s error boundary — `mapDataError` -(a thrown error → the `{ status, body }` ADR-0112 answer, without emitting it), -`declaredHttpStatus`, `declaredServerFaultAnswer`, `sandboxBusinessMessage`, -`boundedDeclaredUserMessage`, `boundedDeclaredRefusalMessage` and -`isEngineDuplicateRecordEnvelope` — moved here unchanged, beside the primitives it -composes, so the bulk-import runner in `@objectstack/core` judges a failed row with -the same table the REST door answers with. `@objectstack/rest` keeps the emitters -and re-exports every name it exported before. The module also exports the eight -helpers the REST emitters compose (`truncateClientMessage`, `thrownCodeFields`, -`withoutDeclaredCodePrefix`, `withDeclaredUserMessage`, `isSandboxOrigin`, -`isSandboxCrash`, `fiveXxArmDisplacesDeclared4xx`, `structuredCodeAnswer`). diff --git a/.changeset/20920-use-grouping-form-row.md b/.changeset/20920-use-grouping-form-row.md deleted file mode 100644 index 20bbe3ba01c..00000000000 --- a/.changeset/20920-use-grouping-form-row.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/platform-objects": patch ---- - -Clause-②: no - -The field form offers `useGrouping` on `number` fields: one plain boolean row beside `scale`, gated to `number` as `scale` is, whose control copies the field form's `allowCreate` row (the same `z.boolean().optional()` node, no default). The key was declared by `FieldSchema` and graded `live` by the liveness ledger once the console's number display began to answer an authored value first, but no form offered it, so an author's only door was the Source tab. The help text follows the key's own description: unset lets the renderer decide, off never groups (a year or an ID), on always groups. It also says that an untouched switch writes nothing, so it reads off even where the renderer groups. - -⛔ **No schema accept set moves and no export changes.** What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles, whose two new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills. diff --git a/.changeset/20921-import-row-dropped-fields.md b/.changeset/20921-import-row-dropped-fields.md deleted file mode 100644 index a3c62d31bf0..00000000000 --- a/.changeset/20921-import-row-dropped-fields.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -The import row report and the validate-only answer can now say which fields a write drops. `ImportRowResultSchema` and each `ValidateDataResponseSchema.results[]` row gain an optional `droppedFields`: an array of `DroppedFieldsEventSchema`, the engine's own strip event. So the reason vocabulary is the engine's (`readonly`, `readonly_when`, `primary_key`, `computed`), and there is no second enum. The row still succeeds: `ok`, `action` and `valid` are unchanged. A server that does not produce the report omits the key, so an absent key alone does not prove nothing was dropped. - -`ImportRowResultSchema` also declares `warnings`, which the REST import dry run already serves: the findings the validate verdict admits, in the `ValidateDataIssue` shape, on an ok dry-run row. Until now `ImportRowResultSchema.parse` stripped the key, and readers typed by the spec could not see it. - -`ImportJobResultsSchema.results` now says what an async reader gets: a capped sample, failures first. An ok row's `droppedFields` or `warnings` reaches that reader only if the row falls inside the sample. The cap is unchanged. - -A consumer that branches on `reason` must stay exhaustive over `DroppedFieldsEvent['reason']`. The known exhaustive consumer is objectui's write-warning toast table, `STRIPPED_LINE` in `packages/app-shell/src/providers/writeWarningToast.ts`, which covers all four reasons today. A reader that renders the import or preview report should word `reason` through that table rather than a second one. When the union widens again, the table keyed by it fails type-check on the missing reason, while a second table would fall behind without a sound. diff --git a/.changeset/20922-per-row-dropped-fields.md b/.changeset/20922-per-row-dropped-fields.md deleted file mode 100644 index a25a10ad837..00000000000 --- a/.changeset/20922-per-row-dropped-fields.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/objectql': minor -'@objectstack/metadata-protocol': minor ---- - -The dry run and the partial-success batch insert now say which row lost which field. `ObjectQL.validate` (and `validateData`, which relays it) answers `droppedFields` on each accepted row of `results`, and `ObjectQL.insertMany` (and `insertManyData`, which passes it through) answers `droppedFields` on each `ok` outcome: the caller-supplied fields the engine legally strips from that row, one `DroppedFieldsEvent` per reason, in the engine's own reason vocabulary (`computed` for a `formula` value, `readonly` for a static `readonly` or runtime-owned field). The key is absent when nothing was taken from the row. - -- **Recorded at the strips, never inferred from the union.** Each strip records what it takes from each row as it runs. A `beforeInsert` hook that assigns a protected key on one row keeps it there, so that row is not named, while a sibling row that supplied the same key and lost it is. -- **A row the write does not complete carries none.** A preview row the verdict refuses, and an `ok: false` outcome, carry no `droppedFields`: a drop means the write completed without the field. -- **The dry run and the commit agree.** On `insert` mode the preview runs the same strips the write runs, so a row's preview drops and its outcome drops are the same list. One gap is unchanged: the preview runs no hooks, so a key a `beforeInsert` hook assigns is reported by the preview and kept by the write. An `update`-mode preview does not run the `readonlyWhen` or primary-key strips, which judge a prior record the preview does not read. -- **Unchanged:** the `onFieldsDropped` listener on `insert`, `insertMany` and `validate` still reports the batch-level union, one event per reason, naming no row. So does `insertManyData`'s top-level `droppedFields`. `insert(object, rows[])` still returns the records, with no per-row slot. `strictReadonlyWrites` still refuses the whole batch before any outcome is built. - -Graded `minor` in both packages: each widens a published method's declared answer with a new optional key (`InsertManyRowOutcome` gains `droppedFields`, and so does each outcome of `insertManyData`'s return type), which is an additive widening of the public surface. Nothing is removed, renamed or refused. The keys on the wire, `ValidateDataResponseSchema.results[].droppedFields` and `ImportRowResultSchema.droppedFields`, were already declared in `@objectstack/spec`. diff --git a/.changeset/20928-master-detail-details-closed.md b/.changeset/20928-master-detail-details-closed.md deleted file mode 100644 index 4b4a3fc1a79..00000000000 --- a/.changeset/20928-master-detail-details-closed.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': patch ---- - -feat(spec)!: an `object-master-detail-form` block's detail entries are a strict shape, and their columns are the inline grid column contract (#20928) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. The console's master-detail grid reads one column shape from three carriers: a relationship field's `inlineColumns`, a form view's `subforms[].columns`, and an `object-master-detail-form` page block's `details[].columns`. The first two were judged; the third was `z.array(z.unknown())`. - -**`@objectstack/spec`** - -- **`ComponentPropsMap['object-master-detail-form'].details`** is now an array of strict detail entries: `childObject` (required), `relationshipField`, `columns`, `formFields`, `inlineMode` (`grid` | `form`), `amountField`, `sortField`, `totalField`, `title`, `minRows`, `maxRows` and `addLabel` — the keys the console's `MasterDetailForm` reads off an entry. An unknown key is named, with a rename for the near-misses a form view's `subforms[]` entry also answers (`foreignKey` → `relationshipField`, `object` → `childObject`, …). -- **`details[].columns`** references `InlineGridColumnSchema`, the strict, name-keyed column the other two carriers take. Every rule the column schema holds applies here too, with its own message: an unknown key is named; the retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`; a column without `name` is refused; `scale` on a column declaring `type: 'currency'` is refused with the currency ruling's remedy. Page-component `properties` is read by the component-props gate, so `objectstack validate`, `build` and `lint` report these as advisory `component-props-unknown-key` / `component-props-invalid` findings; a stored page still saves and loads, because `properties` is not parsed on the metadata save or load path. -- **`defineStack`'s cross-reference check** now reaches the block wherever a page carries it (a region, a container's children, a slot) and judges a detail column that declares no `type` as the type it renders, as it already does on the other two carriers: an identity-only column over a `currency` field of the entry's `childObject` that carries `scale` is refused with the column schema's own message (`STACK_CROSS_REFERENCE_INVALID`, 422). A child object the stack does not declare, a column naming no field of it, and a column the column schema refuses on its own (left to the component-props gate) are not judged there. -- **New type `ObjectMasterDetailFormPropsParsed`** — the post-parse shape of `ObjectMasterDetailFormProps`. The two now differ, because a column's `readonlyWhen` / `requiredWhen` bare-string predicate normalizes to an Expression envelope at parse. - -**`@objectstack/lint`** - -- **`field-no-consumers`** reads an `object-master-detail-form` detail entry as the child collection it is: a column `name`, `amountField` and `relationshipField` credit the field of the entry's `childObject`, `totalField` the parent's, and an entry with no `columns` credits the columns the child derives. A child field drawn only by a master-detail block's grid was reported inert. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `details: [{ title: 'Lines' }]` | `details: [{ title: 'Lines', childObject: 'invoice_line' }]` | -| `details: [{ childObject: 'invoice_line', columns: ['product', 'quantity'] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'product' }, { name: 'quantity' }] }]` | -| `details: [{ childObject: 'invoice_line', columns: [{ field: 'quantity' }] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'quantity' }] }]` | -| `details: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency', scale: 2 }] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency' }] }]` | -| `columns: [{ name: 'amount', scale: 2 }]` where `amount` is a `currency` field of the entry's `childObject` | `columns: [{ name: 'amount' }]` | -| a detail entry or column carrying a key its shape does not declare | the entry or column without that key | - -The one-line fix: give every detail entry its `childObject`, write each column as `{ name, … }` using only the keys a relationship field's `inlineColumns` accepts, and delete `scale` from any column that renders as a currency column, whether it declares `type: 'currency'` or takes it from a `currency` child field. Nothing replaces `scale` there: the currency's ISO 4217 minor unit decides the displayed decimals. - -## Who is affected, measured - -On `origin/main` `ebdb6f2aca`: one authored `object-master-detail-form` block in the examples (the showcase project workspace, one entry `{ title, childObject, addLabel }`, no columns), which parses unchanged, and one documentation example whose three bare-string columns are rewritten as `{ name }` columns in this change. Zero `field`-keyed detail columns. Deployed metadata was not measured. diff --git a/.changeset/20929-field-consumers-inline-grid-columns.md b/.changeset/20929-field-consumers-inline-grid-columns.md deleted file mode 100644 index d85dcb9d292..00000000000 --- a/.changeset/20929-field-consumers-inline-grid-columns.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -`field-no-consumers` no longer calls a field inert when an inline grid column names it - -`os validate`, `os build` and `os lint` warned that a child object's field was inert ("no site of any kind names it") when the only thing naming it was an inline master-detail grid column, such as `{ name: 'quantity' }`. The warning told an author to delete a field the grid draws. A column's `name` is now read as a reference to the child object's field, on both carriers of the column: - -- a relationship field's `inlineColumns`. The field sits on the child object and its `reference` names the parent, so the column names a field of the object that declares the relationship field. The grid is drawn only when that field sets `inlineEdit`. Without it, the columns draw nothing, and the field is reported `carrier-only` with the column listed as a site a removal must clean. -- a form view's `subforms[].columns`, on the view's `form` and on every `formViews` entry. The column names a field of the entry's `childObject`, not of the object the view is bound to. - -`name` anywhere else is still a literal and never a field reference. The rule id, the `warning` severity and the finding's shape are unchanged. The message now also lists an inline grid column among the consumers, and an `inlineColumns` entry on a field without `inlineEdit` among the carriers. diff --git a/.changeset/20932-predicate-guard-comparand.md b/.changeset/20932-predicate-guard-comparand.md deleted file mode 100644 index af0178f3775..00000000000 --- a/.changeset/20932-predicate-guard-comparand.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -fix(plugin-security)!: a field the caller may not read is refused as a cross-field comparand exactly as it is refused as a filter key (#20932) - -Clause-②: no (narrowing) - - - -**BREAKING for queries that compare a column against a field the caller may not read.** - -**What changed.** The security layer refuses a query that filters, sorts, groups or aggregates by a field the caller's field-level permissions hide, with `403 PERMISSION_DENIED`: which rows answer would disclose the value that the field mask withholds from the result. A cross-field comparand (`FieldReferenceSchema`, "compare against another column of the same row") reads the field it names in the same way, and it is now collected into the same set and judged by the same rule. A hidden field named as a comparand, in any position the filter grammar admits for one, in `where`, `having` or a per-aggregation `filter`, answers the same `403 PERMISSION_DENIED`, in the same words, as the same field written as a filter key. This covers `engine.find`, `findOne`, `count`, `aggregate` and the bulk `update` / `delete` predicate, and every route that reaches them. Before, such a comparison was answered. - -**What is not affected.** A comparand naming a field the caller may read answers as before. A system context, and a caller with no permission sets, are unaffected. Row-level policies may still compare against fields the caller cannot read, because they are applied after the guard. A comparand the filter grammar refuses is still refused; when it names a hidden field, that refusal may now be the `403` rather than `400 INVALID_FILTER`, as it already was for a hidden filter key. - -**If a query stopped answering for some users,** it compares against a field those users may not read. Grant that field's read permission to the users who need it, or compare against a field they can read. diff --git a/.changeset/20933-analytics-relationship-path-admission.md b/.changeset/20933-analytics-relationship-path-admission.md deleted file mode 100644 index a737f05eea8..00000000000 --- a/.changeset/20933-analytics-relationship-path-admission.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: an object an analytics query reads through a relationship path is admitted and row-scoped exactly as a declared join to it is, on both strategies (#20933) - -Clause-②: no (narrowing) - - - -**BREAKING for analytics queries that read a related object through a relationship path the cube does not declare: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube.** - -**What changed.** The analytics door admits and row-scopes one object set -before either strategy runs. It held the cube's base object and the joins the -cube declares (`joins`, or a dataset's `include`). An object reached through a -relationship path the cube does not declare was not in it, although both -strategies read that object: a dotted member of an inferred cube, an authored -member whose `sql` walks a relationship the cube's `joins` does not list, or a -dotted member the query names itself. Every such object is now in the set, so -`POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql` and -`POST /api/v1/analytics/dataset/query` treat it exactly as a declared join: - -- a related object the caller may not read answers `403 PERMISSION_DENIED`, - naming that object, before any statement runs; -- the caller's row scope on the related object is applied, so related rows - outside it are not read. On the native-SQL strategy a base row whose related - record is outside the scope drops out of the answer, as it already did for a - declared join; the ObjectQL strategy still groups such rows as restricted; -- a related-object scope the native-SQL strategy cannot compile routes the - query to the ObjectQL strategy, as it already did for a declared join. - -Each hop of a multi-hop path is judged on its own object, resolved the way the -field-level gate resolves it: the join the cube keys by the path, or else the -relationship name itself. - -**What is not affected.** A query through a related object the caller may read -answers as before, within the caller's row scope. A system context, and a -caller with no permission sets, are unaffected, as on the data API. A -deployment with no security service applies no object-level check, as on the -data API. - -**Refusals that change form.** On the ObjectQL strategy a related object the -caller may not read was already refused on `POST /api/v1/analytics/query` and -`POST /api/v1/analytics/dataset/query`, though `POST /api/v1/analytics/sql` -printed the statement; on those two doors it now answers the analytics door's -refusal rather than the engine's, the same one a declared join gets. A filter, -a time window or a two-hop path through such an object moves from -`400 INVALID_FIELD` to that `403`. A relationship path whose relationship name -is not itself an object name was never served by either strategy; for a caller -the object-level check applies to, it now answers `403 PERMISSION_DENIED` -naming that relationship. - -**If a widget stopped answering for some users,** it reads a related object -those users may not read. Grant read access on that object to the users who -need it, or build the widget on objects they can read. diff --git a/.changeset/20935-analytics-masked-field-not-queryable.md b/.changeset/20935-analytics-masked-field-not-queryable.md deleted file mode 100644 index 6442bc50669..00000000000 --- a/.changeset/20935-analytics-masked-field-not-queryable.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: a field the caller is served masked is refused as a group key, an aggregate input, a filter or a sort key on every analytics face, whichever strategy serves the cube (#20935) - -Clause-②: yes (narrowing) - - - -**BREAKING for analytics queries on a SQL deployment that group, aggregate, filter or sort by a field the caller may only see masked.** - -**What changed.** The field-level gate on `POST /api/v1/analytics/query`, -`POST /api/v1/analytics/sql` and `POST /api/v1/analytics/dataset/query` judged -each member by the caller's readable fields. A field whose `maskingRule` applies -to the caller is readable (its values are served masked), so the gate admitted -it, and the native-SQL strategy then grouped or filtered by the stored value. -The gate now also asks which fields the caller may query on, and refuses a -member naming a masked field with `403 PERMISSION_DENIED`, in the words the -engine uses for the same field. The ObjectQL strategy and the data API already -refused these queries. - -**What is not affected.** A caller who holds the capability that lifts a -field's masking rule queries the field as before. A system context is -unaffected. A query that names no masked field answers as before. - -**New hook.** `AnalyticsServiceConfig.getQueryableFields(object, context)` -supplies the answer. `AnalyticsServicePlugin` wires it to the `security` -service's `getQueryableFields`. When that service predates the method, or -answers "no answer", the plugin treats every field that declares a -`maskingRule` as not queryable, for every caller. A host that -constructs `AnalyticsService` itself with `getReadableFields` and without -`getQueryableFields` is warned once at construction. - -**If a widget stopped answering for some users,** it groups or filters by a -field those users see masked. Give the users who need it the capability the -field's `requiredPermissions` names, or build the widget on fields they can query. diff --git a/.changeset/20935-plugin-security-queryable-fields.md b/.changeset/20935-plugin-security-queryable-fields.md deleted file mode 100644 index 669d718c86a..00000000000 --- a/.changeset/20935-plugin-security-queryable-fields.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -The `security` service implements `getQueryableFields(object, context)` (#20935). A field is in the answer exactly when a query naming it as a filter, a sort key, a group key or an aggregate input passes the engine's field guards: the answer is read from the one field map the predicate guard and the aggregate-input guard now share (permission sets, field grants, the `requiredPermissions` check, the on-behalf-of delegator intersection, and every field whose masking rule applies to the caller). The two guards refuse exactly what they refused before. diff --git a/.changeset/20935-security-service-queryable-fields.md b/.changeset/20935-security-service-queryable-fields.md deleted file mode 100644 index 059381101e2..00000000000 --- a/.changeset/20935-security-service-queryable-fields.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -`ISecurityService` (`@objectstack/spec/contracts`) gains an optional `getQueryableFields(object, context)`: the field names field-level security lets the caller filter, sort, group or aggregate by on the object, the query-side twin of `getReadableFields` (#20935). - -Clause-②: yes (widening) - -- It is the exact complement of the fields the engine's field guards refuse when a query names them as a filter, a sort key, a group key or an aggregate input. It is a subset of `getReadableFields`, and the two differ by exactly the fields the caller is served masked: a field whose `maskingRule` applies to the caller is readable (served, its value replaced) and not queryable. -- It fails soft like `getReadableFields`: `undefined` means no answer, `[]` means no field is queryable. A system context gets every field. -- It is optional. A consumer checks `typeof svc.getQueryableFields === 'function'`. When the method is missing, or answers `undefined`, the consumer must treat every field that declares a `maskingRule` as not queryable, whoever the caller is. Falling back to `getReadableFields` alone would admit exactly the masked fields. diff --git a/.changeset/20937-record-related-row-placement.md b/.changeset/20937-record-related-row-placement.md deleted file mode 100644 index c462b1eb33c..00000000000 --- a/.changeset/20937-record-related-row-placement.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the `record_related` action location's docblock names its placement, each row of a related list inside a parent record, instead of "a related list section" (#20937) - -Clause-②: no - -Only the `record_related` line of the `ACTION_LOCATIONS` docblock in `src/ui/action.zod.ts` changes. It now states the contract a renderer implements: a per-row action on each row of a related list shown inside a parent record, in that parent's context only. Unlike `list_item`, which surfaces on every row wherever the object is listed, it never surfaces on the object's own list views. The old words, "actions on a related list section", could be read as the section's toolbar. `ACTION_LOCATIONS` and `ActionLocationSchema` are unchanged: the same six values parse, and no `.describe()` string, export or runtime behaviour moves. The console's placement of `record_related` actions on related-list rows ships separately. diff --git a/.changeset/20943-cube-member-sql-column-reference.md b/.changeset/20943-cube-member-sql-column-reference.md deleted file mode 100644 index 322fb78a783..00000000000 --- a/.changeset/20943-cube-member-sql-column-reference.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-analytics': patch ---- - -feat(spec)!: an analytics cube member's `sql` is a column reference — a SQL expression there is refused at parse, and a derived value is declared on an ADR-0021 dataset (#20943) - -Clause-②: yes (narrowing) - -**BREAKING** — shipped as `minor` under the launch-window convention -(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by -this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, -never by the level). - -`MetricSchema.sql` and `DimensionSchema.sql` — the `sql` of every member in an -analytics cube's `measures` and `dimensions` — admit a column reference only: a -field of the cube's object (`amount`), a relationship path of bare identifiers -ending in one (`account.amount`, `account.owner.region`), or `'*'` for a count. -Any other value — a `CASE WHEN …`, an aggregate or a ratio of aggregates, a quoted -or `$`-prefixed spelling, an empty string — is refused at parse with a -prescription. This is ADR-0021's "zero raw SQL / zero raw expressions" carried -from the dataset layer to the cube members it compiles to (maintainer ruling D on -the card): an expression names no single field, so no platform check can judge -which fields it reads, and the two analytics strategies never agreed on it — the -raw-SQL path ran it verbatim and the ObjectQL path refused it. The rule is a -`pattern` in the published JSON Schema too, so a document validated against -`json-schema/**` is judged as the parse judges it. - -## FROM → TO - -A derived value moves to an ADR-0021 dataset over the same object. A conditional -count or sum is a dataset measure with its own structured `filter`; a ratio, sum, -difference or product of measures is `derived: { op, of: [...] }` over measures -named in the same dataset. - -``` -FROM defineCube({ name: 'delivery', sql: 'task', measures: { - done_rate: { label: 'Done Rate (%)', type: 'number', - sql: "SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)" }, - } }) - -> parsed; the expression ran verbatim on one strategy and was refused on the other -TO -> ZodError at measures.done_rate.sql (invalid_format): `measures..sql` is a - column reference: a field of the cube's object (`amount`), a relationship path ending - in one (`account.amount`), or `'*'` for a count. A SQL expression there was retired … - - defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', - dimensions: [/* … */], - measures: [ - { name: 'task_count', aggregate: 'count' }, - { name: 'done_count', aggregate: 'count', filter: { status: 'done' } }, - { name: 'done_rate', derived: { op: 'ratio', of: ['done_count', 'task_count'] }, format: '0.0%' }, - ] }) -``` - -**Mind the scale.** A `derived` ratio is a 0–1 fraction. An expression that -multiplied by 100 returned percentage points; pair the ratio with a `%` numeral -pattern (the server marks a ratio column's percent scale as a fraction) and -re-check any consumer that read the old number raw. - -**A dimension that bucketed a column with a CASE expression** has no expression -form in the cube layer or the dataset layer: group by the column itself, or keep -the bucket as a field of the object and name that field. - -**The one-line fix:** parse each cube; every refusal at `…sql` is one member to -move — replace it with the column it aggregates, or move the derived value to a -dataset measure as above, and point the dashboards, reports and queries that named -`.` at the dataset measure. - -**What an author who still writes it sees.** `CubeSchema`, `defineCube()`, -`defineStack({ analyticsCubes })` (`STACK_SCHEMA_INVALID` / 422) and the -`analytics_cube` write door refuse the member at its `sql` path with the -prescription. `tsc` does not: the key's type is still `string`. - -## The retirement kit - -- **Schema.** `MetricSchema.sql` / `DimensionSchema.sql` carry the pattern and - their prescriptions (`data/analytics.zod.ts`). A column reference parses - byte-identically to before. The retired metric `filters` guidance and the - analytics query's `filters` guidance no longer offer "fold the condition into - the metric's own `sql` expression" as a live channel; the `metric-filters-removed` - conversion summary and its D3 entry and step-18 rationale fragment say the same. -- **ADR-0087.** The D3 entry `cube-member-sql-expression-retired`, with its - step-18 rationale fragment. No D2 conversion — an expression has no mechanical - rewrite into a dataset — and no `RETIRED_KEYS_BY_MAJOR` row: no key left the - shape, so the authorable-surface, api-surface and JSON-schema manifest - ratchets are unchanged. -- **Liveness.** The `analytics_cube` ledger rows `measures.sql` and - `dimensions.sql` stay `live`, re-verified, with the narrowing recorded. -- **Docs.** The `data/analytics` reference page is regenerated. -- **Example.** The showcase cube's `done_rate` expression member moves to the - `showcase_task_metrics` dataset as `done_count` (a count filtered on - `status: 'done'`) and `done_rate` (`ratio` over `done_count` and `task_count`, - format `0.0%`). -- **`@objectstack/service-analytics`** (README only): its query-body section no - longer tells a reader to fold a per-metric condition into the metric's own - `sql` expression. The runtime is unchanged: its expression branches remain for - a cube that reaches the service without meeting the parse, and their deletion - is a separate change. - -## Reach, measured - -- This repository: one authored expression member (the showcase `done_rate`), - moved here. Test fixtures in `@objectstack/service-analytics` that build - expression members WITHOUT the parse keep exercising the runtime's expression - branches, unchanged. -- Out-of-repo authored cubes: NOT MEASURED. - - diff --git a/.changeset/20946-by-name-flow-read-shipped-name.md b/.changeset/20946-by-name-flow-read-shipped-name.md deleted file mode 100644 index 7fa68340e6d..00000000000 --- a/.changeset/20946-by-name-flow-read-shipped-name.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): the by-name read of a flow name a managed package ships serves the package's flow, as the flow list does (#20946) - -Clause-②: no - -`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. The flow list, `GET /api/v1/meta/flow`, and the execution view the automation engine binds flows from already serve the package's flow for a name a managed package ships (#20913). The by-name read, `GET /api/v1/meta/flow/:name`, did not: for such a name it served a stored flow of that name, marked as the package's flow. So the two read doors answered two different flows for one name. - -The by-name read now applies the same rule the list applies, through the same checks. For a name a managed package ships, it serves the package's flow, with or without a package scope, whatever the stored flow's own package binding or markings say. - -The stored flow is not deleted, rewritten or refused. It stays in the store, and the automation engine still reports it as a shadowed definition at startup. Pending drafts, flow names no managed package ships, organization-scoped rows and every other metadata type are read as before. diff --git a/.changeset/20951-inline-grid-derived-columns.md b/.changeset/20951-inline-grid-derived-columns.md deleted file mode 100644 index b50622e7602..00000000000 --- a/.changeset/20951-inline-grid-derived-columns.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': patch ---- - -`deriveInlineGridColumns` (`@objectstack/spec/data`) derives the default columns of an inline master-detail grid, and `field-no-consumers` stops calling two kinds of in-use child field "inert" (#20951). - -Clause-②: yes (widening) - -- **`@objectstack/spec`.** New exports from `@objectstack/spec/data`: `deriveInlineGridColumns(def, { relationshipField?, exclude?, maxColumns? })`, its element type `DerivedInlineGridColumn`, and `DEFAULT_MAX_INLINE_GRID_COLUMNS` (`6`). The function answers which child fields an inline grid draws when its author listed no columns: a relationship field with `inlineEdit` and no `inlineColumns`, or a `subforms` entry with no `columns`. It returns identity-only entries (`{ name }`, plus `defaultHidden: true` on columns past the visible budget, which collapse into the column chooser and are never dropped), in the child's field order, skipping system, audit, tenancy, ownership and sort-position names, the relationship field, `system` / `readonly` / `hidden` fields and the types a grid cell cannot edit. It is the renderer's current rule, reproduced exactly; the renderer hydrates each column from the child field. No schema accepts anything new or refuses anything new. -- **`@objectstack/lint`.** `field-no-consumers` now reads a `subforms` entry's `amountField` and `relationshipField` against the entry's `childObject`, and keeps `totalField` on the parent. It also credits the columns of a derived inline grid through `deriveInlineGridColumns`. Before, `os validate` warned that the child's summed amount column, the subform's relationship field and every derived grid column were inert, and credited a same-named parent field in the amount column's place. A field the derivation leaves out (for example a `hidden` one) is still reported. diff --git a/.changeset/20958-dimensionless-multi-measure-refused.md b/.changeset/20958-dimensionless-multi-measure-refused.md deleted file mode 100644 index f2ef0756a97..00000000000 --- a/.changeset/20958-dimensionless-multi-measure-refused.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec)!: a dashboard widget with no dimension declares two or more measures only on a type that renders them — `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / `sankey` are refused at `values` (#20958; objectui#8894 ruling D's principle) - -Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface GAINS two exports: the one constant the rule reads, `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`, and the check itself, `checkDashboardWidgetDimensionlessMeasureArity`, exported so objectui's `.shape` mirror can chain it. - - - -**BREAKING** accept-set narrowing at `dashboard.widgets[].values`, shipped as -`minor` under this repo's launch-window convention for breaking changes -(`check-changeset-no-major` refuses `major` while the window is open, so -breaking-ness is carried by this banner and by the ADR-0087 disposition above, -never by the bump level). The prescription is registered under protocol major 18 -as `dashboard-widget-dimensionless-multi-measure-refused`. - -**What was wrong.** Outside the metric family, `DashboardWidgetSchema.values` -(`z.array(z.string()).min(1)`) had no upper bound. Measured on this tree before -the change: `{ type: 'pie', dataset: 'sales', values: ['a', 'b'] }` with no -`dimensions` parsed through `DashboardWidgetSchema`, and so did `donut`, -`funnel`, `scatter`, `radar`, `treemap` and `sankey` — while `bogusProp` on the -same widget was refused by name, the lit control. After it, the same body is -refused at `defineStack`, at `os validate` (which loads through `defineStack`), -and on the metadata save path (`422 INVALID_METADATA`, active and draft). With -nothing to split by, those seven types draw `values[0]`: every measure after it -is queried and dropped on the floor by the renderer. The maintainer's ruling D -(「协议不正确的应该先修改协议」) fixes the protocol where it admits measures a -widget type cannot render; the metric-family narrowing was its first -application, and this is the same principle on the chart types. - -### Write instead - -| wrote | write instead | -|---|---| -| `{ id: 'mix', type: 'pie', dataset: 'sales', values: ['amount_sum', 'count'] }` (no `dimensions`) | `{ id: 'mix', type: 'table', dataset: 'sales', values: ['amount_sum', 'count'] }` — a row of measures | -| the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure | -| the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['amount_sum'] }` **and** `{ id: 'mix_count', type: 'pie', …, values: ['count'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) | - -No conversion does this for you: whether a dimensionless two-measure pie meant a -table, a bar chart or two pies is an authoring choice. The refusal lands at -`widgets[N].values` as ONE `custom` issue naming the widget's `id`, the number -of measures and the authored `type`, and it lists the types that do render -several measures on a dimensionless widget, read from -`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` (`table`, `pivot`, `bar`, `column`, -`horizontal-bar`, `line`, `area`, `combo`). That constant is the one list — the -check, the refusal text and the `values` doc string read it, and objectui's -mirror is to import it rather than restate it. A type that later gains a -declared multi-measure rendering joins it with no migration. - -**Nothing else moves.** The seven types WITH a dimension, and with one measure, -parse exactly as before; every type in the multi-measure set keeps accepting -any number of measures with no dimension; the metric family's refusal is -unchanged and still ONE issue (this check steps aside for `metric` / `kpi` / -`gauge` / `solid-gauge` / `bullet` and for a typeless widget, which resolves to -`metric`); an empty `values` keeps its `too_small`; a `type` outside -`ChartTypeSchema` reports the type refusal alone. Census at the branch point -(`05be35259`), every tracked `.ts` / `.tsx` / `.js` / `.json` / `.md` / `.mdx`: -23 widget literals on the seven types, every one with one dimension and one -measure, and 0 dimensionless multi-measure widgets on them; the same scan over -an objectui checkout (`1263e40`) reads 0 as well. diff --git a/.changeset/20960-widget-options-census.md b/.changeset/20960-widget-options-census.md deleted file mode 100644 index 99f9a25ec11..00000000000 --- a/.changeset/20960-widget-options-census.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the `DashboardWidgetOptionsSchema` doc comment states which widget `options` keys a renderer reads, instead of naming presentation extras (`icon`, `trend`, `columns`, `striped`, `density`) it called renderer-understood - -Clause-②: no — no key is declared and no value is typed, so the accept set is unchanged. - -`options` still parses any key. A widget always binds a `dataset`, so it renders through -objectui's dataset-bound path, and that path reads only the five declared keys -(`dateGranularity`, `sortBy`, `sortOrder`, `limit`, `stageOrder`) and the `description` -sub-caption. Any other key parses and renders nothing. To format a number, set `format` and -`currency` on the dataset measure. To accent a tile, set the widget's `colorVariant`. To style -a chart, set the widget's `chartConfig`. diff --git a/.changeset/20963-not-null-hint-declared-column.md b/.changeset/20963-not-null-hint-declared-column.md deleted file mode 100644 index f54161e343a..00000000000 --- a/.changeset/20963-not-null-hint-declared-column.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the `hint` on a NOT NULL refusal leads with the remedy for a column that requires a value, and names schema drift only as a condition, so an author who declared `storage: { notNull: true }` is no longer sent to `os migrate` (#20963) - -Clause-②: no - -A field that declares `storage: { notNull: true }` without `required` is a column -the database keeps NOT NULL on purpose (ADR-0113). A write with no value for it, -through `POST /api/v1/data/:object` or `PATCH /api/v1/data/:object/:id`, is -refused by the database and answers `400 VALIDATION_FAILED` with a `required` -finding for the field. That answer was right. Its `hint` said the field is -optional in the metadata and "the physical schema has drifted from metadata", -and told the caller to run `os migrate`, which changes nothing for a column -declared NOT NULL. - -The `hint` now reads: "The database column for 'FIELD' requires a value: provide -it, or declare the field `required` in the object metadata. If the object -declares neither `required` nor `storage: { notNull: true }` for 'FIELD', the -physical schema has drifted from metadata instead: run 'os migrate' to reconcile -(or reset the dev database)." The first sentence holds for every NOT NULL -refusal. The second names the drift case under the condition that makes it drift. - -**What is not affected.** The status, `code`, `error`, `fields` and `object` of -the answer are unchanged, and no key is added. The import row is untouched: it -does not carry a `hint`. Nothing reads the field map to tell the two cases -apart, so the `hint` stays advice for the author to read against the object's -declaration. diff --git a/.changeset/20964-approval-snapshot-masked-field.md b/.changeset/20964-approval-snapshot-masked-field.md deleted file mode 100644 index 8fdc17c93da..00000000000 --- a/.changeset/20964-approval-snapshot-masked-field.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-approvals': minor ---- - -fix(approvals): a snapshot field the reader is served masked on the data plane is no longer served as stored (#20964) - -Clause-②: yes (widening) - -The approval payload snapshot is redacted at serve time by the security service's read projection, `getReadableFields`. That projection counts a field whose `maskingRule` applies to the reader as readable, because the data plane serves the column with its value replaced. So the snapshot kept such a field and served it as captured at submission. The redaction now also reads the security contract's `getQueryableFields`, which differs from the read projection by exactly the fields the reader is served masked, and drops those fields, with their derived labels, on both read doors: the approvals inbox reads and the generic data door on the request object. A reader for whom the masking rule is lifted still sees the stored value. The full snapshot stays at rest. - -The field is dropped rather than masked. The contract names which fields are masked for a reader, not the masked value, and reproducing the mask in this plugin would be a second copy of the masking rule. - -The one public-surface addition is an optional `getQueryableFields(object, context)` member on the field-visibility source that `ApprovalServiceOptions.fieldVisibility` and `ApprovalService.attachFieldVisibility` accept. - -A host that constructs `ApprovalService` itself and passes its own `fieldVisibility` source: that source must now also answer `getQueryableFields` (delegate it to the `security` service). A source without it cannot say which readable fields are masked for the reader, so the redaction fails closed and serves no snapshot field. The approvals plugin's own wiring already forwards it. diff --git a/.changeset/20965-analytics-expression-member-refused.md b/.changeset/20965-analytics-expression-member-refused.md deleted file mode 100644 index ab604553d81..00000000000 --- a/.changeset/20965-analytics-expression-member-refused.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -fix(service-analytics): the analytics field-level read gate refuses a cube member whose `sql` names no field, instead of letting the query run (#20965) - -Clause-②: no - -**What changed.** Where the analytics field-level read gate judges a cube's -object (a security service is registered and gives a field answer for that -object), a query that names a cube member whose `sql` is neither a column -reference (a field of the cube's object, or a relationship path ending in one) -nor `'*'` is now refused `403 PERMISSION_DENIED` on -`POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql`, before either -strategy runs. Whatever -the caller may read, the member is refused. That covers an expression member -of a cube that reached the service without the spec's parse (the cube -registry never parses: `analyticsCubes` and `AnalyticsServicePlugin({ cubes })` -arrive as written), a declared member with no `sql` string, and a member the -query names itself that is not a column reference. The gate used to stand down -on such a member, because it names no field, and the native-SQL strategy then -compiled it into its statement as written: a read of fields no permission -verdict was reached for. The refusal names the member and the object, and -never the member's `sql`. - -**What is not affected.** A member that is a column reference is judged by the -field it resolves to, as before. A `count` over `'*'` names no field and is -served. A deployment with no security service, and an object the security -service gives no field answer for, apply no field-level check, as before. The -spec's parse already refuses an expression member, so a cube that parses is -unaffected. - -**If a widget stopped answering,** its cube carries an expression member from -before the parse refused one. Re-author the member as a column reference, or -declare the derived value on an ADR-0021 dataset: a conditional count or sum -is a dataset measure with its own `filter`, and a ratio of measures is -`derived: { op: 'ratio', of: [...] }`. diff --git a/.changeset/20970-migration-entries-readme-probe.md b/.changeset/20970-migration-entries-readme-probe.md deleted file mode 100644 index a08352ccf48..00000000000 --- a/.changeset/20970-migration-entries-readme-probe.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): the shipped `src/migrations/entries/README.md` "Reproduce any row" recipe now fetches both commits into the driver-less bare clone before `merge-tree`, and reads exit 1 with no tree id as a missing object, never a conflict (the old `--shared --no-local` form misread a clean pair as conflicted) (#20970) - -Clause-②: no diff --git a/.changeset/20980-book-tree-orphans-scoped.md b/.changeset/20980-book-tree-orphans-scoped.md deleted file mode 100644 index 25640ad065d..00000000000 --- a/.changeset/20980-book-tree-orphans-scoped.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): a book tree's synthetic *Uncategorized* group holds only the unplaced docs of the book's own packages, per ADR-0046 §6.4 - -Clause-②: no - -- `resolveBookTree` used to put every unclaimed doc it was handed into the book's *Uncategorized* group. `GET /api/v1/meta/book/:name/tree` resolves over every doc in the environment, so a book's tree listed every other package's ungrouped docs there. The docs portal never showed those docs in the book. -- The group now holds a doc only when it belongs to one of the book's packages: the package that ships the book, or a package a group names with `package`. A doc with no stamped package still counts as the book's. A doc of another package stays reachable through its own package's book. -- The implicit per-package book that the tree route serves for a package id catches no other package's docs either. -- Unchanged: a doc whose `group` names one of the book's groups joins that group from any package. A book that declares no package keeps every unclaimed doc in *Uncategorized*. The docs a book claims, and so every doc's audience, do not change. diff --git a/.changeset/20981-aggregation-flag-comparand-refused.md b/.changeset/20981-aggregation-flag-comparand-refused.md deleted file mode 100644 index c93e98e3b04..00000000000 --- a/.changeset/20981-aggregation-flag-comparand-refused.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/objectql': minor ---- - -fix(objectql)!: a per-aggregation `filter` and a `having` refuse a non-boolean `$exists` / `$null` with `INVALID_FILTER` / 400, in the words every driver's `where` refuses it in, instead of reading `$exists` by truthiness and dropping `$null` (#20981) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `aggregate` accepts in two positions, `aggregations[i].filter` and `having`, on every driver. A `$exists` or `$null` comparand that is not a boolean (a string such as `"false"`, a number, `null`, an array) is now refused with `INVALID_FILTER` / 400, before any driver is asked for a row, so an empty table refuses it too, at any depth under `$and` / `$or` / `$not`. A plain object or `undefined` there is refused first by the comparand-type check, in its own words, as before; a `{ $field }` reference there, already refused as a reference outside a scalar comparison, is now refused in this entry's words. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way, per row: it throws the same refusal for a row its per-aggregation filter judges on the flag, with or without a `fields` map (an empty `rows` array, or a row a `$or` branch settles first, is not judged there; `engine.aggregate` judges the whole filter once before any row). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**Why a refusal.** `FieldOperatorsSchema` declares both flags as booleans, and every driver's `where` refuses any other comparand. The engine evaluates a per-aggregation `filter` and a `having` itself, and it read one anyway. Measured through `engine.aggregate` on the in-memory driver and on `SqlDriver` (SQLite), with identical answers: `$exists` was read by truthiness, so `"yes"`, `1` and the string `"false"` selected the rows and groups WITH a value, and `0` / `null` the ones without; and `$null` tested only `true` / `false`, so any other value constrained nothing, and every row and every group came back. - -**What an author sees now.** The message `driver-sql` gives the same flag, beginning `Operator "$exists" on field "FIELD" requires a boolean comparand (true or false).`, naming what arrived and the position (`aggregations[1].filter.stage.$exists`, `having.stage.$null`). Unlike a `where` on `SqlDriver`, the field and the value are not withheld: a per-aggregation `filter` and a `having` never carry a merged read scope. - -**What to write instead.** Write the boolean itself. `"$exists": true` and `"$null": false` match a field that has a value; `"$exists": false` and `"$null": true` match one that has none. - -**Who is affected.** A caller that reaches `engine.aggregate` without the REST query door's schema parse (server-side code, a flow or hook, the analytics bridge that lowers a dataset measure's filter into an aggregation filter, a host calling `applyInMemoryAggregation` directly) and read the count as a real answer. `POST /api/v1/data/:object/query` already refused all three positions with 400 `VALIDATION_FAILED` before the request reached the engine, and still does. - -**Unchanged.** `$exists: true` / `false` and `$null: true` / `false` answer exactly as before, on both positions. `$empty` and every other operator, and `where`. diff --git a/.changeset/20986-analytics-hop-object-reference.md b/.changeset/20986-analytics-hop-object-reference.md deleted file mode 100644 index 0cef1387e15..00000000000 --- a/.changeset/20986-analytics-hop-object-reference.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: a relationship-path hop the cube declares no join for reads the object its lookup field declares, so an inferred cube's dotted path through a lookup named differently from its target is answered - -Clause-②: yes (narrowing) - - - -**BREAKING**: this widens what the analytics query doors answer for a dotted relationship path the cube declares no join for — an inferred cube's dotted member (`owner.region`), or an authored member whose `sql` walks a relationship its `joins` does not list — and narrows it in one case, named below. It holds on `POST /api/v1/analytics/query` and on its dry run `POST /api/v1/analytics/sql`, on both strategies and every SQL driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What an author sees now.** Each hop of the path reads the object its lookup field declares as its target, the way a join the cube declares already did. With a lookup `owner` that references a person object: - -- the caller may read the person object: `dimensions: ['owner.region']` is answered with the person rows' regions on both strategies, and `where: { 'owner.region': 'NA' }` is answered on the native-SQL strategy with what the nested form `{ owner: { region: 'NA' } }` answers. The engine-aggregate strategy keeps refusing a filter on a related value with its own `400 INVALID_FIELD`, as it does through a declared join; -- the caller may not read the person object: `403 PERMISSION_DENIED` naming the person object, before any statement runs; -- the field-level gate judges `region` on the person object, and the caller's row scope on the person object is applied where the related value is read (the join on the native-SQL strategy, the related read on the engine-aggregate one). - -A lookup to the cube's own object (a self-reference such as `parent`) is read the same way. A lookup named after its target answers exactly as before. - -**Why.** An inferred cube declares no join, so a hop fell back to an object named after the lookup field. For a lookup named differently from its target that is no object: a caller who may read both objects was refused `403` "reading "owner" is not permitted", and a caller the object check passes reached a statement over a table named `owner` (`500`). - -**The narrowing.** A lookup whose name is ALSO the name of another object — a field `account` referencing `crm_account` while an object `account` exists — used to be read from that other object: joined by the ids of the records the field points to, admitted and scoped as that other object. It now reads its declared target. So that path answers from the target's rows, and a caller who may not read the target is refused `403 PERMISSION_DENIED` naming it, where the query used to be answered. - -**Unchanged.** A cube that declares a join for the path keeps reading the join's object. A host that wires no `relationshipResolver` (`AnalyticsServicePlugin` always wires it, from the data engine's object schema), or a relationship field it cannot answer for, keeps reading the object named after the field. A dataset's `include` compiles to declared joins, so a path it declares is unchanged. diff --git a/.changeset/20987-analytics-contains-membership.md b/.changeset/20987-analytics-contains-membership.md deleted file mode 100644 index a5f40ca3f22..00000000000 --- a/.changeset/20987-analytics-contains-membership.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/service-analytics': minor -'@objectstack/driver-sql': patch ---- - -fix(service-analytics)!: the analytics read scope and the native `where` answer `$contains` / `$notContains` on a multi-valued or JSON-stored field by membership, with the one construct `driver-sql` emits, now exported from `@objectstack/core` (#20987) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what the analytics doors answer for one class of read. A row policy (the read scope the analytics plugin compiles from the security service, or a host's own `getReadScope`) whose `$contains` or `$notContains` names a field declared multi-valued (`multiple: true` on a multi-capable type, or a multi-option type) or JSON-stored now selects the rows holding the comparand as an ELEMENT of the stored list. It used to select every row whose stored JSON text contained the comparand as a substring, so on SQLite a policy could admit rows outside it, and on PostgreSQL every query under such a policy answered `500` (MySQL was not measured). An analytics count under such a policy now equals what the same caller reads through the data door. On a datasource whose SQL dialect the analytics host cannot name, such a policy now refuses the query (`READ_SCOPE_COMPILE_FAILED` / `500`) instead of falling back to the substring reading. It ships as `minor` under the launch-window convention. - -**The `where`.** `POST /api/v1/analytics/query`, the dataset door and `/analytics/sql` on the native strategy render the same membership test for a `$contains` / `$notContains` in a query's `where` (or a dataset's `runtimeFilter`) on such a field: on PostgreSQL the query answers rows where it answered `500`, and on SQLite the count stops over-counting (`$contains`) and under-counting (`$notContains`). On a datasource whose dialect the host cannot name, the operator on such a field is refused `INVALID_FILTER` / `400`. The ObjectQL strategy already answered membership and is unchanged. - -**Unchanged.** On a scalar text field `$contains` stays the substring test, on every face. `$notContains` keeps its NULL rule: a row with no value satisfies it. A host that wires no field metadata keeps the substring reading, because it cannot tell a JSON column from a text one; the analytics plugin wires it from the data engine. - -**New export.** `@objectstack/core` exports `jsonMembershipPredicate(dialect, emitters, value)` and `jsonMembershipCandidates(value)`, with the `JsonMembershipDialect` and `JsonMembershipEmitters` types: the per-dialect membership construct (#17590) moved from `@objectstack/driver-sql`, where it was module-private, and made placeholder-agnostic. `@objectstack/driver-sql` imports it and emits byte-identical statements and bindings. - -**What to do after upgrading.** Nothing, unless a policy or a dashboard filter relied on the substring reading of a multi-valued or JSON-stored field: such a filter now selects members only, as the data door always did. A host whose analytics `sqlDialect` hook answers nothing for a SQL datasource should answer `'sqlite'`, `'postgres'` or `'mysql'`, or the operator on such a field is refused. diff --git a/.changeset/20995-zero-set-masking.md b/.changeset/20995-zero-set-masking.md deleted file mode 100644 index 17aab4e2f8d..00000000000 --- a/.changeset/20995-zero-set-masking.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -fix(plugin-security)!: a field whose masking rule applies is served masked to a caller who resolves no permission set, and that caller may not filter, sort, group or aggregate on it (#20995) - -Clause-②: no (narrowing) - - - -**BREAKING for callers who resolve no permission set.** - -**What changed.** A field that declares `maskingRule` is masked for every -non-system caller unless the caller holds all of the field's -`requiredPermissions`. A caller who carries a principal but resolves no -permission set holds no capability, so the rule applies to it, but the runtime -served that caller the stored value and let it filter, sort, group and -aggregate on the field. That caller is now served the masked value. A filter, -sort key, group key or aggregate that names the field is refused with -`403 PERMISSION_DENIED`, as it already was for any other masked caller. A write -that sends the masked placeholder back is refused with `400 VALIDATION_ERROR`, so -a client that saves the record it was served cannot overwrite the stored value -with its mask. - -The published field answers agree with what is served. -`ISecurityService.getQueryableFields` no longer lists such a field for this -caller, so a door that compiles its own query refuses it the same way. -`getReadableFields` still lists it, because a masked field is a served column. - -**Who this reaches.** A caller who resolves no permission set but carries a -position, a named permission set or a user id. A caller with none of the three -is handed through untouched, as before, and the field projections say so. A -system context is unaffected. - -**One more refusal, by the same rule.** If the object's security posture cannot -be read, this caller's request is now refused, as every other caller's already -is. The masking rules come from that posture, so they cannot be known without -it. - -**What to do.** Nothing, unless such a caller needs the stored value. A field's -`requiredPermissions` are the gate that lifts its mask, so give the caller a -permission set that holds all of them, or drop the `maskingRule`. A query that -must sort or search on the field needs the same. diff --git a/.changeset/20997-fields-reserved-name-issue-path.md b/.changeset/20997-fields-reserved-name-issue-path.md deleted file mode 100644 index 47cdbccb5d4..00000000000 --- a/.changeset/20997-fields-reserved-name-issue-path.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `ObjectSchema.fields` refuses `constructor` and `prototype` at the offending key (`fields.constructor`, `fields.prototype`), not at the `fields` slot - -Clause-②: no - -A field map carrying a key named `constructor` or `prototype` is refused, as before. The -refusal used to be reported at the path `fields`, which names no field, so a form reading the -structured issues of a refused save could not point at the field that caused it. It is now -reported at that key, like the `__proto__` refusal (`fields.__proto__`) and the key grammar's -`invalid_key` refusal (`fields.Bad Name`) already are. A document carrying both names gets two -issues, one at each key, where it used to get one at the slot. - -Nothing else moves. The same keys are refused and the same documents are accepted. The issue -code (`custom`) and the message are unchanged. The published JSON Schema states the same ban: -it is now written as one `propertyNames` clause per name inside `allOf` instead of one clause -naming both, which accepts and refuses exactly the same documents. diff --git a/.changeset/21002-layered-flow-read-shipped-name.md b/.changeset/21002-layered-flow-read-shipped-name.md deleted file mode 100644 index 485c434f7a7..00000000000 --- a/.changeset/21002-layered-flow-read-shipped-name.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): the layered read of a flow name a managed package ships reports the package's flow as the effective layer, as the by-name read and the flow list do (#21002) - -Clause-②: no - -`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. The flow list, `GET /api/v1/meta/flow`, and the by-name read, `GET /api/v1/meta/flow/:name`, already serve the package's flow for a name a managed package ships (#20913, #20946). The layered read, `GET /api/v1/meta/flow/:name/layers`, did not: for such a name it reported a stored flow of that name as the effective layer, while its lock and provenance flags named the package. So the layered read and the other two read doors answered two different flows for one name. - -The layered read now decides the effective layer with the same check the other two doors use. For a name a managed package ships, the effective layer is the package's flow, with or without a package scope, whatever the stored flow's own package binding or markings say. The stored flow is still reported, as a separate layer of its own scope that does not take effect. The deprecated layers flag on the by-name read answers the same. - -The stored flow is not deleted, rewritten or refused. Flow names no managed package ships, organization-scoped rows and every other metadata type are read as before. diff --git a/.changeset/21002-published-door-shipped-flow.md b/.changeset/21002-published-door-shipped-flow.md deleted file mode 100644 index 05b71c0a10e..00000000000 --- a/.changeset/21002-published-door-shipped-flow.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/rest': patch -'@objectstack/runtime': patch ---- - -fix(rest,runtime): the published-snapshot read of a flow name a managed package ships answers the package's flow, as the layered read does (#21002) - -Clause-②: yes (widening) - -`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Since the previous half of #21002, the layered read, `GET /api/v1/meta/flow/:name/layers`, reports the package's flow as the effective layer for a name a managed package ships, and a stored flow of that name as a separate layer that does not take effect. The published-snapshot read, `GET /api/v1/meta/:type/:name/published`, and its runtime-dispatcher twin read that same layered answer, but served its stored layer whenever one was present. So for such a name they still answered `200` with the stored flow, not the package's. - -Both published-snapshot doors now serve the layered read's effective layer when that read put the package's flow over a stored flow, which is the package's flow. They ask the metadata protocol's own check for that decision rather than repeating it. In every other case they answer exactly as before: a flow name no managed package ships, and every other metadata type, `object` included, still answer the stored layer when one is present, and an item with no stored layer still falls through to the code/package snapshot. The stored flow is not deleted, rewritten or refused. - -**The widening.** `@objectstack/metadata-protocol` makes one existing method public: `ObjectStackProtocolImplementation.isShippedFlowName(type, name)`. It answers whether `name` is a flow name a managed package ships. It was private to the class, so a door in another package could not ask it any other way. Its answer is unchanged, and the layered read, the by-name read and the flow list keep calling it. diff --git a/.changeset/21005-action-row-endpoint-refused.md b/.changeset/21005-action-row-endpoint-refused.md deleted file mode 100644 index bbc5154a5db..00000000000 --- a/.changeset/21005-action-row-endpoint-refused.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: `action:button` / `action:icon` refuse `endpoint` with the rename `ActionSchema` already prescribes — `endpoint` → `target` (#21005) - -**BREAKING** — `endpoint` on an `action:button` or `action:icon` component (`ActionButtonProps`, `ActionIconProps`) is no longer a declared key. `ActionSchema` has always refused `endpoint` with "Did you mean `endpoint` → `target`?", while these two rows accepted it. objectui's console registers its own `api` handler, which reads `target` and never `endpoint`, so an `api` button written with `endpoint` passed the props gate and called nothing. The rows now refuse it with the same rename, read from the one alias table the action and both rows share. Write the endpoint as `target`. - -Clause-②: yes (narrowing) - -## FROM → TO - -| you wrote (17.5 and earlier) | write instead | -| --- | --- | -| `{ type: 'action:button', properties: { actionType: 'api', endpoint: '/api/v1/x' } }` | `{ type: 'action:button', properties: { actionType: 'api', target: '/api/v1/x' } }` | -| `{ type: 'action:icon', properties: { actionType: 'api', endpoint: '/api/v1/x' } }` | `{ type: 'action:icon', properties: { actionType: 'api', target: '/api/v1/x' } }` | -| `endpoint` on a block with no `actionType` | add `actionType: 'api'` and rename `endpoint` to `target` | - -**The one-line fix:** rename `endpoint` to `target` in the block's `properties`; the value (the URL the `api` action calls) is unchanged. - -**What an author who still writes it sees.** A page is never refused for it: a page component's `properties` is an open bag, so `definePage()`, `defineStack({ pages })` and the page write door accept the page as before. `os validate` / `os build` / `os lint` report `component-props-unknown-key` as a warning at `properties.endpoint`, with the rename "Did you mean `endpoint` → `target`?" — the same clause `ActionSchema` prints. The two rows also stop answering `path` with the edit-distance guess `patch` (the declarative write's field values): `url`, `endpoint`, `path` and `href` all rename to `target`, on the action and on both blocks alike. A typed `ActionButtonProps` / `ActionIconProps` input fails `tsc` at `endpoint`. - -## The migration kit - -- **The D2 conversion `action-block-endpoint-to-target`** (protocol 18, retired from the load path) renames `endpoint` to `target` on an `action:button` / `action:icon` whose `actionType` is `api`, the one meaning the key declared, with one notice per block. It reaches blocks in regions, nested in a container's `children`, and in a slotted page's named slots, so a stored `page` row or a built artifact that carries the key loads with `target` through the rehydration seams, which replay it. An already-present `target` wins: a twin with the same value is dropped. A block with no `actionType`, another `actionType`, a non-string `endpoint`, or a `target` that names a different endpoint is left as stored and reported as a TODO. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. -- **The D3 entry `action-block-endpoint-spelling-retired`** names what the rename cannot decide: the TODO sites above, and code — a custom action handler that read `endpoint` off the action reads nothing once the block carries `target`. -- **No deprecation window**, per the project's startup-stage posture. - -Census at landing: no producer in this repository (examples, templates, platform pages, fixtures) or in objectui's examples authors `endpoint` on either block. ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. - - diff --git a/.changeset/21006-translate-object-list-views.md b/.changeset/21006-translate-object-list-views.md deleted file mode 100644 index 049055d6261..00000000000 --- a/.changeset/21006-translate-object-list-views.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): an object's embedded `listViews` are served in the reader's language, from the `_views` keys `os i18n extract` already writes - -Clause-②: no - -- `GET /api/v1/meta/object` and `GET /api/v1/meta/object/:name` now translate each view in an object's `listViews`: its `label`, its `description`, and the copy of its `bulkActionDefs` (label, confirm prompt, confirm button, and each param's label, help and placeholder). They read the keys `os i18n extract` writes for those views, `objects.._views..*`, where `` is the view's key under `listViews`. For example, `sys_account`'s `mine` view is now served as `我的链接` to a `zh-CN` reader. It used to be served as the authored `My Links`. -- A view with no translation for the requested locale keeps its authored text. An object with no `_views` entries is served unchanged. -- A string that was changed after the package shipped still wins over the packaged translation, the same rule a served view document and a dashboard follow. The string is compared with the same view in the packaged object. If it differs, the changed string is served in every locale. A view the packaged object does not declare keeps all of its own strings. -- `translateObject` (and `translateMetadataDocument('object', …)`) in `@objectstack/spec/system` does the translating, so any caller of those functions gets the same result. diff --git a/.changeset/21007-aggregation-filter-json-column-refusal.md b/.changeset/21007-aggregation-filter-json-column-refusal.md deleted file mode 100644 index 3833781bcdd..00000000000 --- a/.changeset/21007-aggregation-filter-json-column-refusal.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/objectql": minor -"@objectstack/core": minor -"@objectstack/driver-sql": patch ---- - -fix(objectql)!: a per-aggregation `filter` refuses `$in` / `$nin` / `$eq` / `$ne` / an ordering / `$between` / implicit equality on a declared JSON-stored field with `INVALID_FILTER` / 400, in the words `where` refuses them in, instead of counting rows the stored arrays cannot support - -Clause-②: yes (widening) - - - -**BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts in one position, `aggregations[i].filter`, on every driver and for every caller that reaches the engine: the REST query door (`POST /api/v1/data/:object/query`), a flow or hook, and the analytics strategy that lowers a dataset measure's filter onto `engine.aggregate`. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a per-aggregation `filter` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, and before any driver is asked for a row, so an empty table refuses it too. That is the set `driver-sql`'s `where` refuses on such a column, for the same reason. - -**What an author sees now.** The same 400 body the same filter gets as a `where`: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, as they are for `where`, and the full diagnostic, naming both and the aggregation position, goes to the server log. - -**Why a refusal.** The engine evaluates a per-aggregation filter itself, and it compared the whole stored array against a scalar. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $in: ['u1', 'u9'] } }` counted 0, `{ owners: { $nin: ['u1', 'u9'] } }` counted all 6, the two rows it was asked to exclude among them, `$gt` / `$lte` / `$between` counted 4 / 1 / 5, and `{ tags: { $eq: 'red' } }` counted the row holding `['red']` by JS loose equality. The same filters in `where` were 400 on both dialects. - -**Who is affected.** A dashboard, report, dataset measure or caller whose per-aggregation filter compares a JSON-stored field with one of those operators and read the count as a real answer. Also a host calling `applyInMemoryAggregation` directly with a `fields` map: it now judges each `aggregations[i].filter` against that map before any row (an empty `rows` array included) and throws the same `INVALID_FILTER` / 400. It takes an optional fifth argument, `reportWithheld(diagnostic)`, which receives the withheld field, operator and position; without it the diagnostic is dropped. A call without `fields` judges nothing, as before. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. - -**Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored; `having`; `where`; and a host whose engine has no declaration for the object, where nothing is judged. - -**`@objectstack/core`** (three new root exports): `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, `jsonColumnOperatorRefusalText(field, op, bare)` and its return type `JsonColumnOperatorRefusalText` (`{ message, diagnostic }`). They are the operator set and the two texts (the withheld message and the full diagnostic) of the JSON-column refusal, so `driver-sql`'s `where` and the engine's per-aggregation filter refuse with one set and one sentence. - -**`@objectstack/driver-sql`**: no behaviour change. Its JSON-column gate reads the set and the text from `@objectstack/core`; every refusal it prints is byte for byte what it printed before. diff --git a/.changeset/21009-json-column-text-operators.md b/.changeset/21009-json-column-text-operators.md deleted file mode 100644 index a40f209bbc8..00000000000 --- a/.changeset/21009-json-column-text-operators.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@objectstack/core": minor -"@objectstack/objectql": minor ---- - -fix(core)!: a filter that aims `$startsWith`, `$endsWith`, `$icontains`, `$like` or `$ilike` at a field stored as a JSON column is refused with `INVALID_FILTER` / 400, as `$eq` / `$in` / `$nin` already are, instead of matching the field's serialized text or failing at query time - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows which filters are answered on a field stored as a JSON column, on every face that reads `@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`: `driver-sql`'s `where` (and `driver-sqlite-wasm` and `driver-turso`'s local transport, which inherit it) on every read and write face that lowers a filter, and the engine's per-aggregation `filter`. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What is refused.** On a field declared multi-valued (an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`) or structured-JSON (`json`, `address`, …), a filter using `$startsWith`, `$endsWith`, `$icontains`, or the staged pattern pair `$like` / `$ilike`, is refused with `INVALID_FILTER` / 400, at any depth under `$and` / `$or` / `$not`. The per-aggregation `filter` refuses the three declared ones; it already refused `$like` / `$ilike` as operators it does not evaluate. Through the engine, a structured-JSON field was already refused all seven text operators by the text-operator declared-type door, which still answers first there, in its own words; what moves for it is a direct driver call. - -**What an author sees.** The body the equality family already gets there, byte for byte: the filter WAS NOT APPLIED, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message and named in the server-log diagnostic; a filter positively marked as the caller's own reads them named. - -**Why a refusal.** Such a column stores the serialization `["u1","u2"]`, and none of these five operators has a membership reading. Measured through `POST /api/v1/data/:object/query` on a multi-value lookup and a `tags` field: on SQLite `$startsWith: "["` and `$endsWith: "]"` matched every row with a value, `$startsWith: "u1"` matched none of the rows holding `u1`, and `$icontains: "U1"` also matched the row holding only `u10`; on PostgreSQL 16 every one failed at query time with a `500` `DATABASE_ERROR`, a `json` column having no `LIKE` operator; the per-aggregation `filter` counted 0 for each. No membership reading is invented for a prefix, suffix or case-folded test. - -**Who is affected.** A saved filter, list view, dashboard widget, report or caller that aims one of these operators at a multi-valued or JSON-stored field. On SQLite it read rows that matched the stored brackets and quotes; it now gets the 400. On PostgreSQL it already failed, with a 500. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. - -**`@objectstack/objectql`: `$search` over a multi-valued field answers by membership.** The search expander (`$search` on `find`, `findOne` and `aggregate`, the REST `search` / `$search` parameter included) used to emit `$in` for a term matching a `select` option label and `$icontains` for any other term, against every field in the resolved search set. On a multi-valued field both are refused by the gate above, so one such field in the set failed the whole search: a label term answered 400 on every dialect, and any other term answered 500 on PostgreSQL and, with this change, 400 on SQLite. The auto-default set includes a `select` declared `multiple: true`, as in `examples/app-todo`'s `todo_task.tags`, and `searchableFields` may name a `tags` field or a multi-valued lookup. Such a field is now matched by membership: a term matching option labels becomes one `$contains` per matched option value, and any other term, or any term on a field with no options, becomes `$contains` of the term. No search answers 400 or 500 for it any more. **The visible cost:** to hit a multi-valued field, a term must now equal one of its members or match one of its option labels; SQLite used to match substrings of the stored array's serialized text as well, so a term like `wood` found a row tagged `redwood`, and it no longer does. Scalar fields are searched exactly as before. - -**Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored, the scalar text column included; `driver-memory`; and `driver-turso`'s remote transport, which compiles its own filters. diff --git a/.changeset/21017-quickadd-remedy.md b/.changeset/21017-quickadd-remedy.md deleted file mode 100644 index 1e2dd7efc12..00000000000 --- a/.changeset/21017-quickadd-remedy.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the `object-kanban` `quickAdd` retirement no longer sends authors to the `kanban-ui` block, which objectui does not register - -Clause-②: no - -- The refusal of `quickAdd` on `object-kanban` now ends "Delete the key; `object-kanban` offers no quick-add control." It used to say the control "is unchanged on the `kanban-ui` block". objectui retired that block (objectui#8257), so a node of that type saves clean and renders nothing. The refusal itself is unchanged: the same key is still refused, with the same code and path. -- The same sentence replaces the old one in the `os migrate meta --from 17` output (the `object-kanban-quick-add-retired` entry) and in the summary of the `object-kanban-quick-add-removed` conversion. That entry no longer offers "move the board to a host that renders the `kanban-ui` block" as a second way out. -- No author action beyond the existing one. `quickAdd: true` on an `object-kanban` is still a parse error. Delete the key. diff --git a/.changeset/21028-environments-create-clone-from-removed.md b/.changeset/21028-environments-create-clone-from-removed.md deleted file mode 100644 index 51370f2ddaa..00000000000 --- a/.changeset/21028-environments-create-clone-from-removed.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/cli': minor -'@objectstack/client': minor ---- - -fix(cli)!: `os environments create` no longer takes `--clone-from`, and the client's `environments.create` request no longer declares `clone_from_environment_id` (#21028) - -Clause-②: no (narrowing) - - - -**BREAKING for scripts that pass `--clone-from`, and for TypeScript callers that pass `clone_from_environment_id`.** - -**What changed.** `os environments create --clone-from ` used to be accepted. It -sent `clone_from_environment_id` on the create request, and the control plane never -read that key: its create schema does not declare it, and an undeclared key is -stripped unread. So the command answered success and created an EMPTY environment, -with nothing saying the clone had not happened. Cloning an environment is not -implemented, so the flag is gone, and so is the key on the request type. - -- `os environments create --clone-from ` (also spelled `--clone-from=`) is - now refused by the argument parser (`Nonexistent flag: --clone-from`, exit 2) before - any request is made. Before, it created an empty environment and exited 0. -- A create without the flag is unchanged: the request body never carried the key. -- `@objectstack/client`: `client.environments.create({ … })` no longer types - `clone_from_environment_id`, so a call that passes it fails to compile, naming the - key. At runtime the request was never different, because the server dropped it. - -**The one-line fix.** Remove `--clone-from ` from the command, or -`clone_from_environment_id` from the object you pass to `client.environments.create`. -The environment it creates is the same empty one the old call created. - -**What is not affected.** Every other flag of `os environments create`, and every -other field of `client.environments.create`. Starter content is still installed into -a new environment afterwards, from the App Marketplace (`os package install`). diff --git a/.changeset/21042-analytics-native-aggregate-policies.md b/.changeset/21042-analytics-native-aggregate-policies.md deleted file mode 100644 index 3b20d7cdd82..00000000000 --- a/.changeset/21042-analytics-native-aggregate-policies.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/driver-sql': patch -'@objectstack/service-analytics': patch ---- - -fix: the analytics native-SQL path aggregates with the engine's own aggregate policies, so one query answers one number whichever strategy serves it: `sum` / `avg` accumulate in double, a PostgreSQL boolean aggregand is cast, and an all-NULL `sum` answers `0`. The operand policies move from `@objectstack/driver-sql` to `@objectstack/core` (#21042) - -Clause-②: yes (widening) - -**New exports.** `@objectstack/core` exports the aggregate operand policies, moved here from `@objectstack/driver-sql`, where they were module-private. The driver now imports them and emits byte-identical statements. - -- `AGGREGATE_ACCUMULATION`: what each declared aggregate function accumulates in on PostgreSQL and MySQL. `avg` accumulates in double; `sum` accumulates in double over a fractional column; the counts, `min` and `max` take the column as stored. -- `aggregandColumnClass(shape)`: the one column-class predicate those policies read, over a column's declared `{ type, multiple }`. It answers `'fractional'`, `'integral'`, `'boolean'`, or `undefined` for every other column, a multi-valued one included. The type `AggregandColumnClass` names the three classes. -- `POSTGRES_BOOLEAN_AGGREGAND_CAST`: the functions whose boolean aggregand is cast to `int` on PostgreSQL. These are `sum`, `avg`, `min` and `max`; the two counts are never cast. -- `doubleAccumulationOperand(operand, dialect)`: the column's text, parsed as a double, spelled for `'postgres'` or `'mysql'`. -- `aggregandOperandSql(func, columnClass, dialect, operand)`: the operand an aggregate wraps, with the cast inside the double operand. The type `AggregandSqlDialect` names its dialects (`'sqlite'`, `'postgres'`, `'mysql'`, `'unknown'`). - -**What changed.** `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query` served by `NativeSQLStrategy` (the default on a SQL driver) skipped three policies `SqlDriver.aggregate()` applies. So the ObjectQL strategy and `engine.aggregate` answered differently for the same query. Measured on SQLite and PostgreSQL 16.13: - -- On PostgreSQL, `sum` / `avg` over an exact-decimal column, and `avg` over an integer one, added exact decimals. For example, `0.1 + 0.2` answered `0.3` and `11 / 9` answered `1.222222222222222`, where the engine answers `0.30000000000000004` and `1.2222222222222223`. The native statement now accumulates in double, as the driver does. -- On PostgreSQL, `sum` / `avg` / `min` / `max` over a boolean field answered `500` (`function sum(boolean) does not exist`). The native statement now casts the boolean aggregand to `int`, as the driver does, and answers the numbers the engine answers. -- On every dialect, a group whose aggregand is NULL in every row, and a measure-scoped `sum` that admits no row, answered `sum` `null` at the cube door. The strategy now folds a `null` answer to `emptyGroupValueFor` (`@objectstack/spec`) for every measure, so that `sum` answers `0`. `avg`, `min` and `max` over nothing stay `null`. The dataset door already answered `0`. - -This is no narrowing: each answer moves to the value the platform already declared for the same query. - -**What did not move.** `@objectstack/driver-sql`'s statements and answers are unchanged: a move-proof test compiles each aggregate function over each column class on SQLite, PostgreSQL and MySQL, and the statements equal the ones captured before the move. SQLite's native statement is unchanged, because neither operand policy applies there. A host that relays no field declarations to the analytics service, or names no SQL dialect, gets today's native arithmetic. diff --git a/.changeset/21044-cube-measure-field-type-table.md b/.changeset/21044-cube-measure-field-type-table.md deleted file mode 100644 index 40a2495a433..00000000000 --- a/.changeset/21044-cube-measure-field-type-table.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: the cube door asks the aggregate × field-type table for every measure, so a configured or suffix-inferred cube measure whose aggregate the table refuses for its column's declared type answers `INVALID_FIELD` / 400 on every driver and both strategies, and a `min` / `max` over a temporal column is described `time` in `fields[]` - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on every driver and on both strategies. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -FROM → TO, for a `measures` entry that resolves to a cube measure over a column of the cube's own object (an authored cube measure, or a suffix-inferred one such as `note_max`): - -- `min` / `max` over a type outside the numeric, temporal and boolean classes (the string family such as `text`, `email` and `url`; `select`, `radio`, `lookup`, `user`; `autonumber`; the JSON-stored, file and `formula` types): FROM, on the native-SQL strategy, `200` with the column's own value (a string such as `"y"`) under `fields[] { type: 'number' }`, on SQLite and PostgreSQL alike; on the ObjectQL strategy the engine's door already answered `400 INVALID_FIELD` after the strategy began. TO `400 INVALID_FIELD` before either strategy reads anything. -- `sum` / `avg` over a type outside the numeric and boolean classes (`sum` also refuses `percent`): FROM `200` with a plausible `0` on SQLite and `500 DATABASE_ERROR` on PostgreSQL (the ObjectQL strategy refused `avg` at the engine and passed `sum` to the driver, which answered the same `0` / `500`). TO `400 INVALID_FIELD`. -- `min` / `max` over a `date`, `datetime` or `time` column: FROM `fields[] { type: 'number' }` beside the instant. TO `fields[] { type: 'time' }`, the `DimensionType` word a temporal dimension column already carries, by the same rule the dataset door applies (`measureResultType`). - -**What an author sees now.** `400 INVALID_FIELD`, naming the measure as the request wrote it, the cube, the column, the object and its declared type, saying the query was not run, and naming the types the aggregate accepts, read off `AGGREGATE_FIELD_TYPE_COMPATIBILITY`. The thrown error carries `member`, `param` (`measures`), `cube`, `field` and `object`. - -**Why a refusal.** The dataset door (`POST /api/v1/analytics/dataset/query`) refuses every one of these pairs at compile by the same table (`DATASET_INVALID`), and the engine's aggregate door refuses most of them on the ObjectQL strategy; the native-SQL strategy compiled its own statement and asked nothing. Measured through the real dispatcher route on SQLite and PostgreSQL 16: a configured cube's `max` over a `text` column answered `"y"` under a column described `number` on the native strategy and `400` on the ObjectQL strategy, and `sum` over the same column answered `0` on SQLite and `500` on PostgreSQL. One cube, one query, an answer chosen by the driver. - -**What to write instead.** Aggregate a field of a type the aggregate accepts. A question that was counting in disguise is `count` (or `count_distinct` over a scalar-stored field). A first or last record by a text value is a sort on a list, not an aggregate. A quantity stored as text belongs in a numeric field of its own, aggregated there. - -**Who is affected.** A dashboard, report or caller that asked `min` / `max` / `sum` / `avg` of such a column through `/analytics/query` on the native-SQL strategy and read the answer as a real one. No example app and no shipped cube authors such a pair. A reader that branched on `fields[].type === 'number'` for a temporal `min` / `max` column now sees `time`. - -**Unchanged.** Every pair the table accepts, a `max` over a `boolean` column included (its column keeps `number`: the rule declines the boolean class); `count` over any column; `count_distinct`, which keeps its own door and words; a measure over a relationship path (`account.name`), which this door does not judge; a column the host's field metadata cannot resolve, or a type outside `FieldType`; a measure whose `sql` is `*`; an expression metric type (`number` / `string` / `boolean`); and a host that wires no `sourceFieldMeta`, where the declaration cannot be read. The dataset door keeps its own `DATASET_INVALID` answer at compile. diff --git a/.changeset/21046-discovery-auth-families.md b/.changeset/21046-discovery-auth-families.md deleted file mode 100644 index 22ffaa497c4..00000000000 --- a/.changeset/21046-discovery-auth-families.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': patch -'@objectstack/metadata-protocol': patch ---- - -feat(spec): discovery reports which optional `/auth` route families are mounted, starting with the better-auth admin family (`authFamilies.admin`) (#21046) - -Clause-②: yes - -**New key.** `DiscoverySchema` declares an optional `authFamilies` block, `{ admin: boolean }`. `admin` says whether the better-auth admin family (`{routes.auth}/admin/*`: `list-users`, `set-role`, `update-user`, `ban-user`, …) is mounted on this deployment. On a deployment that does not enable the admin plugin those routes answer a plain `404`, the same as a mistyped path, so a caller checks `authFamilies.admin` before building a URL into the family. `@objectstack/spec/api` also exports the block's schema (`AuthFamiliesSchema`, type `AuthFamilies`) and its reader, `readAuthFamilies(authService)`. - -**Same answer as `/auth/config`.** The value is the auth service's own `getPublicConfig().features.admin`, the object `GET /api/v1/auth/config` serves. Both discovery producers read it through `readAuthFamilies`: `getDiscovery()` in `@objectstack/metadata-protocol` (served by `@objectstack/rest` at `GET /api/v1/discovery`) and `getDiscoveryInfo()` in `@objectstack/runtime` (served at `GET /.well-known/objectstack`). Neither re-derives whether the admin plugin is on, so on one boot the two documents and `/auth/config` agree. On a stock boot `authFamilies.admin` is `false`. With the admin plugin on (`plugins.admin: true`, or SCIM, which forces it on) it is `true`. - -**When the key is absent.** A producer that cannot read the answer emits no `authFamilies`, rather than a guessed `false`. That happens when no `auth` service is registered (then `routes.auth` is absent too), when the registered service has no `getPublicConfig()`, or when that call throws (`/auth/config` answers `500 AUTH_CONFIG_ERROR` in that state). Treat an absent block as "not known to be mounted". - -**What did not change.** No existing key, route or status moved. The unmounted admin routes still answer a plain `404`. diff --git a/.changeset/21054-plan-runs-no-app-hooks.md b/.changeset/21054-plan-runs-no-app-hooks.md deleted file mode 100644 index f7dc8213d3e..00000000000 --- a/.changeset/21054-plan-runs-no-app-hooks.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/runtime': minor ---- - -fix(cli): `os migrate plan` / `apply` no longer run the app's `onEnable` or a host plugin's post-declaration hooks during their boot - -Clause-②: yes (widening) - -The two schema commands boot the host's stack to read what it declares. That boot ran the -config's `onEnable`, and every `kernel:bootstrapped` / `kernel:listening` hook a host plugin -registered from `init()`. A hook that reads a table the plan does not declare then failed on -every plan. On `examples/app-crm`, whose `onEnable` binds positions to permission sets, each -plan printed six `[sql-driver] DATABASE_ERROR` lines and six `position binding lookup failed` -warnings, on a database `apply` had just migrated as well as on an absent one. - -The boot now composes host code for its declarations only: - -- `AppPlugin` takes a new `skipOnEnable` option. When it is set, `start()` does not run the - bundle's `onEnable`, logs that it withheld it, and reports it through `onEnableWithheld`. The - migrate commands set it on the app they compose from `objectstack.config.ts`. -- A host plugin's `init()` gets a context that does not register `kernel:bootstrapped` or - `kernel:listening` hooks. The kernel contract defines those phases as work after registration - ends: reconcile/backfill, and opening listeners. `kernel:ready` hooks still run, and the - write guard still refuses their row writes. `kernel:shutdown` hooks and data hooks register - as before. -- The plan's notes, and the `--json` payload's `composition.notes`, carry one line naming what - was not run. - -The plan itself is unchanged: the same tables, the same pending DDL, the same drift. `apply` -still flushes the DDL the operator confirms and still runs the coverage pass. The platform's own -plugins are untouched, so the value-shape gate announcement still prints. - -`@objectstack/runtime` widens its public surface, additively: `AppPlugin`, exported from the -package root, gains the optional constructor option `skipOnEnable` (default `false`) and the -read-only getter `onEnableWithheld`. A composition that does not pass the option gets exactly -the behaviour it had, `onEnable` included. diff --git a/.changeset/21058-datasource-admin-record-judgement.md b/.changeset/21058-datasource-admin-record-judgement.md deleted file mode 100644 index 137e4cffeb5..00000000000 --- a/.changeset/21058-datasource-admin-record-judgement.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): `POST` / `PATCH /api/v1/datasources` and `POST /api/v1/datasources/test` judge the datasource record they will persist against `DatasourceSchema`, the contract `os build` and `PUT /api/v1/meta/datasource/:name` already enforce (#21058) - -Clause-②: no - -- Before, these doors checked the driver `config` alone. A record that `DatasourceSchema` refuses was accepted `201`, and `GET /api/v1/meta/datasource/:name` then reported it `valid: false`. The record is now judged as it will be stored, with the `external.credentialsRef` that a supplied `secret` (or the existing binding) carries, before any secret or record is written. A refusal answers `400 DATASOURCE_ADMIN_ERROR` with `DatasourceSchema`'s own message, and nothing is persisted. -- Newly refused, for example: a mongo `config.url` whose userinfo names no user, or a composed mongo `config` with no `username`, beside a `secret`. The bound secret was never used and the datasource connected anonymously. Fix: put the user in the url (`mongodb://user@host/db`) or in `config.username`, or send no `secret`. Also refused: `schemaMode: 'external'` or `'validate-only'` with no `external` block. Fix: send `external: {}` or the federation settings. Also refused: a create with no `config`, or a `pool` key the schema does not declare. -- `POST /api/v1/datasources/test` answers `ok: false` with the same message instead of probing, so a green test no longer comes before a refused save. -- A `PATCH` that changes only `label` and/or `active` is not judged. A row stored before this change can still be renamed or taken out of service. diff --git a/.changeset/21059-layered-code-null-unshipped.md b/.changeset/21059-layered-code-null-unshipped.md deleted file mode 100644 index 93e9e3bf9fd..00000000000 --- a/.changeset/21059-layered-code-null-unshipped.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): the layered read's code layer is empty for an item no package ships, whether or not a stored copy of it has been loaded into the registry (#21059) - -Clause-②: no - -The layered read, `GET /api/v1/meta/:type/:name/layers`, reports an item's code layer as the packaged definition, and as empty when no package ships the item and it exists only as a stored customization. For an item no package ships, it answered that correctly only until the stored copy had been loaded into the in-memory registry, for example by a restart's startup load or by a list read. After that, the code layer answered the stored copy, and the lock and provenance flags were derived from it. So the same read of the same item gave two answers, depending on what had been loaded. - -The code layer now skips a stored copy the registry holds, using the tenant-authorship mark the startup load already puts on every stored copy it registers. An item no package ships has an empty code layer before and after the load, and its flags come from the stored layer both times. This includes a stored copy whose own content claims a package's provenance: a claim is not a packaged definition. The deprecated layers flag on the by-name read, `GET /api/v1/meta/:type/:name`, answers the same. - -Packaged items keep their packaged code layer. An item registered at runtime with no package keeps its code layer, as before. The stored rows are not changed. diff --git a/.changeset/21061-analytics-anonymous-deny.md b/.changeset/21061-analytics-anonymous-deny.md deleted file mode 100644 index 135e99facd4..00000000000 --- a/.changeset/21061-analytics-anonymous-deny.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/runtime': minor ---- - -fix(runtime)!: the analytics dispatcher faces refuse an unauthenticated caller with `401 UNAUTHENTICATED`, like every other data-serving door (#21061) - -Clause-②: no (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. The three analytics faces the dispatcher mounts under `/api/v1/analytics` (cube read, SQL echo, meta) now answer a caller without a session `401 UNAUTHENTICATED`, in the dispatcher's wrapped envelope (`{ success: false, error: { code: 'UNAUTHENTICATED', message, httpStatus: 401 } }`). They answered that caller as a guest before. - -**What changed.** The analytics domain handler opens with the shared anonymous-deny decision (`shouldDenyAnonymous`, ADR-0056 D2), the same floor the `/data`, `/meta`, `/actions`, `/automation` and `/packages` doors stand on, and the one the REST analytics dataset door already applied. The floor is the handler's first statement: - -- it runs before the analytics service is looked up, so an unauthenticated caller gets `401` whether or not the analytics capability is installed, never the `404` an empty slot answers; -- it runs before the request body is validated, so a malformed body from an unauthenticated caller is `401`, never the `400 VALIDATION_FAILED` the entry check answers. - -**What is not affected.** A signed-in caller, an API-key caller and an internal system context are served exactly as before: the same `200`, the same `400` for a malformed body, the same `404` when no analytics service is installed. Object admission and the row scope behind the service are unchanged by this release. - -**If an analytics call now answers `401`,** it was made without a session: send it with the signed-in user's session or bearer token, or with an API key. diff --git a/.changeset/21062-picker-queryable-key.md b/.changeset/21062-picker-queryable-key.md deleted file mode 100644 index 18e50548e58..00000000000 --- a/.changeset/21062-picker-queryable-key.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@objectstack/rest': minor ---- - -fix(rest)!: a public form's lookup picker searches and sorts by the first display field the caller may query, so a picker whose first display field is masked for its caller serves its rows instead of answering 403 (#21062) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this widens what the public lookup picker serves and narrows it in one composition. The narrowing: with a security service that lacks `ISecurityService.getQueryableFields`, or that answers no answer for the object, the picker passes over every display field whose declaration carries a `maskingRule`, for every caller, including a caller the rule is lifted for. So its search and order move to the next display field that declares no rule, and a picker whose display fields all declare a rule is refused `403 PERMISSION_DENIED` without the engine being asked, where it used to be served. The security service this repository ships implements the method, so a deployment using it is not narrowed. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What changed.** The public lookup picker (`GET /forms/:slug/lookup/:field`) -matches the visitor's search and orders its rows by one key. That key used to -be the first entry of `publicPicker.displayFields`. It is now the first entry -the caller may query on, as the security service answers it -(`ISecurityService.getQueryableFields`). A field whose masking rule applies to -a caller is served to that caller masked, and the engine refuses to search or -sort on it with `403 PERMISSION_DENIED`. A picker whose first display field -declares such a rule therefore answered `403` to every caller the rule applies -to, on every request. It now serves its rows, sorted and searched on the next -display field the caller may query. The masked field is still returned in each -row, masked, as before. - -**When no display field is queryable** for the caller, the picker answers -`403 PERMISSION_DENIED` with the engine's refusal for those fields, without -running a query. - -**Unchanged.** A picker with no masked display field, and a caller the masking -rule is lifted for, keep the first display field as the key, with the security -service this repository ships. A deployment with no security service keeps the -first display field. - -**What to do.** Nothing. To choose the field a picker searches when its first -display field is masked for some of its callers, list a field those callers may -query among `displayFields`: the first such entry is the one searched and -sorted on. A picker whose only display fields are masked for its callers is -refused, so give it one they may query. - -**What to do after upgrading, if your security service predates `getQueryableFields`.** -Implement `getQueryableFields` on it: it answers which fields a caller may filter, -sort, group or aggregate by, and the picker then keys on the first display field -in that answer. Until it does, give each picker at least one display field that -declares no `maskingRule`, or the picker is refused for every caller. diff --git a/.changeset/21062-public-form-read-back-masking.md b/.changeset/21062-public-form-read-back-masking.md deleted file mode 100644 index b79a34d72de..00000000000 --- a/.changeset/21062-public-form-read-back-masking.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker, so a field whose masking rule applies is echoed masked (#21062) - -Clause-②: no (narrowing) - - - -**BREAKING for the anonymous public-form submit's response.** - -**What changed.** A public form's submission is authorized by the ADR-0056 -declaration-derived grant, which admits the create and the read-back on the -form's declared object and passes before any permission set is resolved. The -grant handed the operation to the engine and returned before the result masker -ran, so the record echoed in the `201` body carried every field whose -`maskingRule` applies as stored: the field the form collects, and a field -filled from its `defaultValue` that the form never shows. - -The grant now hands what it returns to the same result masker the data plane -uses, for the caller it stands in for: the permission sets resolved for the -grant's context (the deployment's guest set when it registers one, otherwise -none) and the object posture those sets read. A field whose masking rule -applies is echoed masked. A field the caller's sets mark unreadable, or whose -`requiredPermissions` they do not hold, is masked the way the data plane masks -it for that caller. The read-backs the grant admits are masked the same way. - -**What did not change.** The grant admits exactly what it admitted: the create -and the read-back on the form's declared object, and nothing else. The -server-managed fields are still stripped from the submitted row. The stored row -is unchanged; only the echo is masked. - -**One more refusal, by the same rule.** If the caller's permission sets or the -object's security posture cannot be read, the submission is now refused with -`403 PERMISSION_DENIED` before anything is written, as every other caller's -request already is. The masking rules come from that posture, so the echo -cannot be masked without it. - -**What to do.** Nothing, unless a client reads a masked field's stored value -back out of the submit response. The response now carries the masked value, as -every other non-system read does. A field's `requiredPermissions` are the gate -that lifts its mask, so a deployment whose guest set holds all of them is -echoed the stored value; otherwise drop the `maskingRule`. diff --git a/.changeset/21063-security-contract-zero-set-fields.md b/.changeset/21063-security-contract-zero-set-fields.md deleted file mode 100644 index e978a4b6a72..00000000000 --- a/.changeset/21063-security-contract-zero-set-fields.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -docs(spec)!: the security service contract's field answers for a caller who resolves no permission set exclude the fields that declare `requiredPermissions` (#21063) - -Clause-②: yes (narrowing) - - - -**BREAKING for implementers and consumers of `ISecurityService` field answers.** - -**What changed.** The contract in `@objectstack/spec/contracts` now states the -field answers for a non-system caller who resolves no permission set. Such a -caller holds no permission-set field grant and no capability. No grant narrows -its answers, and a field's own declarations still apply: a field that declares -`requiredPermissions` is not in its `getReadableFields` answer (unless a -`maskingRule` on the field serves it masked, which keeps it as a served -column), and it is not in its `getWritableFields` answer. -`getMetadataReadableFields` answers the same for that caller when the -deployment's fallback set resolves to nothing. The contract used to say the -data-plane answer for that caller was the full field set, because the engine -middleware skipped its whole field gate for it. The middleware skips only its -permission-set grant gates. - -**Who this reaches.** An implementation of `ISecurityService` must answer this -way for that caller. A consumer that relied on the full field set for that -caller now receives the narrower answer from the reference implementation -(`@objectstack/plugin-security`). - -**What to do.** An implementation folds each field's `requiredPermissions` -into its answer for this caller exactly as it does for a caller whose -permission sets lack the capability. A consumer needs no change. diff --git a/.changeset/21063-zero-set-capability-fold.md b/.changeset/21063-zero-set-capability-fold.md deleted file mode 100644 index 7c329a763b8..00000000000 --- a/.changeset/21063-zero-set-capability-fold.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -fix(plugin-security)!: a field that declares `requiredPermissions` is not served to a caller who resolves no permission set, and that caller may not query on it or write it (#21063) - -Clause-②: yes (narrowing) - - - -**BREAKING for callers who resolve no permission set.** - -**What changed.** A field that declares `requiredPermissions` is masked on read -and denied on write unless the caller holds all of them (ADR-0066 D3). A caller -who carries a principal but resolves no permission set holds no capability, so -the gate applies to it, but the runtime served that caller the stored value, -let it filter, sort, group and aggregate on the field, and accepted a write -that named it. The explain engine already reported the field hidden for that -caller. Now the field is not served to it (a field that also declares a -`maskingRule` is served masked, as before). A filter, sort key, group key or -aggregate that names the field is refused with `403 PERMISSION_DENIED`, and so -is a write payload that names it, as for any other caller who lacks the -capability. - -The published field answers agree with what is served and refused. -`ISecurityService.getReadableFields`, `getQueryableFields` and -`getWritableFields` no longer list such a field for this caller, and neither -does `getMetadataReadableFields` when the deployment's fallback set resolves to -nothing. The write preview answers such a payload as the write path does. - -**Who this reaches.** A caller who resolves no permission set but carries a -position, a named permission set or a user id. A caller with none of the three -is handed through untouched, as before, and the field projections say so. A -caller who resolves at least one permission set is unaffected, and so is a -system context. Whether this caller may read or write the object at all is -unchanged. - -**What to do.** Nothing, unless such a caller needs the field. A field's -`requiredPermissions` name the capabilities that open it, so give the caller a -permission set that holds all of them, or drop the requirement from the field. diff --git a/.changeset/21066-memory-json-column-family-refusal.md b/.changeset/21066-memory-json-column-family-refusal.md deleted file mode 100644 index 60560c2c62b..00000000000 --- a/.changeset/21066-memory-json-column-family-refusal.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@objectstack/driver-memory": minor ---- - -fix(driver-memory)!: on a declared JSON-stored field, the query path and the analytics face refuse `$eq` / `$ne` / an ordering / `$between` / `$in` / `$nin` / implicit equality with `INVALID_FILTER` / 400, in the words the SQL family refuses them in, instead of answering each per element - -Clause-②: yes (narrowing) - - - -**BREAKING** (`@objectstack/driver-memory`): this narrows what the driver's filter doors accept, for every caller that reaches them: `find`, `findOne`, `count`, `updateMany`, `deleteMany` and `aggregate` with a `where`, through the engine or called directly, and `MemoryAnalyticsService`'s `query` and `generateSql`. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a `where` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, before any row is read. On the analytics face a `where` key is a cube member, judged by the field it resolves to. That is the set `driver-sql` refuses on such a column, for the same reason. - -**What an author sees now.** The body `driver-sql` answers for the same filter: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, and the full diagnostic, naming both and the position in the filter, is written to the driver's (or the analytics service's) logger at `warn`. - -**Why a refusal.** This driver answered each of those operators per element, through mingo's array semantics. Measured through `engine.find` over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $eq: 'u1' } }` and `{ owners: { $in: ['u1', 'u9'] } }` returned those two rows, `$nin` the other four, and `{ owners: { $gt: 'u1' } }` four rows by comparing each member as text, where every SQL dialect answers the same filters 400. An application whose tests run on this driver passed on a filter its production backend refuses. - -**Who is affected.** A test suite, demo or dev setup on this driver that filters a JSON-stored field with one of those operators and read the per-element rows as the answer. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. - -**Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not declared JSON-stored; and an object this driver holds no declaration for (one never passed through `syncSchema`), where nothing is judged and every operator answers as before. `InMemoryDriver` gains one method, `filterFieldDeclarations`, tagged `@internal`: it exists so the analytics face judges its `where` by the same declarations, and it is not a consumer contract. diff --git a/.changeset/21067-json-refusal-under-bound.md b/.changeset/21067-json-refusal-under-bound.md deleted file mode 100644 index 7da1122f96b..00000000000 --- a/.changeset/21067-json-refusal-under-bound.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/core": patch -"@objectstack/driver-sql": patch -"@objectstack/driver-memory": patch -"@objectstack/objectql": patch ---- - -fix(core): the refusal a filter gets for a scalar comparison or text operator on a multi-value or JSON field reads true on every backend that prints it, and reaches a REST caller whole - -Clause-②: no - -The `INVALID_FILTER` / 400 refusal `driver-sql`'s `where`, the engine's per-aggregation `filter` and `driver-memory` all print (`jsonColumnOperatorRefusalText`) explained itself with `driver-sql`'s storage ("a field this driver stores as a JSON TEXT column") and the two wrong answers SQL used to give. That is untrue on the engine and on `driver-memory`. The message was also 748 characters, and the REST envelope cuts a 4xx message at 499 plus an ellipsis, so callers on SQLite and PostgreSQL read `…Refused rather than compiled because the answ…` and never reached the sentence saying the field and the operator were withheld. - -The message now reads, on every backend, in 486 characters: `A constraint in this filter WAS NOT APPLIED: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member.`, then the same `$contains` / `$or` of `$contains` remedy, then `For no value, use "$null" or "$empty".` (a `null` comparand such as `{ f: null }`, `$eq: null` or `$ne: null` is refused too, and `$contains` could not express it), then `The field and the operator are withheld from the message; the full diagnostic is in the server log.` The diagnostic (the server-log text, and what a filter's own author is shown) gives the same reason with the operator named, names the field, and spells the remedy with the field's name. It drops the storage and the SQL history too, and is now whole on the wire for field names up to 26 characters (it was 643 characters or more and always cut). - -Code, status, the refused operator set and the `$contains` remedy are unchanged. A client that matched on the old words `JSON TEXT column` or `Refused rather than compiled` should match on `code: "INVALID_FILTER"` instead. diff --git a/.changeset/21068-macro-past-date-range.md b/.changeset/21068-macro-past-date-range.md deleted file mode 100644 index ad71d5ff2ec..00000000000 --- a/.changeset/21068-macro-past-date-range.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/core': minor ---- - -fix(core)!: a date macro whose offset lands past every instant a JavaScript `Date` can hold is refused `INVALID_FILTER` / 400, naming the placeholder, instead of resolving to the text `Invalid Date` or throwing an uncoded `RangeError` - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `resolveFilterToken` and `resolveFilterTokens` answer for one class of inputs, and so what every door that resolves filter placeholders answers. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What is refused now.** A relative-date placeholder whose offset lands past the instants a JavaScript `Date` can hold names no day and no time. The resolver used to answer a day-or-coarser one (`{300000_years_ago}`) with the text `Invalid Date`, which compares as text, and to throw an uncoded `RangeError: Invalid time value` for a sub-day one (`{99999999999999999999_minutes_ago}`). Measured before this through `engine.find` on InMemoryDriver and `POST /api/v1/data/:object/query` on SqlDriver over SQLite, over a `datetime` field with a row in 2026 and a row in 1500: - -- `$lt {300000_years_ago}` answered both rows, and `judgeFilter` answered `{ ok: true }`; -- `$lt {99999999999999999999_minutes_ago}` threw the uncoded `RangeError` from `engine.find` and `judgeFilter`, and the REST door answered `500 INTERNAL_ERROR`. - -Both are refused now with `INVALID_FILTER` / 400 at every position the engine resolves (`where` on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`, a per-aggregation `filter`, `having`) and through `judgeFilter`, before any driver read. Every other caller of `resolveFilterToken` / `resolveFilterTokens` receives the same coded error in place of the text or the `RangeError`. The refusal is the same on every column, because the resolver does not know the column: such a placeholder names no value at all. - -**What an author sees.** `Relative-date placeholder "{300000_years_ago}" names no instant: its offset lands past every instant a JavaScript Date can hold, so it resolves to no day and no time, and it is refused rather than compared. A date value names a year from 0001 to 9999, and a datetime value a year from 1000 to 9999: use a relative-date placeholder whose offset lands inside those years.` The thrown error carries `code: 'INVALID_FILTER'`, `status: 400` and `token` (the placeholder's name), the code the engine already answers for a placeholder that resolves outside its column's years. - -**Unchanged.** A placeholder that names an instant resolves as before, including one past the years 0001..9999 that a `Date` still holds (`{273847_years_ago}` resolves to `-271821-09-30`); the engine's per-column year range judges that one, as before. Context placeholders and an unknown placeholder answer as before. diff --git a/.changeset/21078-oidc-discovery-sync-mount.md b/.changeset/21078-oidc-discovery-sync-mount.md deleted file mode 100644 index 998177ed801..00000000000 --- a/.changeset/21078-oidc-discovery-sync-mount.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): the OIDC discovery documents answer on every boot, including one whose first request arrives before the auth instance is built - -Clause-②: no - -- `GET /.well-known/openid-configuration` and `GET /.well-known/oauth-authorization-server` used to be mounted only after the better-auth instance finished building, in the background. When the server answered any request before that (a readiness probe, for example), the router was already sealed. The late mount failed, the failure was logged, and both documents answered 404 until the process restarted. The RFC 8414 path-inserted alias and the two RFC 9728 protected-resource documents had the same problem. -- All five routes are now mounted while the auth routes are registered, before the server opens its socket. Each request waits for the auth instance and then serves the document. A route that cannot be mounted now fails the boot instead of being logged and skipped. -- When the OIDC provider plugin is degraded, these paths answer as they did before, as if they were not mounted. A boot-time error line still reports this. -- If the auth instance cannot be built, a discovery request answers a server error, and the next request tries the build again. Before, these paths kept answering 404. diff --git a/.changeset/21079-zero-set-deny-baseline.md b/.changeset/21079-zero-set-deny-baseline.md deleted file mode 100644 index 40b531c5ee2..00000000000 --- a/.changeset/21079-zero-set-deny-baseline.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/plugin-security': minor -'@objectstack/spec': minor ---- - -fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope, and the security contract says so (#21079) - -Clause-②: yes (narrowing) - - - -**BREAKING for callers who resolve no permission set.** Shipped as `minor` under the launch-window convention. - -**What changed.** ADR-0056 D2 gives an unauthenticated principal the deny baseline, not "no checks", and ADR-0090 D9 gives a guest the `guest` position and nothing else. A non-system caller that carries a principal (a position, a named permission set or a user id) but resolves no permission set was instead admitted to every object no set grants, for reads and writes, and read with the record-sharing predicate as its only row scope. Now an empty set list grants nothing: - -- **Object admission refuses it.** Every engine operation (find, findOne, count, aggregate, insert, update, delete) is refused with `403 PERMISSION_DENIED`, the same refusal any caller gets for an object its sets do not grant. `ISecurityService.canReadObject` and `canExport`, and the write preview's admission, answer `false` for it. -- **Its row scope is the deny filter.** `ISecurityService.getReadFilter` answers the filter that matches zero rows for it, as it already did on a resolution failure. -- **The second principal of a delegated request is held to the same answer.** An agent acting on behalf of a delegator who resolves no permission set was already refused by the engine; `canReadObject`, `canExport` and the write preview now refuse it too. - -The field answers for this caller (`getReadableFields`, `getQueryableFields`, `getWritableFields`, `getMetadataReadableFields`) are unchanged: they are field-level answers, and the contract now says that object admission is not part of them. The `ISecurityService` docblocks in `@objectstack/spec/contracts` that stated the old zero-set admission (`canReadObject`, `canExport`, the metadata-plane field projection) and the deny cases of `getReadFilter` narrow to match. No method, parameter or return type changes. - -**Who this reaches.** - -- An unauthenticated request carried as the guest envelope, on a deployment that grants anonymous callers no permission set. -- A context that names only permission sets the deployment does not register. -- A signed-in user on an embedder that switches the baseline off (`fallbackPermissionSet: null`) and grants that user nothing. - -A context that carries no principal at all (no position, no named set, no user id) is handed through as before; ADR-0096 stages it separately. A caller who resolves at least one permission set is decided by its sets, as before, and so is a system context. The public form submit is unaffected: its declaration-derived grant admits the create and its read-back ahead of object admission. Signed-in users of a stock `objectstack serve` deployment are unaffected: it applies the member baseline to every one of them, so none resolves an empty list. - -**Migration.** A caller that resolves no permission set is refused object admission and reads nothing. An app-declared anonymous endpoint (`authRequired: false`) can no longer read or write objects until the `guest` anchor's bindings are resolved for anonymous callers (#21158). An embedder that sets `fallbackPermissionSet: null` must grant its signed-in users a set explicitly. - -**For implementers of `ISecurityService`.** Answer `canReadObject`, `canExport` and the object-admission half of a write `false`, and `getReadFilter` with your deny filter, for a non-system caller that carries a principal and resolves no permission set; admit only the principal-less context. diff --git a/.changeset/21080-native-sql-engine-middleware-decline.md b/.changeset/21080-native-sql-engine-middleware-decline.md deleted file mode 100644 index f9118a290bc..00000000000 --- a/.changeset/21080-native-sql-engine-middleware-decline.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/objectql': minor -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: the analytics native-SQL strategy declines an object an engine middleware is registered for, so the engine serves it and that object's read gates apply; the engine answers which objects carry one (`IObjectQLEngine.hasObjectMiddleware`) (#21080) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what the analytics doors serve for one class of query. It ships as `minor` under the launch-window convention for narrowings. - -**What changes.** On a SQL driver, `NativeSQLStrategy` compiled a query to SQL and ran it through the driver's raw-SQL seam, so no engine operation ran and no engine middleware did. It applied the security service's object admission and read filter and nothing else, so the read gates that live in the engine as per-object middlewares did not apply there: a caller admitted to such an object at object level read grouped results and counts over every row, rows about parent records that caller cannot read included. It now declines a query that reads (as its base object, a declared join, or through a relationship path) an object the data engine holds a middleware registered for. The ObjectQL strategy serves it through the engine with the caller's context, so the engine's middlewares run, and the analytics answer for that caller equals the data door's. On the stock composition the objects that move off the native path are `sys_comment`, `sys_activity` and `sys_attachment` (read gates), `sys_approval_request` (the snapshot redaction), and `sys_user_position` and `sys_permission_set` (write-side middlewares, which move as a side effect: a middleware does not declare its operation). No shipped dataset or dashboard reads any of them. - -**What is newly refused.** A query on such an object that the ObjectQL strategy cannot serve is refused with that strategy's existing `400`, where the native strategy used to serve it: for example a dimension reached through a relationship path combined with a measure that cannot be recombined across it (`avg`, `count_distinct`). Correctness wins over the fast path for a gated object. - -**It fails closed.** `AnalyticsServicePlugin` asks the data engine. An engine without `hasObjectMiddleware`, or no engine, cannot say, and the strategy declines then too: every query on such a host is served by the ObjectQL strategy, and the plugin says so once at `warn`. A host that constructs `AnalyticsService` with `executeRawSql` and without the new `hasObjectMiddleware` config member keeps the native path for every object and is told so once at construction. - -**New, additive.** `IObjectQLEngine.hasObjectMiddleware?(objectName): boolean` (`@objectstack/spec`), `ObjectQL.hasObjectMiddleware(objectName)` (`@objectstack/objectql`): whether a `registerMiddleware(fn, { object })` names the object; a global registration (no `object`, or `'*'`) is keyed to none and is not counted. `AnalyticsServiceConfig.hasObjectMiddleware` (`@objectstack/service-analytics`), which the plugin fills from the data engine. - -**Unchanged.** Objects no middleware names keep the native path. The middleware chain, `registerMiddleware` and every gate are unchanged. - -**What to do after upgrading.** Nothing on the stock composition. A host whose `"data"` service is not ObjectQL should implement `hasObjectMiddleware` to keep the native path for ungated objects. A host that builds `AnalyticsService` itself with `executeRawSql` should pass `hasObjectMiddleware` from its engine. diff --git a/.changeset/21081-activity-field-values.md b/.changeset/21081-activity-field-values.md deleted file mode 100644 index 791dfe29758..00000000000 --- a/.changeset/21081-activity-field-values.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -fix(plugin-audit): an activity row serves a parent field's value only to a reader the security service serves that field (#21081) - -Clause-②: no - -The activity stream's CRUD mirror composes each row once, at write time, as the system. The row's summary, its record label and its recorded change can carry the values of the parent record's fields. The activity read gate keeps a row for every reader who can read the parent record, so a reader who may not read one of that record's fields was served the field's stored value through the row. This held for a field served masked to the reader, a field gated by `requiredPermissions` the reader does not hold, and a field a permission set the reader holds marks non-readable. The data plane answered the same reader masked or without the key. - -The rows are now redacted at read time, keyed on the reading caller, through the security service's own answer: the read projection intersected with the query-side answer, whose difference the contract defines as exactly the fields served masked. The recorded change drops every key the reader is not served. The summary and the record label are each served whole or dropped whole: the mirror now declares, in the row, which parent fields each was composed from, and a text composed from a field the reader is not served is dropped. A text composed only from served fields is kept. The full row stays at rest, and system reads are unchanged. - -Rows written before this release carry no such declaration. Their summary and record label are served only to a reader who is served every field of the parent record, until the rows age out with the stream's retention. Rows an app writes itself are served as written, except that a recorded change in the mirror's shape is narrowed the same way. diff --git a/.changeset/21086-data-door-stored-metadata-projection.md b/.changeset/21086-data-door-stored-metadata-projection.md deleted file mode 100644 index 2d470cd3bff..00000000000 --- a/.changeset/21086-data-door-stored-metadata-projection.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol)!: stored metadata bodies read through the generic data door are served as their type's read projection, so stored credentials are withheld there too; grouping those tables by the body column is refused (#21086) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the generic data door's query accepts as a grouping target on two system tables. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What changes.** A row of `sys_metadata` or `sys_metadata_history` read through `GET /api/v1/data/:object`, `POST /api/v1/data/:object/query`, `GET /api/v1/data/:object/:id` (and anything that reads through the same `findData` / `getData`, such as the export route) now carries its `metadata` column as the body's type's read projection: the same object every `/meta` read exit serves, chosen through the same `@objectstack/spec/kernel` redactor registry. For a `datasource` body that means the stored credential material the datasource doors already withhold is withheld here too, decided by the same redactor. A body with nothing to withhold, and every body of a type that registers no redactor, is served as the stored bytes. - -- A projection that names `metadata` without `type` (`?select=metadata`) still works: the door reads `type` to choose the redactor and does not serve it. -- A body the door cannot judge is omitted rather than served: one whose row carries no `type`, and one that does not parse while its type registers a redactor. - -**What an author sees now on a grouping.** `400 INVALID_FIELD` for a `groupBy` entry naming `metadata` on either table, located at the entry (`groupBy[0]`, or `groupBy[0].field` for the object form), saying the query was not run and naming the route: group by `type`, `name` or another scalar column, and read the bodies with a plain list. A group key stands for every row that shares it, and the redactor is chosen per row, so the key cannot be projected without changing which rows it counts. - -**Unchanged.** Every other object, including one with a column of its own named `metadata`; every other grouping on these tables; and every internal reader of `sys_metadata`, which reads through the engine rather than through this door and keeps reading the stored body. diff --git a/.changeset/21087-datasource-meta-read-capability.md b/.changeset/21087-datasource-meta-read-capability.md deleted file mode 100644 index cc1650ecb88..00000000000 --- a/.changeset/21087-datasource-meta-read-capability.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/rest': patch -'@objectstack/runtime': patch ---- - -fix(rest,runtime): reading `datasource` and `external_catalog` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability each type's own door already requires (#21087) - -Clause-②: no - -- A `GET` or `HEAD` of `/api/v1/meta/datasource` or `/api/v1/meta/external_catalog` (and their plural spellings) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`GET /api/v1/datasources`, `GET /api/v1/datasources/:name`) and the federation read door (`GET /api/v1/datasources/:name/external/tables`) already require for the same data. Every read route under the type is judged alike: the list, the item read and each of its query switches, `/published`, `/layers`, `/history`, `/audit`, `/diff` and `/references`. `/history`, `/audit` and `/diff` still also require an authoring capability, as before. -- A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. The answer is the same whether or not the named item exists, and nothing is read from the metadata store first. -- Holders of `manage_platform_settings` are served exactly as before. Platform administrators hold it through `admin_full_access`. Every other metadata type, and every write route, is unchanged. -- Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by. -- If you read either type with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or read through a caller that already has it. diff --git a/.changeset/21094-prod-deps-group.md b/.changeset/21094-prod-deps-group.md deleted file mode 100644 index 9db765204b8..00000000000 --- a/.changeset/21094-prod-deps-group.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/cli': patch -'create-objectstack': patch -'@objectstack/connector-mcp': patch -'@objectstack/core': patch -'@objectstack/objectql': patch -'@objectstack/rest': patch -'@objectstack/runtime': patch -'@objectstack/spec': patch -'@objectstack/driver-mongodb': patch -'@objectstack/driver-sqlite-wasm': patch -'@objectstack/driver-turso': patch -'@objectstack/mcp': patch -'@objectstack/metadata-core': patch -'@objectstack/metadata-protocol': patch -'@objectstack/metadata': patch -'@objectstack/plugin-auth': patch -'@objectstack/plugin-hono-server': patch -'@objectstack/plugin-pinyin-search': patch -'@objectstack/service-settings': patch ---- - -Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: - -Clause-②: no - -- `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. -- `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. -- `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. -- `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. -- `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. -- `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. -- `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. -- `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. -- `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. -- `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. diff --git a/.changeset/21095-action-outcome-messages.md b/.changeset/21095-action-outcome-messages.md deleted file mode 100644 index 96a37584341..00000000000 --- a/.changeset/21095-action-outcome-messages.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/client': minor -'@objectstack/cli': patch ---- - -feat(spec,client)!: `ActionSchema` gains `outcomeMessages` — success copy per closed handler `outcome`, interpolating `${result.*}` — and `client.environments.delete` no longer guarantees `message` on its archive answer (#21095) - -Clause-②: yes (narrowing) - - - -**BREAKING for TypeScript readers of `client.environments.delete`'s archive answer**: `message` is now `message?: string`. Code that assigns it to a `string` stops compiling at that read. Nothing else in the SDK changes, and the request is unchanged. - -**`ActionSchema.outcomeMessages`** (`@objectstack/spec/ui`). A server-executing action can succeed in more than one way: an environment delete archives, finds the environment already archived, defers a purge, or destroys it. One static `successMessage` cannot say which of these happened. The handler now reports a closed `outcome` fact in its success payload, and the action declares the copy for each outcome: - -```ts -outcomeMessages: { - archived: 'Environment ${result.environmentId} archived.', - already_archived: 'Environment ${result.environmentId} was already archived.', -} -``` - -- Keys are snake_case outcome names; values are `I18nLabel`s. A key that is not snake_case is refused at its own path (`invalid_key`). -- The key is valid on `type: 'api'` and `type: 'script'` actions only, the two types with a success payload that can carry an `outcome`. It is refused with a prescription on `url` / `modal` / `flow` / `form`, beside `resultDialog` (which suppresses the success toast), and beside `operation: 'update'` (no handler, so no outcome). -- `successMessage` and each outcome message may interpolate `${result.*}`, the server-response scope `onSuccess.navigate` already declares. This is not a new dialect. -- The console picks `outcomeMessages[result.outcome]`, falls back to `successMessage`, and then to its default text. The console does not read it yet. Until it does, the key is accepted, validated, translated and extracted, and the liveness ledger grades it `planned`, so `os lint` tells an author who writes it that it is not shown yet. - -**Translation.** `TranslationData` carries the copy beside `successMessage`: `objects.OBJECT._actions.ACTION.outcomeMessages.OUTCOME` and `globalActions.ACTION.outcomeMessages.OUTCOME`. Outcome keys there are snake_case too. `translateAction` overlays them per outcome (object-scoped first, then global) and only for outcomes the action declares. `os i18n extract` emits one key per declared outcome. - -**`client.environments.delete`** (`@objectstack/client`). The control plane is replacing its English `message` with the closed `outcome` fact: the server returns facts, and the console composes the message in the user's locale. The archive answer declares `outcome?: 'archived' | 'already_archived' | 'purge_deferred'`, and the teardown answer declares `outcome?: 'destroyed'`. Both `outcome` and `message` are optional, because a 200 may carry either one or both depending on which control-plane release answers. To learn what happened, read `deleted` and `purgeDeferred`, which every answer still carries, or `outcome` when it is present. diff --git a/.changeset/21096-liveness-hint-never-note.md b/.changeset/21096-liveness-hint-never-note.md deleted file mode 100644 index 3c2bdc512da..00000000000 --- a/.changeset/21096-liveness-hint-never-note.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -fix(lint): a liveness finding's fix text is the row's `authorHint`, else the verdict's default hint, and never the row's internal ledger `note`, for every row class. Before this, a row that opted in with `authorWarn`, and an `experimental` row, with no `authorHint` printed its `note` as the fix text: on `app-showcase`, the two `liveness-planned-property` findings for `externalSharingModel` printed a 728-character maintainer note that cites a tracker id. They now print "Keep it — a consumer is being built against this property; it has no runtime effect yet." No rule id, message, severity or exit code changes, and a row that has an `authorHint` prints it exactly as before (#21096) - -Clause-②: no diff --git a/.changeset/21106-error-code-ledger-provenance-rows.md b/.changeset/21106-error-code-ledger-provenance-rows.md deleted file mode 100644 index 255c997d320..00000000000 --- a/.changeset/21106-error-code-ledger-provenance-rows.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -`ERROR_CODE_LEDGER['@objectstack/service-automation']` now lists `MAPPING_NOT_FOUND` and `UNSUPPORTED_TRANSFORM`, the two registered codes the connector sync executor (`pullConnectorSource`) stamps onto `ConnectorPullError.code` (#21106). - -Clause-②: yes - -Provenance, not identity. The per-package face of `ERROR_CODE_LEDGER` changes in this release in two steps, and neither changes the `ErrorCode` union, the wire or any HTTP answer: - -- The bulk-import runner, the mapping pipeline and the data-error classification moved out of `@objectstack/rest` (#20919), and each code's row moved to the package that now stamps it. `@objectstack/core` gains `AMBIGUOUS_MATCH`, `BLANK_MATCH_KEY`, `NO_MATCH`, `SUMMARY_RECOMPUTE_FAILED` and `UNSUPPORTED_TRANSFORM`. A new `@objectstack/types` key lists `CONCURRENT_UPDATE`, `ERR_DATASOURCE_UNAVAILABLE` and `UNIQUE_VIOLATION`. `@objectstack/rest` no longer lists those seven, because it stamps none of them now; it keeps `UNSUPPORTED_TRANSFORM`, which it still stamps. -- `@objectstack/service-automation` gains the two rows above. Both codes were already registered, under `@objectstack/rest` (and `UNSUPPORTED_TRANSFORM` under `@objectstack/core` as well). - -So a consumer reading `ERROR_CODE_LEDGER['@objectstack/rest']` sees seven fewer entries, and one reading the `@objectstack/core`, `@objectstack/types` or `@objectstack/service-automation` key sees the new ones. Nothing to migrate: every code keeps its wire value and its status. diff --git a/.changeset/21109-rls-check-stored-form.md b/.changeset/21109-rls-check-stored-form.md deleted file mode 100644 index 5a74377f9ce..00000000000 --- a/.changeset/21109-rls-check-stored-form.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): a row-level `check` judges a `date`, `datetime` or `time` column as the row will be stored, so the write and the read the same policy scopes give one answer for one row (#21109) - -Clause-②: no - -The write check evaluates the compiled `check` filter in-process against the write's post-image. That image held each value as the caller or a hook wrote it, while the read compares the value the driver stored, in its column's storage form, against a comparand put into the same form. On a temporal column the two forms differ, so one row could get two answers. Measured through `ObjectQL.insert` with `SecurityPlugin` on SQLite, as a member resolving a permission set, with the same predicate as `using` and `check`: - -| `check` | written | write, before | stored | read | -|---|---|---|---|---| -| `record.due_on == '2026-01-05'` | `'2026-01-05T15:00:00Z'`, or a `Date` on that day | 403 | `2026-01-05` | shown | -| `record.start_time == '09:00'` | `'09:00:00'` | 403 | `09:00:00` | shown | -| `record.due_at == '2026-01-05T10:00:00Z'` | `'2026-01-05T18:00:00+08:00'` | 403 | `2026-01-05T10:00:00.000Z` | shown | -| `record.due_on > '2026-01-05'` | `'2026-01-05T15:00:00Z'` | admitted | `2026-01-05` | hidden | - -Now, before the check is judged, every column the object declares `date`, `datetime` or `time` is put into `@objectstack/core`'s `temporalStorageForm`, the rule the drivers write and compare those columns by. That applies to the post-image's value and to the check's value comparands on the column (`$eq`, `$ne`, the orderings, `$in`, `$nin`, `$between`). The first three rows above are now admitted. The last is now refused, which is the read/write agreement this change buys: the read never showed that row, so a write that stores a day the predicate excludes is no longer admitted on the strength of the time of day it was sent with. Every insert, by-id update and predicate update takes the same step. - -Unchanged: a column the object does not declare temporal is judged as written, whatever its value looks like; an object whose schema cannot be loaded is judged as before; a value the storage rule cannot read is judged as written; and refusals keep their code and status (`PERMISSION_DENIED` / 403). diff --git a/.changeset/21113-turso-remote-autonumber.md b/.changeset/21113-turso-remote-autonumber.md deleted file mode 100644 index c62bb3b2daf..00000000000 --- a/.changeset/21113-turso-remote-autonumber.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@objectstack/driver-turso': minor -'@objectstack/driver-sql': patch ---- - -feat(driver-turso): the remote transport issues `auto_number` values (#21113) - -Clause-②: yes (widening) - -A `create()`, `bulkCreate()` or `upsert()` on the Turso REMOTE transport that -leaves an `autonumber` field empty (`undefined`, `null` or `''`) now gets a -generated value. It used to be refused with `NOT_IMPLEMENTED` / 501, so on a -hosted tenant database — which is on this transport — no object declaring an -`auto_number` field could get a new record at all. Nothing is declared anew in -the spec or in the package exports; `supports.autonumber` stays `true` and is -now honoured. - -- The value comes from the same persistent `_objectstack_sequences` counter the - local and embedded-replica transports use, rendered by the same format rules - (`autonumberFormat` / `format`, organization scope, date and `{field}` - tokens), bootstrapped from the table's highest existing value by the same - reading, and re-seeded the same way after rows land above the counter by a - seed replay or import. A remote driver and an embedded replica of one - database draw from one counter row. -- The counter moves in one statement over the connection (`UPDATE … RETURNING`, - or on a cold counter `INSERT … ON CONFLICT (key_hash) DO UPDATE … RETURNING`), - so writers in different processes never draw the same number. -- An `upsert()` that merges into an existing row keeps the number already in - the row; a row that carries its own number is written unchanged. -- A `_objectstack_sequences` table in the pre-`key_hash` shape is refused in - remote mode with `DATABASE_ERROR` / 500 and the remedy in the message (open - the database once through the local or embedded-replica transport, which - migrates it); remote mode does not migrate it and does not key by the legacy - rule. -- `RemoteTransport.upsert()` takes an optional fifth argument naming columns - that are written on insert and left alone on merge. - -`@objectstack/driver-sql`: the sequence rules a second transport shares are -now `protected` members of `SqlDriver` (`resolveSequenceTenantId`, -`defineSequencesTable`, `maxAutonumberCounter`, `escapeLikePrefix`, -`sequencesTableName`, `autoNumberCollisionRetries`). No export is added and no -behaviour changes on any dialect. diff --git a/.changeset/21114-start-signal-forwarding.md b/.changeset/21114-start-signal-forwarding.md deleted file mode 100644 index 0ef829ee59a..00000000000 --- a/.changeset/21114-start-signal-forwarding.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os start` forwards SIGTERM and SIGINT to its `serve` child and takes the child down when it exits - -Clause-②: no - -`os start` runs the server as a separate `serve` child process. It used to -listen for that child's exit and nothing else. A SIGTERM or SIGINT sent to the -`start` process alone (a plain `kill`, `docker stop`, a systemd stop, a CI step) -ended `start` and left `serve` running with no parent. The port stayed bound, -`/health` kept answering 200, and the next start on that port collided with it. - -`os start` now supervises its child the way `os dev` already does, through the -same mechanism: - -- SIGTERM or SIGINT to `start` is forwarded to the child. `start` waits for the - child to shut down, then exits with the child's exit code, which is 0 after a - graceful shutdown. It used to die on the signal at once (shell status 143 for - SIGTERM, 130 for SIGINT) while the child kept running. -- Whatever else ends `start`, the child is sent SIGTERM on the way out. -- A child that exits on its own still ends `start` with the child's exit code, - as before. - -One visible side effect, the same one `os dev` already has: Ctrl-C at a terminal -signals `start` and the child together, so the child now receives SIGINT twice -and logs one `Shutdown already in progress, ignoring SIGINT` warning. The -terminal prompt also returns only after the server has stopped, not before. - -No flag, port, environment variable, banner line or `os dev` behaviour changes. diff --git a/.changeset/21120-stored-metadata-body-closeout.md b/.changeset/21120-stored-metadata-body-closeout.md deleted file mode 100644 index 045782e2933..00000000000 --- a/.changeset/21120-stored-metadata-body-closeout.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/service-analytics': minor -'@objectstack/plugin-audit': minor -'@objectstack/objectql': minor ---- - -fix(security)!: stored metadata bodies are projected or refused at the audit, analytics, realtime and data-door filter/sort exits too - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what three doors accept or serve for the two stored-metadata tables — the generic data door refuses a filter or sort on the body column, the analytics door refuses it as a dimension / measure / filter / sort member, and the realtime event and the audit/activity copy now carry the body as its type's read projection instead of the stored bytes. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What changes.** - -- **Audit / activity copy (`@objectstack/plugin-audit`).** The audit writer copies a `sys_metadata` / `sys_metadata_history` row into `sys_audit_log.new_value` / `old_value` and `sys_activity.metadata`. That copy now projects the body through the shared redactor, so stored credential material is withheld from the second store too. A new `os migrate audit-metadata-bodies` command rewrites the copies already at rest (dry run by default, `--apply` to write, idempotent). -- **Analytics (`@objectstack/service-analytics`).** A query naming the stored body column of these objects as a dimension, measure, filter or sort is refused with `400 INVALID_FIELD`, before any strategy runs — the posture analytics already takes for a member it will not evaluate. -- **Realtime (`@objectstack/objectql`).** A `data.record.*` event projects its `after` / `changes` body through the same redactor, so a subscriber to these objects' events receives no stored credential. -- **Data door filter / sort (`@objectstack/metadata-protocol`).** A filter or sort on the body column is refused with `400 INVALID_FIELD`, the same family and shape as the existing groupBy refusal. - -**What stays answerable.** Every scalar column of these objects — `type`, `name`, `scope`, `state`, timestamps — is still grouped, filtered, sorted, counted and served; only the body column is affected. Every other object is unchanged. diff --git a/.changeset/21120-stored-metadata-body-seam.md b/.changeset/21120-stored-metadata-body-seam.md deleted file mode 100644 index 3082f441c5e..00000000000 --- a/.changeset/21120-stored-metadata-body-seam.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/cli': minor ---- - -feat(spec,cli): shared seam for projecting stored metadata bodies, and the audit rewrite command - -Clause-②: no - -`@objectstack/spec/kernel` gains the family-wide primitives for the -stored-metadata-body security invariant, beside the per-type redactor registry -they build on: `STORED_METADATA_BODY_OBJECTS` / `isStoredMetadataBodyObject`, -the `STORED_METADATA_BODY_COLUMN` / `STORED_METADATA_TYPE_COLUMN` names, and -`redactStoredMetadataBody` / `redactStoredMetadataRow` / `redactStoredMetadataRows`. -These project a stored row's body through the one `getMetadataTypeRedactor` -definition, so every surface that serves, copies or evaluates such a body shares -one rule rather than a copy per package. Additive — no existing export changes. - -`@objectstack/cli` gains `os migrate audit-metadata-bodies`, the one-off rewrite -of at-rest metadata-body copies in `sys_audit_log` / `sys_activity` (dry run by -default, `--apply` to write, idempotent). diff --git a/.changeset/21124-datasource-meta-write-capability.md b/.changeset/21124-datasource-meta-write-capability.md deleted file mode 100644 index 12d4e90645c..00000000000 --- a/.changeset/21124-datasource-meta-write-capability.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/rest': patch -'@objectstack/runtime': patch ---- - -fix(rest,runtime): writing `datasource` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability the datasource admin door already requires (#21124) - -Clause-②: no - -- A write of a `datasource` definition through `/api/v1/meta` (and its plural spelling) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`POST /api/v1/datasources`, `PATCH` and `DELETE /api/v1/datasources/:name`) already requires for the same create, update and remove. Every write verb is judged alike: the save (`PUT /meta/datasource/:name`, a draft save included), the reset (`DELETE`), `/publish` and `/rollback`. -- A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. Nothing is written. The answer is the same whether or not the named item exists. -- The write doors' own authoring admission is unchanged and still applies, so a datasource write needs `manage_platform_settings` and `manage_metadata` both. Platform administrators hold both through `admin_full_access`. Every other metadata type, and every read route, is unchanged. `external_catalog` writes are unchanged: that type's own write door requires `manage_metadata`. -- Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by. -- If you write datasource definitions through `/api/v1/meta` with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or write through the datasource admin door with a caller that already holds it. diff --git a/.changeset/21125-oclif-core-5.md b/.changeset/21125-oclif-core-5.md deleted file mode 100644 index 040861cd6bf..00000000000 --- a/.changeset/21125-oclif-core-5.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -`@objectstack/cli` moves to the `@oclif/core` 5 line: its `@oclif/core` dependency goes from `^4.13.3` to `^5.1.2`. The Command classes the package exports (`CompileCommand`, `ValidateCommand`, `ServeCommand` and the rest) now build on `@oclif/core` 5, so code that extends one of them or runs it beside its own oclif setup should use `@oclif/core` 5 as well. - -Node.js 22 or later is required. Every `@oclif/core` 5 release declares `engines.node` `>=22.0.0`, the same floor this package already declared. - -The `os` commands, flags and output are unchanged. On 5.1.2 every help page renders byte-for-byte as it did on 4.13.3. The published entry also answers `--version`, `--help`, an unknown command, an unknown flag and a refused `--port` with the same exit codes and the same bytes. diff --git a/.changeset/21127-connector-source-live-row.md b/.changeset/21127-connector-source-live-row.md deleted file mode 100644 index 769a69ff361..00000000000 --- a/.changeset/21127-connector-source-live-row.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): a stack whose mapping authors `connectorSource` validates and lints again — the liveness ledger's `live` row no longer carries an author warning - -Clause-②: no - -`os validate` and `os lint` exited 1 on any stack with a `mappings[]` entry that -authored `connectorSource`, and the only output was the liveness lint's internal -error `ledger entry has unrecognised status "live"`. The ledger graded the key -`live` (the connector sync executor reads every key of the binding) and still -asked the lint to warn whoever authored it; the lint has no warning for a key -that works, and stops on that inconsistency by design. The row carries no warning -now, so both commands judge the stack and exit 0 when nothing else is wrong. The -runtime metadata door no longer returns an `authoring-rule-threw` advisory for -the same mapping. - -The note the warning used to carry is on the key's description, where an author -reads it: a pull runs when a `job` drives it, nothing schedules one yet, so the -binding alone moves no rows. The retired `connector.syncConfig` prescription and -the `connector-sync-keys-retired` upgrade entry no longer say that authoring the -binding warns. - -`check:liveness` now refuses a `live` ledger row with `authorWarn: true` at any -depth, and prints how many rows opt into an author warning on every run. A -`planned` row with `authorWarn` still warns. No key, value or default changed. diff --git a/.changeset/21129-relationship-path-measure-type.md b/.changeset/21129-relationship-path-measure-type.md deleted file mode 100644 index 6ec089324fa..00000000000 --- a/.changeset/21129-relationship-path-measure-type.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: a cube measure whose `sql` is a relationship path (`account.name`) is judged by the aggregate × field-type table, described in `fields[]` and presented on the native-SQL strategy by the declaration on the object the path's last hop reaches, as a measure over the cube's own column already was - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept on the native-SQL strategy, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -The column a relationship path names is located by the one hop resolver both strategies join and read it through: the cube's declared join at that path, else the relationship field's declared `reference`. - -FROM → TO, for a `measures` entry that resolves to a cube measure over a relationship path (an authored cube measure, or a compiled dataset's measure over an `include`d relationship): - -- `min` / `max` / `sum` / `avg` over a related field of a type the table refuses for that aggregate (the string family such as `text`, `select`, `lookup`; the JSON-stored, file and `formula` types; and the rest the table lists): FROM, on the native-SQL strategy, `200` with the related column's own value (a string such as `"zeta"`) under `fields[] { type: 'number' }` on SQLite and PostgreSQL, and for `sum` a plausible `0` on SQLite and `500 DATABASE_ERROR` on PostgreSQL; the ObjectQL strategy refused it as a cross-object measure. TO `400 INVALID_FIELD` on both strategies, before either reads anything — the refusal a base-object column of the same type already got. -- `min` / `max` over a related numeric field on PostgreSQL: FROM the exact-decimal string (`"250.000000000000000000000000000000"`) under `fields[] number`. TO the number `250`. -- `min` / `max` over a related `date`, `datetime` or `time` field: FROM `fields[] { type: 'number' }` beside the instant. TO `fields[] { type: 'time' }`. - -**What an author sees now.** `400 INVALID_FIELD`, naming the measure as the request wrote it, the cube, the path, the related object and the type it declares, saying the query was not run, and naming the types the aggregate accepts. The thrown error carries `member`, `param` (`measures`), `cube`, `field` (the path, `account.name`) and `object` (the related object that declares the column). - -**What to write instead.** Aggregate a related field of a type the aggregate accepts, or `count` the rows. A first or last related record by a text value is a sort on a list, not an aggregate. - -**Who is affected.** A dashboard, report or caller that asked `min` / `max` / `sum` / `avg` of such a related column through the native-SQL strategy and read the answer as a real one. No example app and no shipped cube or dataset authors such a pair. A dataset whose measure aggregates such a related field is now refused when its query runs (`INVALID_FIELD`), where its compile check, which reads the base object's declaration, still lets it through. - -**Unchanged.** Every pair the table accepts; a measure over the cube's own column; `count`, and `count_distinct`, which keeps its own door; a related column the host's field metadata cannot describe; an expression `sql` or `*`; a host that wires no `sourceFieldMeta`; and the ObjectQL strategy's refusal of a related-field measure the table accepts (`max` over a related `number`), a capability limit of the engine aggregate — run that query on a native-SQL driver. diff --git a/.changeset/21131-dashboard-widget-options-door.md b/.changeset/21131-dashboard-widget-options-door.md deleted file mode 100644 index 56386ec932d..00000000000 --- a/.changeset/21131-dashboard-widget-options-door.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -`os validate`, `os build` and `os lint` now warn on a dashboard widget `options` key that no renderer reads, by name, as `unconsumed-widget-option` — the check the SDUI page save gate already ran on a `dashboard` node, now run over dashboard metadata (`*.dashboard.ts`, `defineStack({ dashboards })`). - -Clause-②: yes - -**What is flagged.** Every key in a dataset-bound widget's `options` outside the read set `CONSUMED_WIDGET_OPTION_KEYS` from `@objectstack/sdui-parser`: `dateGranularity`, `description`, `limit`, `sortBy`, `sortOrder`, `stageOrder`. `DashboardWidgetOptionsSchema` stays open (`.passthrough()`), so such a key still parses; it just stops being silent. Typical cases are `icon`, `columns`, `format`, `currency`, `color`, `suffix`, `showLegend` and `horizontal`, none of which styles anything on a widget bound to a dataset, and a misspelled declared key such as `sortDirection` or `granularity`. The finding is reported at `dashboards[N].widgets[M].options`, and its message names the key. - -**Where presentation goes instead.** A number's format and currency are the dataset measure's `format` and `currency`; a tile's accent is the widget's `colorVariant`; a chart's look is the widget's `chartConfig`. - -**Same check, same level, same exemptions.** The rule is `validateDashboardWidgetOptions`, exported from `@objectstack/lint` and registered for all three commands. It calls `checkDashboardWidgetOptions` from `@objectstack/sdui-parser` and keeps its `warning` level and `code`. Widgets with no `dataset`, legacy `component` widgets and widgets carrying `suppressWarnings: ['unconsumed-widget-option']` are not flagged. It does not run on the Studio/REST/MCP publish path. - -**What changes for a project.** Nothing is refused, and nothing changes without `--strict`. `os validate --strict` and `os lint --strict` now exit 1 instead of 0 on a stack that was otherwise warning-clean and writes such a key. Across this repository's example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`), no dashboard writes one: zero findings, and no example's exit code changes. To clear the warning, delete the key, or move the intent to the home named above. diff --git a/.changeset/21142-line-items-columns-name.md b/.changeset/21142-line-items-columns-name.md deleted file mode 100644 index 5a35922802b..00000000000 --- a/.changeset/21142-line-items-columns-name.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: a `record:line_items` page block's props are a strict shape, and its columns are the inline grid column contract (#21142) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface: a new `ComponentPropsMap` row judges a props bag nothing judged before. Shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a failing key or column as an advisory `component-props-unknown-key` / `component-props-invalid` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`ComponentPropsMap['record:line_items']`** — new row, `RecordLineItemsProps`. `record:line_items` was the one entry on the string-arm registration ledger (`STRING_ARM_REGISTERED_TYPES`, now empty), so the props gate skipped it as unregistered and any key rode through. The row declares the fifteen keys the console's `LineItemsPanel` reads: `childObject`, `relationshipField` (required), `columns` (required, at least one), `parentObject`, `parentId`, `recordId`, `amountField`, `totalField`, `title`, `readonly`, `minRows`, `maxRows`, `filter` (the ViewFilterRule array), `sort` (the SortItem array) and `limit` (a positive integer). `childObject` may come from the component-level `dataSource` binding instead. An unknown key is named. A near-miss gets its rename (`foreignKey` → `relationshipField`, `filters` → `filter`, …). The four keys of an `object-master-detail-form` detail entry that this block does not read (`addLabel`, `sortField`, `formFields`, `inlineMode`) are refused with the reason. -- **`columns`** references `InlineGridColumnSchema`, the strict, name-keyed column a relationship field's `inlineColumns` takes. The retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`, and a column without `name` is refused. This block draws a column exactly as declared: it does not hydrate `label`, `type` or `options` from the child object's field, so `defineStack`'s identity-only column check does not reach it. -- **New types `RecordLineItemsProps` and `RecordLineItemsPropsParsed`.** They differ because a column's `readonlyWhen` / `requiredWhen` bare-string predicate normalizes to an Expression envelope at parse. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `columns: [{ field: 'title', label: 'Title' }]` | `columns: [{ name: 'title', label: 'Title' }]` | -| `{ childObject: 'invoice_line', columns: [...] }` with no `relationshipField` | `{ childObject: 'invoice_line', relationshipField: 'invoice', columns: [...] }` | -| `columns: []`, or no `columns` | at least one `{ name, label?, type?, … }` column | -| `addLabel`, `sortField`, `formFields` or `inlineMode` on the block | the block without that key | -| any other key the shape does not declare | the block without that key | - -The one-line fix: key every column `name`, give the block its `relationshipField` and at least one column, and remove any key the shape does not declare. - -## Who is affected, measured - -On `origin/main` `1ecb871beb`: one authored `record:line_items` block in the examples, the showcase project detail page. All five of its columns were keyed `field`, so its Tasks grid rendered empty cells; they are keyed `name` in this change. No documentation example authors the block. Deployed metadata was not measured. diff --git a/.changeset/21147-s3-presign-no-checksum.md b/.changeset/21147-s3-presign-no-checksum.md deleted file mode 100644 index afbe7a0e4ac..00000000000 --- a/.changeset/21147-s3-presign-no-checksum.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(service-storage): presigned S3 uploads no longer bake the CRC32 of an empty body into the signed URL (#21147) - -Clause-②: no - -`S3StorageAdapter.getPresignedUpload()` handed the browser a URL carrying -`x-amz-sdk-checksum-algorithm=CRC32&x-amz-checksum-crc32=AAAAAA==`. `AAAAAA==` -is the CRC32 of an empty body: the AWS SDK's default (`WHEN_SUPPORTED`) stamps a -checksum into every `PutObjectCommand` it prepares, and a presign prepares the -command before any byte exists. A store that enforces a query-signed checksum -against the uploaded body refuses every presigned browser upload with a checksum -mismatch, while server-side `upload()` keeps working. Hosted R2 does not enforce -it, which is why this stayed quiet; self-hosted deployments on other -S3-compatible stores may. - -The adapter's `S3Client` now sets `requestChecksumCalculation` and -`responseChecksumValidation` to `WHEN_REQUIRED`. None of the commands the adapter -issues is checksum-required, so the presigned PUT URL carries no `x-amz-checksum-*` -or `x-amz-sdk-checksum-algorithm` parameter, and a presigned GET URL no longer -carries `x-amz-checksum-mode=ENABLED`. - -Two server-side effects of the same setting, measured against a loopback server: -`upload()` and multipart `uploadChunk()` no longer send the client-computed -`x-amz-checksum-crc32` header (the SigV4 `x-amz-content-sha256` payload hash is -unchanged), and `download()` no longer asks the store for, or validates, a -response checksum. Both were added only by the SDK's default change; the older -behaviour is restored. SDKs older than the flexible-checksum release ignore the -two options. diff --git a/.changeset/21154-activity-text-predicate.md b/.changeset/21154-activity-text-predicate.md deleted file mode 100644 index ab22fa593d0..00000000000 --- a/.changeset/21154-activity-text-predicate.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/plugin-audit': minor -'@objectstack/plugin-approvals': minor ---- - -fix(plugin-audit,plugin-approvals)!: a query over the activity stream's value-bearing columns, the compliance ledger's before/after snapshots, or an approval request's snapshot is refused for a reader withheld a field of the objects the query can reach — the one parent object it names, or every object when it names none (#21154) - -Clause-②: no (narrowing) - - - -**BREAKING**: an accept-set narrowing on three engine read middlewares, shipped as `minor` under the launch-window convention. - -**What was wrong.** An activity row carries field values of the record it is about in three columns: its one-line summary, its record label and its recorded change. A compliance-ledger row carries them in its before and after snapshots. An approval request carries the submitted record's snapshot. A read-time redaction narrows such values on the rows a reader is SERVED, after the driver has answered. A filter over the same columns was evaluated at rest, before that, so row presence answered whether the stored text held a value the reader is served masked or not at all, one guess at a time. A grouping by one of them handed the stored text back as the group key. - -**What is refused now.** For a non-system caller, on every door that reaches these objects through the engine (the list and query doors, record export, and any other `find` / `count` / `aggregate`), a query that filters, searches, sorts, groups or aggregates by one of those columns is refused with `403 PERMISSION_DENIED`, in the engine's own words for a field the caller may not query, unless the caller is served every field, as the security service answers it, of the objects the query can reach: - -- when the query names exactly one parent object, by equality on the column that names it, at the root of its filter (or inside a root `$and`): that object; -- when it names none: every object registered in the deployment, the set these rows can concern, read from metadata and never from the rows. - -A grouping or aggregation by such a column answers the engine's aggregate refusal; every other position answers its predicate refusal. Both are followed by one sentence naming the remedy. - -**Who is affected.** A caller withheld any field of the parent object (served masked, gated by a capability it does not hold, or not granted by its permission sets) can no longer filter, search, sort or group by those columns of that object's activity rows, ledger rows or approval requests. A caller withheld any field of any registered object can no longer do so in a query that names no parent object, or names one only inside an alternative — that includes a free-text search over the activity stream or the compliance ledger, whose searched sets include those columns. A caller served every field of the parent it names, or, for a query naming none, of every object (an administrator in a stock deployment), queries as before; the latter costs three security-service calls per registered object per such query. - -**One-line fix:** a caller withheld some field names one parent object it is served in full, by equality in the query's filter; for a parent it is withheld a field of, it reads the rows unfiltered by those columns. - -**Unchanged.** A query that names none of those columns answers as before, for every caller. System-context reads are not judged. A deployment without the security service answers as before: the columns are served whole there, so a filter over them discloses nothing the rows do not. The approvals service door's own search keeps its own rule for the snapshot. diff --git a/.changeset/21155-ledger-snapshot-fields.md b/.changeset/21155-ledger-snapshot-fields.md deleted file mode 100644 index e17211745d2..00000000000 --- a/.changeset/21155-ledger-snapshot-fields.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -fix(plugin-audit): the compliance ledger's before/after snapshots serve a parent field's value only to a reader the security service serves that field (#21155) - -Clause-②: no - -The CRUD mirror writes one `sys_audit_log` row per record write, once, as the system. A create row's after-snapshot, an update row's before/after snapshots of each changed field, and a delete row's before-snapshot carry the parent record's stored field values. The ledger is read through the generic data doors under its own object grant and tenant wall, so a reader whose permission sets grant the ledger read was served every snapshot key. This held for a field served masked to the reader, a field gated by `requiredPermissions` the reader does not hold, and a field a permission set the reader holds marks non-readable. The data plane answered the same reader masked or without the key. - -Ledger readers are not field-unrestricted by default. The snapshots of create, update and delete rows are now narrowed at read time, keyed on the reading caller, through the security service's own answer: the read projection intersected with the query-side answer, whose difference the contract defines as exactly the fields served masked. Every key the reader is not served is dropped. A reader served every field reads the snapshots byte-identical to the row at rest, and system reads are unchanged. An auditor who must see every field is granted that by a permission set that unmasks those fields. - -Rows of other actions are served as written: their snapshot columns are empty, a settings digest, or the administrator roster, and none of them is a parent record's field map. A create, update or delete row whose snapshot cannot be judged key by key (not a JSON object, or no parent object named) loses that snapshot. The activity stream's redaction and this one now share one served-fields helper. diff --git a/.changeset/21156-analytics-caller-member-column-reference.md b/.changeset/21156-analytics-caller-member-column-reference.md deleted file mode 100644 index 4f338e15608..00000000000 --- a/.changeset/21156-analytics-caller-member-column-reference.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/service-analytics": patch ---- - -Clause-②: no - -Security: refuse a caller-named analytics member that is neither a declared cube member nor a column reference at the query door, in every tier — including a deployment with no security service and an object the field-level read gate does not judge — so caller-supplied member text can no longer reach a native statement unjudged. The refusal reuses the existing field-read gate's envelope (`PERMISSION_DENIED` / 403); no new error code, and the declared-cube paths are unchanged. diff --git a/.changeset/21163-autonumber-like-escape.md b/.changeset/21163-autonumber-like-escape.md deleted file mode 100644 index 10b3057e683..00000000000 --- a/.changeset/21163-autonumber-like-escape.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -fix(driver-sql): an autonumber format whose rendered prefix carries `_`, `%` or `\` seeds its counter from the stored MAX on SQLite - -Clause-②: no - -The SQL driver reads the highest counter already stored under an autonumber prefix in two places: the first issue of a counter (the cold bootstrap) and the re-seed after a create collides with a number that a seed replay, an import or direct SQL already wrote. Both escape the prefix's `\`, `%` and `_` with a backslash for a `LIKE` scan, but the scan declared no `ESCAPE` character, and SQLite's `LIKE` has none unless one is declared. On SQLite (better-sqlite3, and the Turso local and embedded-replica faces; the WebAssembly SQLite driver inherits the same scan) such a prefix therefore matched no stored row: - -- **Cold**, the counter started at 1 under numbers already stored. Measured: a format `SO_{0000}` over a stored `SO_0007` issued `SO_0001`. -- **On the re-seed**, the counter could not move, so every retry collided again and the create was refused once the retries ran out. - -The prefix is rendered, so the character can come from data as well as from the format: `{region}-{0000}` with a region value of `north_east` was affected in the same way. - -The scan now binds the driver's one `LIKE` escape character on every dialect, as the driver's filter `LIKE` already does. PostgreSQL and MySQL already used a backslash as their default `LIKE` escape, so the answer there does not change; a prefix with none of the three characters is not affected anywhere. Counters already seeded too low are not rewritten: the next collision on one now re-seeds it from the stored MAX, as on any other prefix. diff --git a/.changeset/21166-remote-upsert-id-insert-only.md b/.changeset/21166-remote-upsert-id-insert-only.md deleted file mode 100644 index d9b99130f10..00000000000 --- a/.changeset/21166-remote-upsert-id-insert-only.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/driver-turso': patch -'@objectstack/driver-sql': patch ---- - -An `upsert` keyed on a business column keeps the stored row's primary key on the Turso remote face, and both drivers answer the stored row. - -Clause-②: no - -**Remote face (`@objectstack/driver-turso`).** `upsert(object, data, ['email'])` on a remote (hosted) database used to replace the matched row's `id`: with the payload's `id` when it carried one, else with a freshly generated one. Every reference to the old id was left pointing at nothing, and no error was raised. The merge now leaves `id` and `created_at` alone, as the local and embedded-replica faces already do. It reads the columns to leave alone from the same list the local faces use, so `id`, `created_at` and the `auto_number` columns are kept on a merge on every face. An upsert on the primary key (no `conflictKeys`, or `['id']`) is unchanged, and an upsert that inserts still writes the payload's `id`, or a generated one. - -**The answer (`@objectstack/driver-sql`, and the remote face).** On such a merge, `upsert` returned the payload instead of the stored row, so the answer carried the payload's `id` (or the generated one), an id no stored row has. It now returns the stored row: its own `id`, with the merged values. The row is read back by the conflict-key values. When a conflict key is empty in the payload, nothing can have matched it, so the row was inserted and it is read back by its `id`, as before. - -To change a row's `id` on purpose, use `update()`. An `upsert` never changes it. diff --git a/.changeset/21174-admin-audit-metadata.md b/.changeset/21174-admin-audit-metadata.md deleted file mode 100644 index 5b76956c8bb..00000000000 --- a/.changeset/21174-admin-audit-metadata.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): the compliance-ledger rows the admin identity endpoints write record the admin's decisions, never a value of a field of the user (#21174) - -Clause-②: no - -The admin create-user and set-user-password endpoints each write their own `sys_audit_log` row beside the rows plugin-audit's CRUD mirror writes for the same call. That row's free `metadata` copied values the call had just written into fields of the user. The ledger's read side narrows the mirror's before/after snapshots to what each reader is served, but it cannot narrow free metadata without deriving masking a second time, so a ledger reader the data plane withholds one of those fields from was served its value through the explicit row. - -The explicit row now carries only the admin's decisions — which operation ran, whether the password was generated, whether the account's address is a generated placeholder, whether the membership was bound and to which organization — plus its reference to the user (`object_name` and `record_id`). The values the call writes into the user's fields are recorded where they already were: on the mirror's `create` and `update` rows for those same writes, in the snapshot columns the read side narrows per reader. The decision set is a closed type, so a field value no longer compiles into the row. - -Migration: a reader that took a user field's value from the explicit row's metadata reads it from the mirror's row for the same write instead (its after-snapshot), served according to the reader's field access. Rows written before this release are stored data and are not rewritten. diff --git a/.changeset/21175-ledger-parent-read-gate.md b/.changeset/21175-ledger-parent-read-gate.md deleted file mode 100644 index bc3342c563a..00000000000 --- a/.changeset/21175-ledger-parent-read-gate.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/plugin-audit': minor ---- - -fix(plugin-audit)!: a read of the compliance ledger returns only the rows about records the caller can read, the same way a read of the activity stream is narrowed (#21175) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a read of `sys_audit_log` returns to every caller that is not system context, admins included. Some rows an admin was served before are no longer served. It ships as `minor` under the repo's launch-window convention for narrowings. - -**What changes.** `AuditPlugin` now mounts the activity stream's parent-record read gate on the compliance ledger. It is an engine middleware, so it narrows `find`, `findOne`, `count` and `aggregate`, which on the generic data doors are the list, its `total`, the by-id read and both query shapes. A ledger row that names a record (`object_name`, `record_id`) is returned only when the caller's own engine read of that record finds it, so the parent object's sharing, RLS and object-level permissions decide. Parent reads are batched, one per parent object. The gate's mechanism is one module shared with the activity stream's gate. - -**Rows no longer served:** - -- to a caller who cannot read the record a row is about: that row; -- to every caller that is not system context, admins included, because the gate has no readable record to judge them by: - - a row about a record that no longer exists: every `delete` row, and every other row about a deleted record; - - a sign-out row, and a sign-in row whose session has since been removed (sign-out removes the session the row names); - - a create, read, update or delete row that names no record, a row naming an object the engine does not know, and a row naming the ledger itself. - -**Unchanged.** The rows stay stored, and system-context reads still return every row. Rows about no record are served as before, under the ledger's own grant: `config_change` rows, the run-level user-import row, the platform-admin standing rows, and an auth event that carried no session id. A caller who can read a record keeps every row about it, and the field-level redaction of the before/after snapshots applies to the rows that are served, as before. A broad read whose pre-scan reaches the gate's 2,000-row bound fails closed and logs a warning, as the activity stream's does. - -**Migration.** No metadata, code or configuration change is needed. A view or report that lists deletions or sign-outs from `sys_audit_log` through the data API now shows fewer rows. A server-side job that must read every ledger row reads it under system context, which this gate does not narrow. diff --git a/.changeset/21177-analytics-inline-dataset-field-admission.md b/.changeset/21177-analytics-inline-dataset-field-admission.md deleted file mode 100644 index 8ab41072ecf..00000000000 --- a/.changeset/21177-analytics-inline-dataset-field-admission.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: a dataset's own `field` text that is not a column reference is refused at the analytics dataset door, inline or saved - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the analytics dataset door accepts. A dataset whose dimension or measure `field` is not a column reference is now refused with `403 PERMISSION_DENIED` instead of being evaluated — an inline dataset and a saved dataset queried by name alike, since both reach the same door. No shipped dataset carries a non-column `field`. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What changes.** The service compiles a dataset into a cube whose members read as declared, so a dimension or measure whose `field` was a raw expression resolved to a declared cube member and was left to the field-level read gate, which stands down with no security service and on an object its reader answers `undefined` for; in those tiers the expression reached the native statement as written. The dataset's own `field` text is now judged at the dataset door, before compile and before any strategy runs, through the field-read gate's existing judge (`PERMISSION_DENIED` / 403, naming the member and never the expression text), for every caller, admin included, and with or without a security service. There is no new error code and no new admission module. - -**What stays answerable.** Every dataset whose fields are columns or relationship paths is unchanged, inline or saved. A saved dataset whose `field` is an expression is refused the same way as an inline one; refusing such a `field` when it is authored belongs to the dataset schema's own retirement of expression fields, not to this door. The dataset's own filter, the selection's runtime filter and cube-query members are lowered into the compiled query and already judged on the query path, so they are unchanged. diff --git a/.changeset/21178-remote-json-column-gate.md b/.changeset/21178-remote-json-column-gate.md deleted file mode 100644 index aa35d6be9dd..00000000000 --- a/.changeset/21178-remote-json-column-gate.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@objectstack/driver-turso": minor ---- - -fix(driver-turso)!: in remote mode, a filter on a declared JSON-stored field is refused with `INVALID_FILTER` / 400 for every operator the local face refuses there, and `$contains` / `$notContains` answer membership instead of a substring of the stored text (#21178) - -Clause-②: yes (narrowing) - - - -**BREAKING** (`@objectstack/driver-turso`, remote mode): this narrows what `TursoDriver` answers when its `url` is a remote libSQL endpoint (such as `libsql://` or `https://`), the transport every hosted tenant database runs on, for every door that compiles a `where`: `find`, `findOne`, `count`, `updateMany`, `deleteMany`, `aggregate` and distinct values. It ships as `minor` under the launch-window convention for accept-set narrowings. Local and embedded-replica mode inherit `driver-sql`'s compiler and already answered this way; nothing moves there. - -**What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a `where` that aims any operator in `@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS` at the field is refused with `INVALID_FILTER` / 400, at any depth under `$and` / `$or` / `$not`, before any statement runs: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin`, `$startsWith`, `$endsWith`, `$icontains`, `$like`, `$ilike`, and implicit equality (`{ "owners": "u1" }`), whatever the comparand, `null` included. - -**What `$contains` / `$notContains` answer now.** Membership: `{ "owners": { "$contains": "u1" } }` matches the rows whose stored list holds `u1` as an element, so it no longer matches a row holding only `u10`; `$notContains` is its exact complement, a row with no value included; and a structured-JSON object answers no member at all, instead of matching text inside its serialization. On a scalar text field both remain the substring test they were. - -**What an author sees now.** The body the local transport answers for the same filter, byte for byte: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, and the full diagnostic, naming both, is written to the driver's logger at `warn`. - -**Why.** The remote transport compiles its own SQL and read neither the shared refused set nor the membership construct, so over a `multiple: true` lookup holding `["u1","u2"]`, `["u2"]`, `["u3","u1"]` and `["u10"]` it answered: `$nin: ["u1"]` and `$ne: "u1"` every row, the rows holding `u1` included; `$eq`, `$in` and implicit equality no row; `$lt` / `$lte` a lexicographic verdict over the serialization; `$startsWith: "["` and `$endsWith: "]"` every row with a value; `$contains: "u1"` the row holding only `u10` too. One driver gave two answers to one filter depending only on the connection string, and the exclusion operators failed open. - -**Who is affected.** A caller, saved filter, list view, report or read scope that reaches a remote-mode `TursoDriver` with one of those operators on a JSON-stored field and read the rows it got as the answer. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", `$not` around either for the exclusion, and `$null` / `$exists` / `$empty` for presence. - -**New optional API.** `RemoteTransport.setJsonColumnResolver(resolver)` in `@objectstack/driver-turso`, which `TursoDriver` wires to its own `isJsonColumn`, beside `setDeclaredValueShapeResolver`. A `RemoteTransport` driven standalone without it treats no column as JSON-stored and compiles as before. - -**Unchanged.** `$contains` and `$notContains` on a scalar field, `$exists`, `$null` and `$empty`; every operator on a field that is not declared JSON-stored; and a table this driver holds no declaration for, where nothing is judged. diff --git a/.changeset/21180-retire-public-picker.md b/.changeset/21180-retire-public-picker.md deleted file mode 100644 index 0ca9b0f2fd6..00000000000 --- a/.changeset/21180-retire-public-picker.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/rest': minor -'@objectstack/lint': patch ---- - -**BREAKING** — an anonymous public form no longer offers record search. The form field's `publicPicker` block (`view.form.sections[].fields[].publicPicker`: `displayFields`, `maxResults`, `filter`, `object`) is removed, and the anonymous lookup route `GET /api/v1/forms/:slug/lookup/:field` is deleted. A public form's `lookup`, `master_detail` and `user` fields are now always left off its anonymous rendering, whatever the form declares. - -Clause-②: yes (narrowing) - -Retired immediately (ADR-0087 D2), with no alias window: the maintainer's ruling reverses the earlier one that had declared the key. Mainstream web-to-lead forms do not let an anonymous visitor search records either, and no example, template, plugin or first-party UI declared or called the picker. - -## FROM → TO - -| you wrote (17.5 and earlier) | write instead | -| --- | --- | -| `{ field: 'account', publicPicker: { displayFields: ['name'], maxResults: 10 } }` on a public form | delete the `publicPicker` block — the field is left off the anonymous rendering anyway | -| a public form whose visitors chose from a short, fixed list of records | a `select` field with static `options` listing the choices | -| a public form whose visitors had to pick an existing record | the same form behind sign-in (an internal form), where the lookup field searches with the signed-in user's own access | -| a client calling `GET /api/v1/forms/:slug/lookup/:field` | nothing to call: the path is no longer registered and answers what any unregistered path answers (`404 ENDPOINT_NOT_FOUND`) | - -**The one-line fix:** delete the `publicPicker` block; an anonymous public form no longer offers record search. Use a `select` field with static `options`, or put the form behind sign-in. - -**What an author who still writes it sees.** `tsc` fails at the authoring site (`FormFieldInput` types the key `never`), and the parse — `defineView()`, `defineStack({ views })`, `os validate`, `PUT /api/v1/meta/view/:name` — refuses it at `…sections[N].fields[N].publicPicker` with the prescription: - -> `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. - -**What a REST client sees.** The two error codes only that route produced, `LOOKUP_NOT_PUBLIC` and `LOOKUP_TARGET_MISSING`, leave the error-code ledger with it. `GET /api/v1/forms/:slug` and `POST /api/v1/forms/:slug/submit` are unchanged apart from the unconditional strip above. - -## The retirement kit - -- **A `retiredKey()` tombstone on the form field**, so the parse carries the prescription instead of a bare unknown-key verdict. The block's own schema and its two types go with it: `FormFieldPublicPickerSchema`, `FormFieldPublicPicker` and `FormFieldPublicPickerParsed` are no longer exported, and `ui/FormFieldPublicPicker` is no longer published as a JSON Schema. -- **The D2 conversion `form-field-public-picker-removed`** (protocol 18, retired from the load path) deletes the key from every form field of every form payload — `sections[]`, `groups[]`, top-level `fields[]` and nested rows. Its D3 record is the semantic entry `form-field-public-picker-retired`, which asks the author how a visitor should now choose. -- **`@objectstack/rest`:** the lookup route and its filter-lowering helper are deleted, and the resolve route's strip of lookup / `master_detail` / `user` fields no longer has an opt-in. -- **`@objectstack/lint`:** the preset-comparand rule no longer reads a picker's `filter` (its claiming reader for that position went with the key). - -## What an operator with a STORED form sees - -A `sys_metadata` view row saved before this release may still carry the key. Nothing breaks at read: the conversion replays on rehydration and strips it, so the view is served canonical and parses, and the field stays off the anonymous rendering either way. `os migrate meta --stored` lists those rows, and `--apply` rewrites them. - - diff --git a/.changeset/21182-object-image-field.md b/.changeset/21182-object-image-field.md deleted file mode 100644 index e294fcbecf3..00000000000 --- a/.changeset/21182-object-image-field.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): an object declares which of its fields is the record's picture — `imageField`, beside `nameField` - -Clause-②: yes (widening) - -`ObjectSchema` accepts one more optional key, `imageField`. It names the field -whose value is the record's picture, the way `nameField` names the field that is -the record's name: one object-level declaration, read by the record page header -(the record chrome every record detail page shares) — not a per-page -`page:header` prop. - -```ts -defineStack({ - objects: [{ - name: 'crm_account', - nameField: 'name', - imageField: 'logo', - fields: { - name: Field.text({ label: 'Name' }), - logo: Field.image({ label: 'Logo' }), - }, - }], -}); -``` - -- **What it may name.** A field of the same object whose type is `image` or - `avatar`. A name the object does not declare, or a field of any other type - (a `text` URL column, a `file`), is refused at parse with an issue at - `imageField` that names the two accepted types — so `defineStack`, - `ObjectSchema.create()`, `os validate` and the metadata save door - (`422 INVALID_METADATA`) all refuse it. -- **An empty field.** The contract the reader is held to: a record whose - picture field is empty shows no picture — no initials or placeholder in its - place. -- **Who draws it.** No renderer reads the key yet. The record chrome in - `@object-ui/components` is the reader to come, and until it lands an authored - `imageField` is accepted, stored and served but nothing draws it. It takes - effect when that renderer ships, with no re-authoring. The liveness ledger - records the key as `planned`. - -Nothing that parsed before is refused: the key is new, and an object that does -not set it is unchanged. diff --git a/.changeset/21185-upsert-cross-org-refusal.md b/.changeset/21185-upsert-cross-org-refusal.md deleted file mode 100644 index 47a42b42c20..00000000000 --- a/.changeset/21185-upsert-cross-org-refusal.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -'@objectstack/driver-sql': minor -'@objectstack/driver-turso': minor -'@objectstack/spec': patch ---- - -fix(driver-sql,driver-turso)!: an upsert whose conflict lands on another organization's row is refused with `UNIQUE_VIOLATION` and writes nothing, and an upsert never changes a row's organization (#21185) - -Clause-②: no (narrowing) - - - -**BREAKING for `upsert` callers on the SQL drivers and on `TursoDriver`.** - -**What changed.** `upsert` resolves its conflict against the whole table, and the -primary key and a `unique: 'global'` column are installation-wide, so the row a -tenant-scoped call (`options.tenantId` on an object with a tenant column) collided -with could belong to another organization. The merge wrote the payload onto that -row, tenant column included. Now: - -- **A tenant-scoped upsert merges only into a row of the organization the row is - written under**, for any conflict target, the primary key included. A conflict - that lands on a row of another organization, or on a row with no organization, - is refused with `code: 'UNIQUE_VIOLATION'`, `status: 409`, and nothing is - written. That is the answer `create()` gets for the same collision: from the - caller's organization the call is an insert, and that insert collides. The - refusal names no organization and no value of the row it collided with. -- **The tenant column is insert-only** (`insertOnlyUpsertColumns`), like `id`, - `created_at` and `auto_number` columns: an upsert with no tenant context merges - into the row it lands on and keeps that row's organization. - -Mechanism, per face: on SQLite, PostgreSQL and the remote (libSQL) face, the merge -statement carries the organization predicate (`DO UPDATE … WHERE`), so another -organization's row is never written. On MySQL, whose `ON DUPLICATE KEY UPDATE` -takes no `WHERE`, the statement and a read of the landed row run in one -transaction (a savepoint inside a caller's transaction), and the read's failure -rolls the write back. The remote face now also stamps the caller's organization on -the row it inserts, as the local faces do. - -## FROM → TO - -| you relied on | now | -|:--|:--| -| a tenant-scoped `upsert` merging into a row of another organization | refused with `UNIQUE_VIOLATION` / 409, nothing written | -| an `upsert` payload's tenant value moving the row it merges into | the row keeps its organization; to move a row between organizations, use `update()` | - -**What is not affected.** A tenant-scoped upsert whose conflict lands on a row of -its own organization merges as before, on every target. An upsert that inserts -lands under the caller's organization, or under the organization the payload -names explicitly, as before. diff --git a/.changeset/21189-text-operator-entry-control.md b/.changeset/21189-text-operator-entry-control.md deleted file mode 100644 index 8d33974ae2f..00000000000 --- a/.changeset/21189-text-operator-entry-control.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the `filter-text-operator-declared-type-refused` migration entry's control no longer counts JSON-stored fields, and it names the separate door that refuses text operators there - -Clause-②: no - -**`filter-text-operator-declared-type-refused` (protocol 18).** The entry's acceptance criteria -named every text-valued field, `multiselect` / `checkboxes` / `tags` and lookup and `user` ids -included, as the control that "must keep answering exactly as before". The declared-type door this -entry registers still leaves every text-valued type alone. A second door, though, judges a field by -how it is STORED: on a column stored as JSON it now refuses `$startsWith`, `$endsWith`, -`$icontains`, `$like` and `$ilike` with `INVALID_FILTER` / `400`, as it already refused the scalar -comparisons there. So a stored filter that uses one of those operators on a multi-valued field -answers that `400` after the upgrade, and the old sentence called it a control. - -The criteria now say four things: - -- The control is a text-valued field that is NOT stored as a JSON column. -- The JSON-stored population is `multiselect` / `checkboxes` / `tags`, any field declared - `multiple: true` (a multi-valued lookup or `user` among them), and, on a SQL deployment still - inside the ADR-0104 dual-encoding window, a single-value file-class field. -- That door refuses every text operator except the membership pair `$contains` / `$notContains`, - and its refusal names no declared type, so it is outside this entry's repair list. -- The repair on a multi-valued field is membership: `$contains` for one member, an `$or` of - `$contains` for any-of. A single-value file-class field answers text operators again once the - deployment finishes the media-column move (the column step of - `objectstack migrate files-to-references --apply`). - -Text only: no entry id, `surface`, `replacement`, `reason`, conversion or refusal changes, and -neither door moves. `objectstack migrate meta` prints the corrected `verify:` line, and the -generated migration registry carries the same text. diff --git a/.changeset/21207-mcp-stdio-stored-metadata-body.md b/.changeset/21207-mcp-stdio-stored-metadata-body.md deleted file mode 100644 index 1d5e3a8f24f..00000000000 --- a/.changeset/21207-mcp-stdio-stored-metadata-body.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/mcp': minor ---- - -fix(mcp)!: the MCP stdio transport serves a stored metadata body only as its type's read projection, and refuses to evaluate it - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what one door serves and accepts for the two stored-metadata tables (the stored row and its version history). On the MCP stdio transport, a read now carries the stored body as its type's read projection instead of the stored bytes, and a call that would evaluate the stored body is refused. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What changes.** - -- **Reads.** The stdio bridge's query and get verbs, and the record resource, serve the stored body through the same projection the generic data door and every metadata read serve: stored credential material is withheld, a body that cannot be judged is omitted, and a credential-free body is served unchanged. -- **Evaluate shapes.** A group, filter, sort or aggregate member on the stored body column is refused with `400 INVALID_FIELD` before the engine runs — the data door's code and envelope. - -**What stays answerable.** Every scalar column of the two tables is still served, filtered, sorted, grouped and counted; only the stored body column is affected, and every other object is unchanged. A member's read of these tables is refused as before. diff --git a/.changeset/21210-widget-options-docblock.md b/.changeset/21210-widget-options-docblock.md deleted file mode 100644 index a59d7efcdac..00000000000 --- a/.changeset/21210-widget-options-docblock.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The `DashboardWidgetOptionsSchema` doc comment no longer says that a misspelled widget `options` key is an author-time type error. The bag ends in `.passthrough()`, so a misspelled key such as `sortDirection` or `granularity` compiles and parses like any other extra key, and it changes nothing at render time. A wrong value for a declared key, such as `sortOrder: 'sideways'`, is the type error and the parse error. `os validate`, `os build` and `os lint` name the misspelled key with the `unconsumed-widget-option` warning, which does not fail any of the three commands. Only the comment changed. The schema's shape is the same. - -Clause-②: no diff --git a/.changeset/21216-action-translation-keyed-children.md b/.changeset/21216-action-translation-keyed-children.md deleted file mode 100644 index ef24283bb12..00000000000 --- a/.changeset/21216-action-translation-keyed-children.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -`os validate`, `os build` and `os lint` now check every keyed child of an action's translation entry against what the action declares, under both `objects.OBJECT._actions.ACTION` and `globalActions.ACTION`. Before this, only `params.NAME` was checked. - -Clause-②: yes - -**What is refused.** Two keys are new `translation-target-unknown` errors, the same code and level an undeclared `params` key already gets: - -- `outcomeMessages.OUTCOME` when the action's own `outcomeMessages` does not declare that outcome, or declares no `outcomeMessages` at all. `translateAction` overlays only the outcomes the action declares, so such copy is never shown. -- `resultDialog.fields.PATH` when no entry of the action's `resultDialog.fields[]` has that literal `path`, or the action declares no `resultDialog` or a dialog with no `fields`. The label lookup is keyed by each declared field's `path`, dots included, so such a label is never shown. The finding's config path quotes a dotted key as one member (`resultDialog.fields["client.ghost"]`). - -**What is warned.** `params.NAME.options.VALUE` under a declared param is judged against that param's inline `options[].value`. It is a `translation-option-key-unknown` warning, the code and level a field's `options` key already gets, and it names the stored value when the key is a display label. An options map under a param with no inline options and no `field` is warned once, because nothing reads it. A field-backed param with no inline `options` inherits its list from the field when the dialog renders, so its option keys are not judged. - -**What changes for a project.** A bundle that carries one of the refused keys now fails `os validate` with exit 1 instead of passing, and `os build` refuses it. The fix is to rename the key to a declared outcome or result-field `path`, declare the outcome or field on the action first, or delete the key. The option-key warning changes an exit code only under `--strict`. The four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and the bundle shipped with `@objectstack/platform-objects` produce no finding on any of these keys, so none of their exit codes change. diff --git a/.changeset/21220-dataset-field-column-reference.md b/.changeset/21220-dataset-field-column-reference.md deleted file mode 100644 index 986d89ebe7d..00000000000 --- a/.changeset/21220-dataset-field-column-reference.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an analytics dataset dimension's and measure's `field` is a column reference — a SQL expression there is refused at parse, as it already is on the cube members a dataset compiles to (#21220) - -Clause-②: yes (narrowing) - -**BREAKING** — shipped as `minor` under the launch-window convention -(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by -this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, -never by the level). - -`DatasetDimensionSchema.field` and `DatasetMeasureSchema.field` — the `field` of -every entry in an ADR-0021 dataset's `dimensions` and `measures` — admit a column -reference only: a field of the dataset's object (`amount`), or a relationship path -of bare identifiers ending in one (`account.amount`, `account.owner.region`); a -measure also admits `'*'` for a count, and a count may still omit `field`. Any -other value — an arithmetic, an aggregate, a `CASE`, a subquery, a function call, -a quoted or `$`-prefixed spelling, a padded or empty string, a broken path — is -refused at `dimensions.N.field` / `measures.N.field` with a prescription, and so -is `'*'` on a dimension. - -Why: the dataset layer was declared to take no raw SQL (ADR-0021 "zero raw SQL / -zero raw expressions") and `field` was documented as a field or a relationship -path, but it was a bare string and parsed anything. The analytics dataset door -already refused an expression `field` on every query (`PERMISSION_DENIED` / 403, -inline or saved), so such a dataset could be saved and never answered — declared, -never enforced (ADR-0049). That door never judged an empty `field`: it skips one, -which is how a `count` measure with `field: ''` kept counting rows on SQLite's -native-SQL path (the D2 repair below). The accept set is the one the cube members a dataset -compiles to already hold: the dataset compiler copies `field` into the member's -`sql` verbatim, and both now read one shared declaration. `'*'` is refused on a -dimension because grouping by every column is no axis — both analytics strategies -answered such a dimension `500`. The rule is a `pattern` in the published JSON -Schema too, so a document validated against `json-schema/**` is judged as the -parse judges it. - -## FROM → TO - -``` -FROM defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', - dimensions: [{ name: 'priority', field: 'priority' }], - measures: [ - { name: 'task_count', aggregate: 'count', field: '' }, - { name: 'done_points', aggregate: 'sum', - field: "CASE WHEN status = 'done' THEN points ELSE 0 END" }, - ] }) - -> parsed; the dataset door refused the expression on every query -TO -> ZodError at measures.0.field and measures.1.field (invalid_format): - `measures[].field` is a column reference: a field of the dataset's object … - - defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', - dimensions: [{ name: 'priority', field: 'priority' }], - measures: [ - { name: 'task_count', aggregate: 'count' }, - { name: 'done_points', aggregate: 'sum', field: 'points', filter: { status: 'done' } }, - ] }) -``` - -A conditional count or sum is a measure with its own structured `filter`; a -ratio, sum, difference or product of measures is `derived: { op, of: [...] }` -over measures named in the same dataset. **Mind the scale:** a `derived` ratio is -a 0–1 fraction, so an expression that multiplied by 100 returned percentage -points — pair the ratio with a `%` numeral pattern. A dimension that bucketed a -column with an expression has no expression form: group by the column itself, or -keep the bucket as a field of the object and name that field. - -**The one-line fix:** parse each dataset; every refusal at `…field` is one member -to change — name the column, omit `field` on a plain count (never `field: ''`), -or move the computation to a measure `filter` or a `derived` measure. The one -mechanical case is done for you: `os migrate meta --from 17` lists, and every -stored-row rehydration replays, the D2 conversion -`dataset-count-measure-empty-field-removed`, which drops a `count` measure's empty -`field` (it still counts rows). Nothing else has a mechanical rewrite. - -**What an author who still writes it sees.** `DatasetSchema`, `defineStack({ -datasets })` (`STACK_SCHEMA_INVALID` / 422), the `dataset` write door and -`POST /api/v1/analytics/dataset/query` (which parses every dataset it is handed, -inline or saved, and now answers `400 VALIDATION_FAILED` at the path where it -answered `403 PERMISSION_DENIED` before) refuse the member at its `field` path -with the prescription. `tsc` does not: the key's type is still `string`. - -## The retirement kit - -- **Schema.** `ui/dataset.zod.ts` holds both keys to the pattern; the pattern is - declared once, in the non-public `data/analytics-column-reference.ts`, and the - cube layer's `CUBE_MEMBER_SQL` is that same `RegExp`. A dimension's pattern is - the same column path without the `'*'` arm. A column reference parses - byte-identically to before. -- **ADR-0087.** D2 carries the one lossless repair: the conversion - `dataset-count-measure-empty-field-removed` (`retiredFromLoadPath`, so an author - is refused at parse while stored rows and `os migrate meta` replay it) drops a - `count` measure's `field: ''`, which compiles to `COUNT(*)` without it. The D3 - entry `dataset-member-field-expression-refused`, linked to that conversion and - with its step-18 rationale fragment, carries the rest — a non-count measure or a - dimension with `''` and every expression have no mechanical rewrite into a - column. No `RETIRED_KEYS_BY_MAJOR` row: no key left the shape, so the - authorable-surface, api-surface and JSON-schema manifest ratchets are - unchanged. -- **Liveness.** The `dataset` ledger rows `dimensions.field` and - `measures.field` stay `live`, re-verified, with the narrowing recorded. -- **Docs.** The `ui/dataset` reference page is regenerated. -- **Runtime.** Unchanged: the analytics dataset door's refusal stays as defence - in depth for a dataset that reaches the service without meeting the parse — a - row stored before this change, which the build probe hands over as read. - -## Reach, measured - -- This repository: no authored dataset carries a non-column `field` — the - examples, `platform-objects`, the hand-written docs and the published skills - were read. Two test fixtures that sent an expression `field` on purpose were - re-pinned: the service door's test builds them unparsed, and the REST route's - test now expects the route's `400`. -- Studio's dataset inspector (objectui) seeds a new dimension or measure row with - `field: ''`. A plain count saved that way parsed before; its query answered - `500` on the ObjectQL path, while SQLite's native-SQL path accepted the - `COUNT()` it compiled to. A row already stored that way is repaired on load by - the D2 conversion above. A NEW save of that shape is refused at the save door - with the prescription to omit the key, because the write path parses with the - current schema and replays no conversion; the producer-side change is - objectui's. -- Out-of-repo authored datasets: NOT MEASURED. - - diff --git a/.changeset/21226-remote-doors-tenant-scope.md b/.changeset/21226-remote-doors-tenant-scope.md deleted file mode 100644 index 8b2eecb78b6..00000000000 --- a/.changeset/21226-remote-doors-tenant-scope.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@objectstack/driver-turso': minor -'@objectstack/spec': patch ---- - -fix(driver-turso)!: a tenant-scoped call on the remote (libSQL) face reaches the rows the local face reaches, and a remote `create` stamps the caller's organization (#21226) - -Clause-②: no (narrowing) - - - -**BREAKING for callers of a remote-mode `TursoDriver` that pass `tenantId`.** - -**What changed.** The engine hands every driver the caller's organization as -`DriverOptions.tenantId`, and the group posture's membership set as `tenantIds` -(ADR-0131 D8). The local face applies them through `SqlDriver.applyTenantScope` -on every read and on every update and delete predicate, and stamps the -organization on a new row. The remote face's doors received no driver options, -so their statements carried the caller's filter and nothing else. Now: - -- **`find`, `findOne`, `count`, `aggregate`, `update`, `delete`, `bulkUpdate`, - `bulkDelete`, `updateMany` and `deleteMany` carry the caller's tenant scope on - the remote face.** The predicate is not a second copy: the remote face asks the - local face's own chokepoint for it and ANDs what that compiles to onto each - statement. So the rows a scoped call reaches are the same on both faces: the - caller's organization, rows with no organization, and, under the group posture, - the caller's membership set. -- **A remote `create` (and `bulkCreate`) stamps the caller's organization** on a - row that names none, as the local `create` does. An explicit value on the row - is kept. -- **`distinct` still refuses a tenant-scoped call on the remote face**, as before. -- A scope the remote face cannot read is refused (`INTERNAL_ERROR` / 500), never - sent without the scope. - -Where the engine's tenant wall composes a predicate above the driver, it already -kept other organizations' rows out of these answers. Where it composes none (the -posture in which that wall is inert, or an elevated caller that carries its -organization), the driver scope is the only fence, and the remote face had none. - -## FROM → TO - -| you relied on | now | -|:--|:--| -| a tenant-scoped remote `find` / `findOne` / `count` / `aggregate` reading another organization's rows | those rows are excluded (`findOne` answers `null`); call without `tenantId` to read every organization, as on the local face | -| a tenant-scoped remote `update` / `delete` by id reaching another organization's row | `update` answers `null` and `delete` answers `false`, and the row is untouched | -| a tenant-scoped remote `updateMany` / `deleteMany` / `bulkUpdate` / `bulkDelete` reaching another organization's rows | only rows in scope are written; the count reports them | -| a tenant-scoped remote `create` landing a row with no organization | the row carries the caller's organization; to write a row with none, call without `tenantId` | - -**What is not affected.** A call without `tenantId`, or on an object with no -tenant column, sends the same statement as before. The local and embedded-replica -faces are unchanged. A scoped call's answer for the caller's own rows, and for -rows with no organization, is unchanged. diff --git a/.changeset/21227-create-reads-back-stored-row.md b/.changeset/21227-create-reads-back-stored-row.md deleted file mode 100644 index e837d2e5dcb..00000000000 --- a/.changeset/21227-create-reads-back-stored-row.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -On MySQL, `SqlDriver.create` and `SqlDriver.bulkCreate` now answer the rows they stored (#21227). - -Clause-②: no - -MySQL has no `INSERT … RETURNING`. knex drops the clause on the MySQL family and answers the insert id instead, so `create` answered `0` and `bulkCreate` answered a one-element array whatever the row count, although every row was stored. Callers that use the answer failed one layer up: on a MySQL datasource, sign-up answered `400 FAILED_TO_CREATE_USER` with the user stored and no account, the dev admin seed failed, and a multi-row `bulkCreate` through the engine was refused after its rows had landed. - -On the MySQL family both doors now read the rows back by the ids they wrote, under the tenant the rows were written with, inside the caller's transaction when there is one: one extra `SELECT` per `create` and per `bulkCreate` batch. SQLite and PostgreSQL still answer from `RETURNING`, with no extra statement and no change in what they answer. If a written row is gone before it can be read back (deleted in between by another statement or a trigger), the call throws `DATABASE_ERROR` (500) and does not retry the insert. - -Nothing to change in a project. Code that read the record from the result now gets it on MySQL as on the other dialects. diff --git a/.changeset/21232-json-door-undeclared-join-hop.md b/.changeset/21232-json-door-undeclared-join-hop.md deleted file mode 100644 index 7906552ab75..00000000000 --- a/.changeset/21232-json-door-undeclared-join-hop.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: a grouped dimension or a `count_distinct` measure over a JSON-stored column reached through a relationship path the cube declares no join for is refused with `INVALID_FIELD` / 400 at the analytics door, as the same member over a declared join already was - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -The column of a dotted path is now located by the one hop resolver both strategies join and read it through: the cube's declared join at that path, else the relationship field's declared `reference`, else the relationship's own name for a host that cannot answer. Before, the door read the cube's declared joins alone and stood down on a path the cube declares no join for. This reverses one clause of the earlier entries for this door in the same release, which listed such a path as unchanged. - -**Before and after**, measured on a configured cube over an object whose lookup the cube declares no join for — `owner`, declaring `reference` a person object — and a member over that lookup. An ad-hoc query's inferred cube declares no join at all, and a dotted dimension on it (`owner.prefs`) now gets the same refusal: - -- A `dimensions` entry, or a `timeDimensions` entry with a `granularity`, over a structured-JSON field (`owner.prefs`, `json`) or a multi-value field (`owner.labels`, `tags`; or a field declared `multiple: true`). Before, on the native-SQL strategy: `200` with one group per serialized value on SQLite and `500 DATABASE_ERROR` on PostgreSQL; the ObjectQL strategy answered `400 INVALID_FIELD` from the engine under its own position (`groupBy[1]`), a name the request never wrote. Now: `400 INVALID_FIELD` from this door on both strategies, before either reads anything. -- A `count_distinct` measure over the same columns. Before, on the native-SQL strategy: `200` with a count of serialized values on SQLite and `500` on PostgreSQL; the ObjectQL strategy refused it as a cross-object measure. Now: the same `400 INVALID_FIELD` from this door. - -**What an author sees now.** The refusal the same member over a declared join already got: `400 INVALID_FIELD`, naming the member as the request wrote it, the cube, the path, the object the lookup declares as its target and the column's declared type, saying the query was not run, and naming the route. The thrown error carries `member`, `param` (`dimensions`, `timeDimensions` or `measures`), `cube`, `field` (the path, `owner.prefs`) and `object` (the target object). - -**What to write instead.** Group by, or count distinct, a related field that stores one scalar value. For a multi-value field, run a record query on the target object filtered by one member with `$contains`, one query per member. - -**Who is affected.** A dashboard, report or caller that grouped or counted distinct such a related column through a lookup the cube declares no join for, on SQLite, and read the serialized values as real groups or a real count. On PostgreSQL the same queries were already a 500. No example app and no shipped cube or dataset authors such a member. - -**Unchanged.** Every member over a declared join; a scalar related column (`owner.email`), which is served on both strategies; a related column whose object the host's field metadata does not describe; an expression `sql`; a host that wires no `sourceFieldMeta`; and a dataset dimension over an `include`d relationship, whose join the dataset compiler declares. diff --git a/.changeset/21238-multi-value-wrap-one-rule.md b/.changeset/21238-multi-value-wrap-one-rule.md deleted file mode 100644 index a0c0b1d40f6..00000000000 --- a/.changeset/21238-multi-value-wrap-one-rule.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/core': minor -'@objectstack/objectql': patch -'@objectstack/plugin-security': minor ---- - -fix(plugin-security): a row-level `check` judges a lone scalar written to a declared multi-valued field as the one-member list it is stored as, so the write and the read the same policy scopes give one answer for one row (#21238) - -Clause-②: yes (widening) - -The write door stores a lone scalar sent to a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `lookup` / `user` / `file` / `image` flagged `multiple: true`) as a one-member list: `tags: 'x'` is stored as `["x"]`. The row-level write `check` judged the value as sent on the insert and on a by-id update, because both images are formed before the write door runs. Measured through `ObjectQL.insert` with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set, with the same predicate as `using` and `check`: - -| `check` | written | write, before | stored | read | -|---|---|---|---|---| -| `record.tags.contains('x')` | `'x'` | 403 | `["x"]` | shown | -| `!record.tags.contains('x')` | `'x'` | admitted | `["x"]` | hidden | -| `record.tags.contains('x')`, a by-id update | `'x'` | 403 | `["x"]` | shown | - -Now the image's value on every field the object declares multi-valued goes through the same rule the write door stores it by, before the check is judged. The first and third rows are admitted. The second is refused: a policy that forbids a member from tagging a row `x` can no longer be passed by sending `'x'` instead of `['x']`. A lone scalar now gets exactly the verdict its stored list gets, on the insert, a by-id update and a predicate update. That includes a policy that compares such a field with a scalar comparison (`==`, `!=`, `in`, an ordering), which the read refuses with `INVALID_FILTER` / 400: there `'x'` used to get the opposite of the verdict `['x']` got, and now gets the same one. - -Unchanged: a field the object does not declare multi-valued is judged as written; a list, `null`, a blank string and an object are judged as written, as the write door leaves them; the check's comparands are left as written, since `contains` takes one member; and refusals keep their code and status (`PERMISSION_DENIED` / 403). - -**`@objectstack/core`** (one new root export, so `minor`; this export is the widening the `Clause-②: yes (widening)` line declares): `multiValueStorageForm(value)`, the rule itself. It wraps a string, a number or a boolean into a one-member list and returns every other value as the same value. `@objectstack/objectql`'s `normalizeMultiValueFields` now calls it, with no change in what the write door stores (`patch`). `@objectstack/plugin-security` is `minor` because the set of writes its check admits widens (the first and third rows above); that is a security-floor behaviour change, not the declared widening. diff --git a/.changeset/21241-mysql-now-default-precision.md b/.changeset/21241-mysql-now-default-precision.md deleted file mode 100644 index 8e0e6a5c1a6..00000000000 --- a/.changeset/21241-mysql-now-default-precision.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -On MySQL, a table that declares a `Field.datetime` with `defaultValue: 'NOW()'` is now created (#21241). - -Clause-②: no - -On MySQL the driver builds a declared `Field.datetime` column as `DATETIME(3)`, but it gave the column's `NOW()` default a bare `CURRENT_TIMESTAMP`, which has precision 0. MySQL refuses a `CURRENT_TIMESTAMP` default whose precision differs from its column's (`Invalid default value for '…'`). The whole `CREATE TABLE` failed, and so did `ALTER TABLE … ADD` for a new field. The object's data endpoints then answered `500`. Two platform tables were affected: `sys_activity` and `sys_presence`. Record writes still succeeded, but none of them got an activity-timeline row. - -The default now carries the column's precision. It is `CURRENT_TIMESTAMP(3)`, the expression the builtin `created_at` / `updated_at` columns already used, and both now read one precision setting. PostgreSQL and SQLite emit the same DDL as before. - -One older case is fixed in the same place. A database created before datetime columns became `DATETIME(3)` holds them as `TIMESTAMP`. Schema sync widens those columns with `ALTER TABLE … MODIFY`, and that statement restated the default of `created_at` / `updated_at` but dropped the default of a declared `NOW()` field. After the widening, an insert that left the field out stored `NULL`. The widening now restates that default too, with the same expression. - -Nothing to change in a project. On the next boot, schema sync creates any table that failed before. No other migration is needed: on MySQL, no table could have been created with the refused default. A column that an earlier widening already left without a default does not get one back. diff --git a/.changeset/21249-native-sql-base-column-qualify.md b/.changeset/21249-native-sql-base-column-qualify.md deleted file mode 100644 index 874234df533..00000000000 --- a/.changeset/21249-native-sql-base-column-qualify.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/service-analytics": patch ---- - -fix(service-analytics): on the native-SQL strategy, a base-table column is qualified with its table whenever the statement joins a related object, not only when the cube declares a join - -Clause-②: no - -A cube that declares no join still joins a lookup's declared `reference` when a query names a relationship path through it (`owner.email`). The native-SQL strategy qualified base-table columns only for a cube that declares a join, so it wrote them bare beside the joined object. When that object declares a column of the same name, the database refused the statement as ambiguous, and `POST /api/v1/analytics/query` answered `500 DATABASE_ERROR` on SQLite and on PostgreSQL. The ObjectQL strategy answered `200` for the same query. - -**Before and after**, measured on a configured cube over a `deal` object that declares no join, whose lookup `owner` points at a person object that also declares `note`, `amount`, `closed_on` and `id`: - -- Dimensions `note` and `owner.email`, with or without a `where` on `note` and an `order` by it: `500` → `200`, one group per (deal note, owner email). -- A `sum` over `amount`, a `timeDimensions` window on `closed_on`, or a `where` on `id`, each grouped by `owner.email`: `500` → `200`. -- An ad-hoc query over the object, whose inferred cube never declares a join: the same. - -The strategy now reads what the statement actually joins, from the one relationship-path resolver, and qualifies every base column in the select list, the grouping, the filters, the measures and the time windows. A statement that joins nothing keeps bare columns. That is now also true on a cube that declares a join when the query uses none of it: the statement it shows on `POST /api/v1/analytics/sql` reads `note` where it read `"deal"."note"`, and the answer is the same. - -**Unchanged.** The ObjectQL strategy; every query on a cube that declares the join it uses; every statement that joins nothing on a cube that declares no join; the refusals. diff --git a/.changeset/21259-undeclared-audit-timestamp-mysql.md b/.changeset/21259-undeclared-audit-timestamp-mysql.md deleted file mode 100644 index 07f119def5d..00000000000 --- a/.changeset/21259-undeclared-audit-timestamp-mysql.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -On MySQL, a write to an object that does not declare `created_at` or `updated_at` no longer fails with `Incorrect datetime value … for column 'updated_at'`. The driver creates both columns on every table it builds, and the engine stamps both on every insert as ISO-8601 text (`2026-10-01T21:49:27.479Z`). Only a column the object declared as `Field.datetime` was rewritten into the `2026-10-01 21:49:27.479` form MySQL accepts. The engine declares both columns on most objects, but not on an object with `managedBy: 'better-auth'` or `systemFields: false`, so those writes were refused. - -Clause-②: no - -**What this fixes.** `sys_jwks` declares `created_at` only, so on MySQL the JWT signing key was never stored. `GET /api/v1/auth/jwks` and `GET /api/v1/auth/token` answered 500, `get-session` carried no `set-auth-jwt` header, and no OIDC or MCP token could be issued. `sys_member` failed the same way, so the seeded admin had no organization membership. Now both answer 200, the key is stored, and the membership is stored. In the CRM example's boot, 9 objects declare `created_at` without `updated_at` and 2 declare neither. Every write door formats the column: `create`, `bulkCreate`, `upsert`, `update` and `updateMany`. - -**SQLite and PostgreSQL.** The value the engine stamps is bound unchanged on both, so their behaviour is the same. One input shape changes on SQLite: a JS `Date` written to an undeclared audit column is now stored as the canonical ISO text, as it already is for a declared `Field.datetime`. Before, it was stored as epoch milliseconds and read back as a number. A column the object declares keeps its declared type. diff --git a/.changeset/5930-shared-filter-lowering.md b/.changeset/5930-shared-filter-lowering.md deleted file mode 100644 index 2083d4b500c..00000000000 --- a/.changeset/5930-shared-filter-lowering.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/objectql': patch -'@objectstack/plugin-security': minor ---- - -feat(spec, objectql, plugin-security): one shared filter lowering, run once at the engine and RLS seams (ADR-0053 D-D1, amended) - -Clause-②: yes - -`@objectstack/spec/data` exports `lowerFilterCondition(filter, options?)` and its `FilterLoweringOptions` type. It is not exported from the package root entry. It is a pure `FilterCondition → FilterCondition` rewrite that applies three rules once: - -- `$between` becomes `$gte` its minimum and `$lte` its maximum. -- A `$lte` whose comparand is a bare `YYYY-MM-DD` day becomes `$lt` the next day, in the calendar-string domain. On the last supported day (`9999-12-31`) a lone `$lte` becomes `{ $null: false }`, and a `$between` keeps only its minimum. -- The NULL-polarity guards the drivers already compile. A `$ne` of a value, a `$nin` or a `$notContains` holds for a row with no value. Every leaf of a `$not` operand is made total. - -The rewrite is copy-on-write, idempotent and never refuses. A node it rewrites keeps its filter-subtree provenance mark. With `options.isDatetimeColumn` (a typed seam), the first two rules change only a declared `datetime` column. Without it they apply to every column. - -As ADR-0053 D-D1 (amended 2026-09-30) requires, the seams now run it once, after the comparand doors and after filter-token resolution: - -- **`@objectstack/objectql`** runs it on every filter position, typed by the object's declared fields. That covers `where` on `find`, `findOne`, `count`, `update` and `delete`, and `aggregate`'s `where`, `aggregations[i].filter` and `having`. `having` is typed by the aggregated row's columns, so `max` of a `datetime` field counts as a `datetime`. Drivers receive the lowered filter. A date macro such as `{today}` is resolved before the lowering reads it. -- **`@objectstack/plugin-security`** runs it on every compiled RLS policy filter (`using` and `check`), right after the two comparand faces. `SecurityPlugin` now hands the compile seam the object's declared `datetime` columns (`RlsFieldGuard.datetime`). A guard without that set treats no column as `datetime`. - -Row answers stay the same on every driver. Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input. One result changes. The engine evaluates `aggregate`'s `aggregations[i].filter` and `having` itself, and that evaluator now treats a row or group with no value the way every driver's `where` already does. It no longer counts such a row in a `$between` on a `datetime` column. It now keeps such a row under a `$not` over an ordering such as `$lt`. - -Nothing is removed or renamed, and there is nothing to migrate. diff --git a/.changeset/cli-provenance-anchors.md b/.changeset/cli-provenance-anchors.md deleted file mode 100644 index 8288fac092a..00000000000 --- a/.changeset/cli-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -Provenance comments in `@objectstack/cli` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Two strings -move with them: the `os i18n extract --source-hashes` help text now says what -the provenance companion records instead of citing a number, and the header -that flag writes into each `.source-hashes.generated.ts` cites the -commit that introduced the companion. No command, flag, exit code, error code, -type, export or runtime behaviour changes. diff --git a/.changeset/client-provenance-anchors.md b/.changeset/client-provenance-anchors.md deleted file mode 100644 index 8c594e58321..00000000000 --- a/.changeset/client-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/client': patch ---- - -Provenance comments in `@objectstack/client` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no request, route, error code, type, export or runtime behaviour changes. diff --git a/.changeset/cloud-connection-provenance-anchors.md b/.changeset/cloud-connection-provenance-anchors.md deleted file mode 100644 index 36304ce36af..00000000000 --- a/.changeset/cloud-connection-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/cloud-connection': patch ---- - -Provenance comments in `@objectstack/cloud-connection` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no route, error code, refusal text, type, export or runtime behaviour -changes. diff --git a/.changeset/console-31971ff1e28f.md b/.changeset/console-31971ff1e28f.md deleted file mode 100644 index 54507fa9391..00000000000 --- a/.changeset/console-31971ff1e28f.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `31971ff1e28f`. This pin carries objectui#11353 (objectui `31971ff1e`): the console bundles one zod instance again, so the Studio's spec-derived forms render their fields — the New Package dialog creates a package and the dashboard and report inspectors show the spec schema. The previous pin shipped objectui's zod 4.4.3 beside the injected spec's 4.6.1. That commit carries no objectui changeset, so it is listed under "declared nowhere" below. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 56 releasing of 58 changesets added across 44 non-merge commits; omitted: 2 release-nothing changesets, 3 commits carrying no changeset (they ship no package code). - -- **minor** — `FormulaFieldMetadata` declares `@objectstack/spec`'s `expression` in place of `formula`, and three readers of a lookup's display pointer read the spec's `displayField` alone (obj… (objectui `19f484f54`) -- **minor** — **BREAKING** — feat(types): an authored `object-gantt` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 6) (objectui `db0beb2aa`) -- **minor** — `object-form.layout` publishes only `vertical` and `horizontal`. This is objectui#11168 slice 3 and delivers objectui#7759 group C. `@objectstack/spec` 17.5.0 retired `inline` and… (objectui `17dc16793`) -- **minor** — The form layout mirrors state the two values the spec and the renderers honour. This is objectui#11168 slice 3 and delivers objectui#7759 group C. (objectui `17dc16793`) -- **minor** — **BREAKING** — feat(layout): a navigation label written as an inline locale map renders in the viewer's locale; three inert resolver props retire (objectui#11299) (objectui `770cc5ba4`) -- **minor** — fix(types): a navigation entry's `label` accepts an inline locale map, as the spec does (objectui#11299) (objectui `770cc5ba4`) -- **minor** — **BREAKING** — feat(types): an authored `object-chart` takes its props in the `properties` bag; the flat spelling is refused by name (objectui#11276) (objectui `5262f7dd3`) -- **minor** — `navigation` is declared on the `object-timeline` block, by reference to `@objectstack/spec` (objectui#8654). This is the timeline arm of the objectui#8652 maintainer ruling: the… (objectui `95bd23c90`) -- **minor** — feat(types): a node bound through `dataSource.object` needs no `objectName` on the validator (objectui#11117) (objectui `0e6e76bc4`) -- **minor** — feat(types)!: retire `data-table`'s `selectionStyle` and `chatbot`'s `floatingConfig` on both faces, and stop teaching `data-table`'s inline-edit flags as authored keys (objectui#… (objectui `b5b928ab0`) -- **minor** — fix(components): a related list's row menu shows an action whose `visible` is blank, as its toolbar does (objectui `be5211522`) -- **minor** — **Breaking (types only):** the `ActionGroup` interface is removed from `@object-ui/types` (objectui#11168, slice 2). Nothing in this repository imported it. (objectui `cd5b19a7e`) -- **minor** — `element:definition-list`, `element:repeater` and `action:button` publish their inputs as the `@objectstack/spec` 17.5.0 rows declare them and as their renderers read them (object… (objectui `cd5b19a7e`) -- **minor** — **BREAKING (authoring, TypeScript only):** `chartConfig.aria` on a dashboard widget is now a compile error, the same verdict the validator already gives (objectui#4044). (objectui `f3c2bb0f9`) -- **minor** — **BREAKING** — feat(types): an authored `object-map` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 5) (objectui `997ce38cb`) -- **minor** — The text-family field types and every length reader use `@objectstack/spec`'s own `minLength` / `maxLength`, and the snake_case `min_length` / `max_length` are retired at once, wi… (objectui `dd5ff190e`) -- **minor** — `element:text` takes the nine `variant` values `ui:text` publishes (`h1`-`h6`, `body`, `caption`, `overline`) and renders each one the way `ui:text` does (objectui#7450). (objectui `caa0cd392`) -- **minor** — **BREAKING** — `showFilters` is retired on `object-grid` (objectui#11068). (objectui `582edef1c`) -- **minor** — fix(plugin-dashboard): a served dashboard draws its own title, description and sub-caption, not the packaged catalog's (objectui#11295) (objectui `b28bde8a4`) -- **minor** — `navigation` is declared on the `object-kanban` and `object-calendar` blocks, by reference to `@objectstack/spec` (objectui#8652). This is the objectui half of the maintainer ruli… (objectui `d79f525d9`) -- **minor** — **BREAKING** — The object-metadata write guard now holds a `select` / `radio` field that has no option source. (objectui `1563d3e10`) -- **minor** — **BREAKING** — The Field Designer's drawer no longer offers `select` as a field type for a new field, or as a type to change an existing non-choice field into. (objectui `1563d3e10`) -- **minor** — A related list inside a record now places its child object's `record_related` actions on each row (objectui#11270; the renderer half of the enforce answer on objectstack-ai/object… (objectui `a8b988933`) -- **minor** — **BREAKING** — feat(types): an authored `object-form` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 4) (objectui `e3782d26e`) -- **minor** — **BREAKING (authoring):** `assignedProfiles` on a `page` node is now refused on both published faces (objectui#9409). (objectui `02f1813f8`) -- **minor** — `JoinedReportBlock` is now the spec's own type, derived from the installed `@objectstack/spec` instead of hand-written (objectui#10940). It is `JoinedReportBlock` from `@objectsta… (objectui `92970c4d6`) -- **minor** — **BREAKING** — fix(core): `ActionDef` no longer declares `aria`, which `@objectstack/spec` 17.5.0 retired on an action (objectui `e2271568f`) -- **minor** — feat(types): four declared keys nothing honoured are retired on both faces, and two chatbot keys gain their zod mirror (objectui#6152, round 4) (objectui `3e4fa2cfb`) -- **minor** — A blank gate predicate is diagnosed on the three paths that still drew it in silence, and objectui's two gate mirrors whose protocol key refuses a blank now refuse it too (objectu… (objectui `58da8aeca`) -- **patch** — fix(plugin-detail): `record:quick_actions.requiredPermissions` publishes what the gate does — the contract's shared record-block describe, verbatim (objectui `fd6f5da44`) -- **patch** — perf(console): the first screen no longer downloads the spec entries only the metadata designers' validation uses (objectui `993f27e60`) -- **patch** — docs(plugin-gantt): authored `object-gantt` examples write their props in the `properties` bag (objectui#10859, batch 6) (objectui `db0beb2aa`) -- **patch** — The page-block inspector no longer offers `Inline` and `Grid` for an `object-form`'s `layout` (objectui#11168 slice 3, objectui#7759 group C). `@objectstack/spec` 17.5.0 refuses b… (objectui `17dc16793`) -- **patch** — fix(app-shell): a Studio pillar whose draft load is cancelled no longer leaves its canvas on "Loading…" (objectui#11331) (objectui `bef9f204a`) -- **patch** — fix(app-shell): a navigation label written as an inline locale map shows in the viewer's language across the console (objectui#11299) (objectui `770cc5ba4`) -- **patch** — chore(plugin-designer): follow `@object-ui/layout`'s and `@object-ui/types`' navigation label changes (objectui#11299) (objectui `770cc5ba4`) -- **patch** — fix(runner): the sidebar no longer reads the app's retired `version` key (objectui `47e3ce008`) -- **patch** — fix(plugin-gantt, plugin-calendar): the `objectName` input description names the `dataSource.object` binding (objectui#11117) (objectui `0e6e76bc4`) -- **patch** — fix(plugin-grid): a grid row shows an action whose `visible` is blank, as the action's toolbars do (objectui `be5211522`) -- **patch** — `object-calendar` is taught with its `calendar` block, not with flat field-name keys (objectstack-ai/objectui#8831). (objectui `50583364a`) -- **patch** — docs(plugin-map): authored `object-map` examples write their props in the `properties` bag, and the `objectName` input names the `dataSource` binding (objectui#10859, batch 5) (objectui `997ce38cb`) -- **patch** — Message text only: the refusal an `element:text` node draws for an authored `body` or `children` describes what the block renders in the converged `variant` vocabulary, the headin… (objectui `caa0cd392`) -- **patch** — The Studio page-block inspector's `element:text` **Variant** select offers the nine values `ui:text` publishes (Heading 1 to Heading 6, Body, Caption, Overline) in place of Headin… (objectui `caa0cd392`) -- **patch** — The `page` preview sample authors its "Quick links" section heading as `variant: 'h3'`, replacing the pre-convergence spelling `subheading` that the ruling retires (objectui#7450)… (objectui `caa0cd392`) -- **patch** — fix(app-shell): Studio's Automations rail stops telling a flow with a declared trigger that it has "no trigger" (objectui `9cfe9977f`) -- **patch** — fix(app-shell): a served dashboard and a served list view are named as served, on the dashboard page, the view tab and the breadcrumb (objectui#11295) (objectui `b28bde8a4`) -- **patch** — fix(layout): a present navigation label renders as written, with no exception, and an inline locale-map label renders its text (objectui#11201) (objectui `1ccb5ba7d`) -- **patch** — fix(app-shell): a Studio pillar never saves one item's document into another while the second is loading, after a package switch, or after visiting a non-editable leaf (objectui `5f2d9676c`) -- **patch** — docs(plugin-form): the README's authored `object-form` examples write their props in the `properties` bag (objectui#10859, batch 4) (objectui `e3782d26e`) -- **patch** — The flow designer's edge `condition` type now mirrors the server's edge slot, the spec's `EvaluatedExpressionInput` (objectui#8946). (objectui `48401689f`) -- **patch** — docs(core): the `ElementDataSourceConfig.filter` note now separates what an author may write from what a renderer may still receive (objectui#8945) (objectui `969d4f291`) -- **patch** — fix(app-shell): the generic metadata editor renders a stored `view`, and a string-array repeater edits strings (objectui `1b30c0fe0`) -- **patch** — `object-grid` re-reads its rows when an input its query reads changes: a `conditionalFormatting` rule, a row or bulk action def, or the view's `searchableFields` (objectui#10689). (objectui `be0ad007b`) -- **patch** — `list-view` re-reads its rows when a `conditionalFormatting` rule, a row action def or a bulk action def adds a field its predicates read (objectui#10689). (objectui `be0ad007b`) -- **patch** — fix(app-shell,components,console): the three remaining select placeholders show the locale's own "Select…" word (objectui#11252) (objectui `7fed09d07`) -- **patch** — fix(plugin-dashboard): a dimensionless table renders a row of every measure, and a dimensionless chart one mark per measure (objectui `b4333ab8a`) - -⚠️ 12 of these carry a breaking change: 12 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 3 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ fix(console): an injected spec shares the console's one zod instance (objectui#11327) (#11353) (objectui `31971ff1e`) -- _(no changeset)_ ci(half-state-patrol): call objectstack's composite action pinned to a sha, with the no-anchor opt-in (objectui#11174) (#11332) (objectui `2c274e3a8`) -- _(no changeset)_ docs: layout.md names the object-grid object with objectName, and flex.mdx teaches the four direction values (objectui#11298) (#11314) (objectui `0c6f9bbd7`) - - - -objectui range: `e420df310f5b...31971ff1e28f` diff --git a/.changeset/console-db11afd4967c.md b/.changeset/console-db11afd4967c.md deleted file mode 100644 index 8d29edda012..00000000000 --- a/.changeset/console-db11afd4967c.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `db11afd4967c`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 89 releasing of 99 changesets added across 113 non-merge commits; omitted: 10 release-nothing changesets, 20 commits carrying no changeset (they ship no package code). - -- **minor** — An `object-grid` now honours `description` and `emptyState`, and four keys it declared but never read are retired (objectui#11068). (objectui `db11afd49`) -- **minor** — fix(app-shell,core): the record page's Undo captures only what the record carries, through core's one capture rule (objectui#11082) (objectui `76e9df06c`) -- **minor** — The generated JSX types let a declared input win with its type for every base prop, so `sdui-intrinsics.d.ts` compiles as generated (objectui#11075). (objectui `4d377ed65`) -- **minor** — A `record:path` whose stage labels are per-locale maps now shows the viewer's language instead of failing to render, and five label inputs on `object-metric`, `object-grid` and `r… (objectui `27ae63279`) -- **minor** — fix(plugin-form): `object-form`'s `modalCloseButton: false` hides the modal's close button (objectui `559a2e207`) -- **minor** — A number field's authored `useGrouping` now decides whether its value renders with thousands separators (objectui#11026). This is the renderer half of `FieldSchema.useGrouping`, t… (objectui `ae582b70a`) -- **minor** — `element:text`, `element:button`, `element:image` and `element:number` now render the accessible name an author declares in their `aria` prop (objectui#11051). (objectui `a4b017eda`) -- **minor** — An `object-view`'s `table` now hands the grid it draws every grid key it types, and stops typing the grid keys that had nothing to act on there (objectui#10976). (objectui `24a0f146e`) -- **minor** — The base-prop list is declared once, and the renderer's `visibleWhen`, `hiddenOn` and `testId` join it (objectui#11044). (objectui `c80236ec8`) -- **minor** — fix(app-shell): the exported sign-in page says why a sign-in is refused, in the reader's language (objectui#11058) (objectui `827550193`) -- **minor** — An `object-form` whose `title`, `description`, `submitText`, `cancelText`, `nextText`, `prevText` or `successMessage` is a per-locale map now shows the viewer's language instead o… (objectui `9b85600b0`) -- **minor** — fix(app-shell): the exported register page says why a sign-up is refused, in the reader's language (objectui#11030) (objectui `180a34a47`) -- **minor** — **BREAKING** — feat(core)!: bare-string `globalFilters[].options` is no longer lifted; use `{ value, label }` objects, the spec's form (objectui#4356). (objectui `a51fa0cca`) -- **minor** — The strict authoring face accepts a correctly authored `metric-card` in a dashboard's widget slot (objectui#11022). This widens a published accept set; nothing that parsed before… (objectui `0eb9f36ac`) -- **minor** — **BREAKING (shipped as `minor` — see below):** nineteen ADR-0080 public-block arms and the dashboard widget slot's `metric-card` node now refuse an authored `children` by name. No… (objectui `f6fb83f0c`) -- **minor** — New SDUI widget `cloud:plan-status`: a "Current plan" badge for one plan card on the Cloud pricing page, shown when that card's plan is the organization's plan (objectui#10919). (objectui `24d3e6562`) -- **minor** — **Narrowing — `ObjectView`'s props are the declared `ConsoleObjectViewProps`, not `any`.** (objectui `ad0310fb5`) -- **minor** — **BREAKING (shipped as `minor` — see below):** thirteen node types now refuse both content channels by name. Each one's renderer reads neither `body` nor `children`, so an authore… (objectui `2049b03df`) -- **minor** — feat(types): `ObjectMapConfigSchema` is `.strict()`, so `objectui validate` refuses an undeclared key in an `object-map` node's `map` block (objectui#5157) (objectui `d6d8fb9d7`) -- **minor** — A master-detail form whose `title`, `submitText` or `cancelText` is a per-locale map now shows the viewer's language instead of crashing or toasting "[object Object] saved" (objec… (objectui `8aa68b159`) -- **minor** — feat(components): a `kind: 'react'` page's author scope injects `useDataInvalidation` (objectui `deca847a8`) -- **minor** — **BREAKING** — A spec-shape conditional-formatting rule's `condition` and a bulk action's `visible` now declare the named view's own expression slots, read by reference from `@objectstack/spec`… (objectui `d570eaa59`) -- **minor** — **BREAKING** — `drillDown` is retired on the bare `pivot` node: author `object-pivot` to drill (objectui#10932) (objectui `cc4e47638`) -- **minor** — The console's assistant dock binds its build conversation to the app you are in (objectui#10926). (objectui `fe563943b`) -- **minor** — Four Console surfaces that read English under a zh-CN session now read the session's language (objectui#10900). English stays the default. (objectui `328abeb55`) -- **minor** — `safeValidateSchema` — and so `objectui validate` — accepts `element:number`, the ADR-0080 public block held back from batch 1, in both of the binding forms `@objectstack/spec` ac… (objectui `b45d463a9`) -- **minor** — `element:number` reads its object from the node-level `dataSource` binding, as the spec declares it (objectui#10909). (objectui `dd0d78f74`) -- **minor** — `safeValidateSchema` — and so `objectui validate` — accepts three more registered node types: `pivot`, `object-metric` and `object-master-detail-form` (objectui#10859, batch 2). (objectui `3b469c8ea`) -- **minor** — **BREAKING (shipped as `minor` — see below):** the `input` node type now refuses both content channels by name. Its renderer reads neither `body` nor `children`, so an authored ch… (objectui `7e8b3c033`) -- **patch** — fix(console,plugin-form,i18n): the public form page and the master-detail form chrome speak the user's language (objectui `54997fffa`) -- **patch** — fix(app-shell): Studio's app preview resolves a locale-map app, nav-item and group label (objectui#11100) (objectui `8a5ae3d63`) -- **patch** — Nine blocks now render the accessible name an author declares in their nested `aria` bag (objectui#11083, batch 2). (objectui `0ecaa7dbb`) -- **patch** — The dashboard's refresh button now speaks the session language (objectui#11097). `DashboardRenderer` and `DashboardGridLayout` hard-coded "Refresh All", "Refreshing…" and the acce… (objectui `cff8641e2`) -- **patch** — Grouped grid lists whose columns are objects load their rows again, instead of showing INVALID_FIELD in every group (objectui#11105). (objectui `3100bef65`) -- **patch** — An `object-grid` whose deprecated `title` is a per-locale map now shows the viewer's language in the table caption and the export file name, instead of failing to render (objectui… (objectui `0bc5c5a01`) -- **patch** — A page now renders the accessible name an author declares in its `aria` bag, and the list view and the `record:*` blocks read the same bag through the one shared reader (objectui#… (objectui `b24f93a72`) -- **patch** — fix(plugin-timeline): a gantt-variant timeline draws its headers and bars on one continuous axis, so each bar lines up under its header (objectui#11079) (objectui `2fb0f9ab8`) -- **patch** — fix(react): one Ctrl+Z undoes one record write, and Ctrl+Z inside a text field is the field's own undo (objectui#11081) (objectui `06451334b`) -- **patch** — feat(app-shell): Studio's app, permission and view previews show the authored area descriptions, RLS policy labels and view label (objectui `5b2ea1757`) -- **patch** — The console now honours a dashboard's authored `refreshIntervalSeconds` (objectui#11062). `DashboardView` used to render `DashboardRenderer` with no `onRefresh`, and the renderer'… (objectui `88fbd793d`) -- **patch** — fix(app-shell): Studio's flow start node stops offering 「Platform event」, a trigger no engine routes (objectui `17fc68873`) -- **patch** — fix(plugin-calendar): a week or day resize of an overnight event keeps its other edge (objectui#11060) (objectui `3e71a9825`) -- **patch** — feat(app-shell): the flow designer shows a connector action's description and offers its output references (objectui `a5841be35`) -- **patch** — The console's undo confirmation toast reads the session's language (objectui#11056). (objectui `873284657`) -- **patch** — fix(plugin-gantt): a gantt move shifts a task's end by the calendar days it shifts the start, keeping each value's time of day across a DST change (objectui#10866, slice 6) (objectui `e119f120c`) -- **patch** — fix(app-shell): a record page's delete asks through the console's own confirm dialog (objectui#11001) (objectui `981389ba3`) -- **patch** — The form family's own feedback chrome now reads the locale packs (objectui#11039). The default success toast, the thank-you heading, the loading line, the load-failure heading and… (objectui `51c294958`) -- **patch** — fix(app-shell): Studio's flow start node writes the `api` trigger the engine routes, and can set its secret (objectui `b31591b4a`) -- **patch** — fix(plugin-gantt): a zoned chart reads and writes a stored day as that day on a DST change (objectui#10866, slice 5) (objectui `f6ae5e22d`) -- **patch** — fix(plugin-dashboard): the auto-refresh interval reads its handler through a ref, not a memoised identity (objectui `6466a09df`) -- **patch** — `objectui check` prints the key and path a file was refused for, and no longer ends with 「✓ All checks passed」 over files it never validated (objectui#11007). (objectui `37a19d4e0`) -- **patch** — fix(plugin-calendar): a week or day view move keeps the event's own length and grab point (objectui#11037) (objectui `2a1779f96`) -- **patch** — The console strings objectui#10900 left English under zh-CN now read the session's language (objectui#10969). (objectui `6cd8f66e8`) -- **patch** — fix(plugin-view): `ObjectView` fetches only for views that draw the rows, and a re-read keeps them on screen (objectui `30f912a0d`) -- **patch** — The Add reaction button renders only on a comment row of a record's discussion feed (objectui `62d6f56bb`) -- **patch** — A record form whose fields are all locked because the user may not create (or edit) records of its object now says so (objectui#11000). The ADR-0092 D4 lock disables every field w… (objectui `bf7ab35ce`) -- **patch** — Citations of objectstack cards now name their repository (objectui#11016). (objectui `63ab76112`) -- **patch** — A `matrix` report that declares `columns` now draws its declared `chart` below the cross-tab (objectui#10964). `ReportSchema` has always accepted a matrix report carrying both `co… (objectui `49f76725a`) -- **patch** — `validateTree` no longer calls a declared `bind` or `hidden` unknown (objectui#11008). (objectui `99878d8e3`) -- **patch** — `WizardForm`'s own chrome now reads the locale packs (objectui#10999). The default Cancel, Back, Next, Submitting, Create and Update labels, the "Step x of y" counter, the step in… (objectui `2eaf5be27`) -- **patch** — fix(plugin-calendar): a month-grid move keeps the wall-clock time across a DST change (objectui#11005) (objectui `f9b6dfe5a`) -- **patch** — With a `page` record surface and no `onNavigate` handler, `ObjectView`'s New button, a row click and Edit now open the record form on the drawer (objectui#11015). (objectui `3ad61001b`) -- **patch** — A reaction click on a record page's discussion keeps every other user's stored reaction (objectui `4e5cb61a9`) -- **patch** — `record:alert`: a `body` written directly on the node, rather than inside `properties`, is now refused with a pointer to `properties.body`, where the banner's message text lives (… (objectui `b3c96d6bc`) -- **patch** — fix(console): a sign-up refused by the server's audience gate reads in the session's language (objectui#10998) (objectui `e327c8998`) -- **patch** — fix(app-shell): a page action refreshes a custom page's data in place instead of remounting the page (objectui#10519) (objectui `a33cf7e92`) -- **patch** — Correct the last four published "no error, no warning" clauses that the parser tier contradicts (objectui#10981, closing the family of objectui#10928 and objectui#10959). (objectui `797a30f48`) -- **patch** — fix(app-shell): a failed reaction write takes the reaction back and says so, instead of staying shown as applied (objectui#10899) (objectui `e2dffc9d1`) -- **patch** — Under a `split` or `popover` navigation, `ObjectView`'s New button now opens a create form (objectui#10975). (objectui `e9ca14ca9`) -- **patch** — fix(plugin-calendar): a day event moved across a DST change writes the days it was dropped on (objectui#10866, slice 4) (objectui `665025908`) -- **patch** — `DashboardGridLayout` and `DashboardRenderer` now share one auto-refresh timer (objectui#8820). (objectui `1f5a6445c`) -- **patch** — fix(components): an `element:number` that asks for an aggregate and names no object says so instead of painting a silent dash (objectui `4b742f41d`) -- **patch** — The metadata form's code editor reads and writes an expression slot through the ADR-0089 envelope (objectui#10963). (objectui `79a935c87`) -- **patch** — fix(app-shell, plugin-detail): the unmapped activity type warnings no longer point at an objectstack issue that answers 404 (objectui `285e36bd1`) -- **patch** — Correct a false clause in the `header-bar` refusal messages and in nine `body?: never` docblocks (objectui#10959). (objectui `42687baf2`) -- **patch** — A named view's `navigation`, `fieldOrder` and `inlineEdit` now reach the registered `object-view` renderer's grid (objectui#10885, member 4). (objectui `4d22e3351`) -- **patch** — fix(plugin-timeline,core): the timeline's gantt axis draws a stored date-only day on that day in every viewer zone, and the formula date functions do their day arithmetic on the U… (objectui `9e6619ffa`) -- **patch** — fix(plugin-chatbot): the confirm-changes card's actions wait until no turn is in flight (objectui `be66b5621`) -- **patch** — Correct a false clause in the content-channel refusal messages (objectui#10928). (objectui `95a7c8d38`) -- **patch** — fix(plugin-chatbot): the proposed-plan card's actions wait until no turn is in flight (objectui `7d82957b1`) -- **patch** — fix(app-shell): console actions supply the spec-declared `${ctx.org.*}` scope (objectui#10918) (objectui `06a96e948`) -- **patch** — The registered `object-view` renderer reads a named grid view's own grid members off the named view (objectui#10885). (objectui `b73e15bf7`) -- **patch** — fix(plugin-designer): editing an app keeps its stored navigation (objectui#10894) (objectui `44b67dabd`) -- **patch** — fix(app-shell): the assistant FAB is hidden while the chat dock is open (objectui#10899) (objectui `ac15833eb`) -- **patch** — fix(plugin-chatbot): a reloaded multi-step build no longer shows an empty 「执行过程」 block under every step (objectui#10899) (objectui `ac15833eb`) -- **patch** — fix(data-objectstack, plugin-dashboard): a dataset tile the viewer may not read shows a localized "no access" state (objectui#10899) (objectui `ac15833eb`) -- **patch** — fix(app-shell, plugin-detail): a record comment whose write fails is never shown as sent (objectui#10899) (objectui `ac15833eb`) -- **patch** — fix(app-shell): the marketplace offers an "update" only for a HIGHER version (objectui#10899) (objectui `ac15833eb`) -- **patch** — fix(app-shell): the Studio edit/design affordances follow the server's authoring capability (objectui#10899) (objectui `ac15833eb`) - -⚠️ 6 of these carry a breaking change: 6 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 20 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ docs(plugin-gantt): the drag's time-of-day sentence names the shift-band exception (objectui#10866) (#11110) (objectui `d1e683fa1`) -- _(no changeset)_ fix(ci): the Test aggregator re-reads a present shard the jobs API answered with no conclusion (objectui#10931) (#11108) (objectui `80c2d5e61`) -- _(no changeset)_ docs(changeset): date-note nine pending entries whose repository count moved (objectui#10979) (#11046) (objectui `480de81fd`) -- _(no changeset)_ docs(changeset): date-note three pending entries on the pivot node's drillDown that PR #10972 made false (objectui#10974) (#11045) (objectui `338482fe7`) -- _(no changeset)_ fix(ci): re-pin the console eager-closure baseline and ceiling on main's own reading (objectui#10996) (#11018) (objectui `6c57c779e`) -- _(no changeset)_ fix(scripts): parse a `json` doc fence strictly with parseJsonFence (objectui#10943) (#10985) (objectui `f667c1df9`) -- _(no changeset)_ docs(agents): split the ruleset bullet, and date the governed guard's enrolment as a required context (objectui#9520) (#10984) (objectui `a097316a2`) -- _(no changeset)_ docs(contributing): a repository count in a changeset is a reading at a named commit (#10978) (objectui `a2de9e943`) -- _(no changeset)_ docs(changeset): date the spec-symbol gate's export filter in the #6286 release note (objectui#9528) (#10970) (objectui `7a9db9148`) -- _(no changeset)_ docs(agents): a force-push is governed by the landing repo's AGENTS.md (objectui#9666) (#10958) (objectui `4dc491a12`) -- _(no changeset)_ fix(scripts): walk both sides of a pipe in the strict-face measurement twin (objectui#10076) (#10966) (objectui `c32890016`) -- _(no changeset)_ docs(adr): ADR-0057 Amendment A2 — the dock binds its build thread to the current app (objectui#10926) (#10947) (objectui `1345e182d`) -- _(no changeset)_ fix(scripts): correct the RETIRED_FIELD_TYPES `excluded` sentence that dropping the line refutes (objectui#10070) (#10961) (objectui `b120b6607`) -- _(no changeset)_ fix(ci): label PRs touching the published docs tree `documentation` (objectui#10012) (#10960) (objectui `97dabdc5b`) -- _(no changeset)_ docs(changeset): date the three named-view census entries that PR objectui#10884 made false (objectui#10885, member 3) (#10955) (objectui `462c85868`) -- _(no changeset)_ docs(changeset): the read-rate banner and its hook are not exported from the package entry (objectui#10913) (#10954) (objectui `a4fb7082a`) -- _(no changeset)_ docs(skills): testing.md Pattern 5 publishes `userRole` through the scope channel the renderer reads (objectui#9380) (#10941) (objectui `7e1a8d098`) -- _(no changeset)_ fix(scripts): the polarity census pins stop asserting what the live `.changeset/` holds, so the post-version tree validates (objectui#10010) (#10933) (objectui `a93ba8792`) -- _(no changeset)_ fix(console): drop the two optimizeDeps.include entries the dev server cannot resolve (objectui#10865) (#10938) (objectui `51401e63c`) -- _(no changeset)_ docs(agents): port objectstack's model-free commit-trailer rule into the multi-agent section (objectui#9441) (#10922) (objectui `af2221d45`) - - - -objectui range: `dd3f7e1be356...db11afd4967c` diff --git a/.changeset/console-e420df310f5b.md b/.changeset/console-e420df310f5b.md deleted file mode 100644 index 5823fd0d9eb..00000000000 --- a/.changeset/console-e420df310f5b.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `e420df310f5b`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 87 releasing of 93 changesets added across 90 non-merge commits; omitted: 6 release-nothing changesets, 1 commit carrying no changeset (they ship no package code). - -- **minor** — `flex`, `object-grid` and `object-chart` accept the node-level `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares on every page component, and judge it a… (objectui `f3135a4d1`) -- **minor** — Setup › Packaged automation shows the platform's reason for a packaged flow the engine has not armed, verbatim and in muted text under the flow name (objectui#9217). (objectui `1cb3732ce`) -- **minor** — **BREAKING (scored `minor` per this repo's version-alignment convention)** — `ObjectKanbanSchema.onQuickAdd` is retired on `object-kanban` (objectui#11234). This completes ruling… (objectui `52aad5cef`) -- **minor** — fix: a row-menu action gated through `disabled` stays disabled until the permissions payload has loaded, in a grid and in a related list's table, and an `` answers… (objectui `18d1a0abd`) -- **minor** — `safeValidateSchema` — and so `objectui validate` — and `StrictAnyComponentSchema` judge a component nested in a page container's props bag (objectui#11223). (objectui `7d074baae`) -- **minor** — `reference` is now the only spelling ObjectUI writes or reads for a relational field's target object (objectui#11070, round 4, under the objectui#6837 ruling: 「objectui不是前端的项目吗?后端… (objectui `f61dab169`) -- **minor** — `record:related_list` now renders the actions its `actions` key names (objectui#11163; ENFORCE ruling on objectstack#20665). (objectui `f4ed2387e`) -- **minor** — feat(app-shell): the sidebar and `nav:menu` hide a `doc` entry the member may not read (objectui#10188) (objectui `d6a1a80d5`) -- **minor** — feat(console): the docs portal refuses a doc or book the member may not read, and opens a doc in the book that claims it (objectui#10188) (objectui `d6a1a80d5`) -- **minor** — feat(layout): a host can hide a `doc` navigation entry the member may not read (objectui#10188) (objectui `d6a1a80d5`) -- **minor** — Every ADR-0080 public-block arm accepts the node-level `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares, and judges it as the spec does (objectui#10872… (objectui `54a78308a`) -- **minor** — fix(types,components,plugin-form,console,core): a faulted `visibleWhen` refuses the submit, naming the field and the rule; a blank field rule is refused; blank gates are diagnosed… (objectui `af9e9572c`) -- **minor** — `safeValidateSchema` — and so `objectui validate` — accepts one more registered node type: `object-timeline` (objectui#10859, batch 3). (objectui `ae0b9d390`) -- **minor** — feat(app-shell): the flow node inspector marks the config keys the installed spec refuses the node without (objectui#10948). (objectui `68c9ca721`) -- **minor** — **BREAKING** — The formula and summary field widgets read `@objectstack/spec`'s own spellings, `returnType` and `summaryOperations`, and the snake_case spellings they used to read are retired at… (objectui `615346d61`) -- **minor** — feat(app-shell): Studio has a Markdown editor for `doc` items, with a live preview and book placement (objectui#10188) (objectui `02fe8ca8a`) -- **minor** — Declare the `filter` and `sort` inputs on the `object-map`, `object-gantt`, `object-timeline` and `view:timeline` registrations (objectui#8220) — the html tier stops reporting `un… (objectui `233a1b318`) -- **minor** — fix: an action gated on `current_user.can(object, verb)` stays hidden until the permissions payload has loaded on every action `visible` surface, a `page:header` action gated thro… (objectui `8bab1571d`) -- **minor** — fix(fields): a currency grid column's width is its currency's minor unit, never an authored `scale`; a hydrated currency column's `scale` is reported (objectui#10783) (objectui `b32e7debc`) -- **minor** — **BREAKING** — `quickAdd` is retired on `object-kanban` (objectui#8285, ruling B of the director seat's decision batch #91: the board does not grow an inline record-creation write… (objectui `6f864cf62`) -- **minor** — feat(types): four zod mirrors declare members their TypeScript twins already declared, and `pagination` retires its `page` spelling (objectui#6152, round 3) (objectui `0c95d3d8d`) -- **minor** — The four `page:` containers take their child list in `properties.children`, the member their `@objectstack/spec` row declares, and refuse a node-level `children` by name: `page:ca… (objectui `dded788ad`) -- **minor** — fix(app-shell,plugin-detail): the record feed says "no permission" when its read is refused, instead of showing an empty list (objectui `1263e405d`) -- **minor** — `ObjectGrid` takes `onNavigate` as a component prop, and the list channel's navigation callback takes one closed mode token, `'view' | 'new_window'`. (objectui `c3df43a42`) -- **minor** — feat: a `doc` navigation entry (ADR-0046) validates and draws as a link into the docs portal (objectui `e6bc087a3`) -- **minor** — **BREAKING** — **The console reads a saved view by the spellings `@objectstack/spec` declares, and stops reading the keys nothing writes (objectui#11013).** This is the console end of the ruling… (objectui `3c13675e5`) -- **minor** — feat: an action's `visible` / `disabled` predicate can ask `current_user.can(object, verb)` — the caller's object permissions, from the payload the built-in Edit / Delete buttons… (objectui `9cebfca5a`) -- **minor** — A filter on a record page can now be scoped to the record the page shows (objectui#7297). Write `{record_id}` as a filter value, for example `{ "assignee": "{record_id}" }` on an… (objectui `cfc9b6db9`) -- **minor** — A page size with nothing declared is now the one `@objectstack/spec` declares for `pagination.pageSize`, on every surface that has a pager; a fetch that has no pager keeps its own… (objectui `de5d400bf`) -- **minor** — The four `action:*` blocks now publish the `@objectstack/spec` keys their renderers honour, and stop publishing what the spec refuses (objectui#11168, slice 1). Each key was decid… (objectui `3cc4fe567`) -- **minor** — **Breaking behaviour change — `object-tree` now honours only the `data` spelling its published row declares.** (objectui `846cec0ef`) -- **minor** — feat(types): the `object-form` zod mirror declares the members its TypeScript twin already declared (objectui#6152, round 1) (objectui `3a3db763b`) -- **minor** — `record:details`, `record:highlights` and `record:related_list` declare the field-security triple — `enforceFieldSecurity`, `redactFields` and `requiredPermissions` — on their pub… (objectui `647908686`) -- **minor** — The six public blocks that objectui#10872 batch 4 armed now refuse an authored `children`, and name the right remedy for a flat `body`: `action:button`, `action:icon`, `action:gro… (objectui `3f9d9263e`) -- **minor** — feat(types,layout,app-shell): a navigation entry with no `label` shows its target's current label, resolved at render time (objectui#9868) (objectui `a8198de22`) -- **minor** — `safeValidateSchema`, and so `objectui validate`, accepts the six ADR-0080 public blocks held back until `@objectstack/spec` carried a `ComponentPropsMap` row for each: `action:bu… (objectui `e978ed5ea`) -- **minor** — The strict authoring face accepts keys a registered renderer reads, which it used to refuse as undeclared (objectui#11070). Each key below is now declared on the TypeScript face a… (objectui `b0a05dda1`) -- **minor** — feat(cli): `objectui check` refuses a `${…}` on a text key its node never evaluates (objectui `33da643e9`) -- **minor** — objectui now resolves `@objectstack/*` 17.5.0 and `zod` 4.6.5, and follows every contract move that release makes (objectui#11073). `@objectstack/spec` 17.5.0 and `@objectstack/co… (objectui `81f849852`) -- **minor** — fix(plugin-gantt,core,plugin-timeline): both gantt surfaces read a date-only end inclusively through one core rule, and a drag writes the same day back (objectui#11141) (objectui `858eafb4f`) -- **patch** — fix(plugin-detail): a related list's `list_toolbar` action authored `visible: false` is hidden (objectui `e420df310`) -- **patch** — fix(console): the docs portal's book sidebar keeps a doc placed by its own `group` key from outside the book's package (objectui#11245) (objectui `f16c01e90`) -- **patch** — fix(app-shell): switching Studio to another flow, page or package no longer saves the previous item's unsaved edit into the one just opened (objectui `fc650380d`) -- **patch** — fix(app-shell): the Studio surface, its nav-item inspector and a new canvas entry inherit a label-less entry's label, the way the console does (objectui#11196) (objectui `02a22957c`) -- **patch** — fix(fields,plugin-detail,plugin-grid): every percent face reads its width through the spec's `resolveFieldScale`, so an undeclared percent renders the same everywhere (objectui `741864f7b`) -- **patch** — fix(app-shell): a flow screen select with no placeholder shows the locale's own "Select…" word (objectui#11220) (objectui `f523bd684`) -- **patch** — fix(plugin-list,plugin-grid): a grouped list view under a toolbar search groups on the server, and each group counts all its matching rows (objectui `d0ae5d025`) -- **patch** — fix(app-shell,plugin-designer): standard navigation entries are written with no `label`, never with a copy of their target's text (objectui#11201) (objectui `cb2f6fb5b`) -- **patch** — fix(app-shell,plugin-designer): the designer's nav surfaces name a nav entry with no `label` the way the console draws it (objectui#11196) (objectui `8741cb71a`) -- **patch** — fix(app-shell): Studio's draft autosave saves an edit made while a save is in flight, in every editor that uses it (objectui `395f4fa51`) -- **patch** — fix(components): a `kind:'react'` page no longer writes the host adapter under each block's `dataSource` (objectui `c021b3529`) -- **patch** — fix(components): an `action:menu` trigger stays disabled while its action runs, and a disabled `action:group` disables its buttons (objectui#11182) (objectui `2e3da72ad`) -- **patch** — fix(app-shell): the Studio flow screen preview draws a screen field's `options`, `placeholder` and `defaultValue` as the runtime dialog does (objectui `ed498ac91`) -- **patch** — fix(app-shell): the page designer stops offering the retired `page:header` breadcrumb toggle (objectui#11173) (objectui `52bf34824`) -- **patch** — fix(app-shell): the permission editor's row-level-security policy list draws each policy's label and description (objectui `f8334f877`) -- **patch** — fix(app-shell): Studio's nav autosave sends every nav edit it has shown — "Done" sends the edit, and a completing save clears only what it sent (objectui `0389650f3`) -- **patch** — fix(app-shell): the save warning has a sentence of its own for a formula field the server ignored (objectui `9419df198`) -- **patch** — fix(data-objectstack): the rule-entry filter form refuses an empty or non-string `icontains` comparand (objectui `0b8c63831`) -- **patch** — fix(app-shell): Studio prints an app's own locale-map label in the designer locale instead of `[object Object]` (objectui `39e625de2`) -- **patch** — feat(app-shell): the screen-flow dialog renders a screen field's declared bound, help text and lookup target (objectui `81778b955`) -- **patch** — fix(app-shell): Studio's Interfaces pillar closes nav editing when the package answers read-only, so no edit is taken on screen that its autosave will refuse (objectui `37d166280`) -- **patch** — fix(plugin-grid): a search reaches the grouped grid's header query and every group's row query, so the groups are the searched ones (objectui `7e4fa1bb2`) -- **patch** — Studio's Interfaces pillar no longer loses its nav rail when a nav item's label is a locale map (objectui#11158). (objectui `3ab51503b`) -- **patch** — fix(app-shell): the metadata form routes a condition builder to a member the served derivation marks as an erased string arm (objectui `32b131017`) -- **patch** — The ingestion choke point's diagnostic for a stored `id_field` now carries the reason `@objectstack/spec` publishes for that key (objectui#7650). (objectui `1e215c40b`) -- **patch** — `page:header` no longer draws an empty breadcrumb slot, and an authored `breadcrumb` is ignored (objectui#11166). (objectui `0ffc423b1`) -- **patch** — fix(app-shell): the report and dashboard-widget pickers show a dataset member's label and the dataset's description (objectui `cecd6a031`) -- **patch** — fix(app-shell): a shared `?sel=nav:ID` link survives the designer's mount and opens that nav item, without entering editing on a read-only package (objectui#11153) (objectui `957f69520`) -- **patch** — fix(fields,plugin-form,i18n): the line-items grid's required-cell text and the master-detail form's config hints read the locale packs (objectui `c2a8d23c6`) -- **patch** — fix(plugin-form,fields,i18n): the line-items panel, the grid field and the master-detail heading finish speaking the user's language (objectui `a8c550938`) -- **patch** — fix(app-shell): Studio's nav-item inspector shows a locale-map label and edits only the designer locale's entry (objectui `9a45088c4`) -- **patch** — An unlabelled undoable action's Undo and Redo toast names the object and carries no English verb (objectui#11080). (objectui `a782fa732`) -- **patch** — fix(fields): a multi-value select shows its placeholder while nothing is selected (objectui `3a0e7ab05`) -- **patch** — fix(app-shell): Studio's Interfaces pillar hands its page inspectors, canvas and source editor the package's real read-only flag (objectui `d71d972ae`) -- **patch** — fix(plugin-form): a master-detail child with authored inline columns derives its sort field and amount field, so line order persists and the total shows (objectui#11144) (objectui `263dcd77f`) -- **patch** — The console's global Undo and Redo toasts read the session's language (objectui#11080). (objectui `1d6a23d60`) -- **patch** — The `object-master-detail-form` registration's `fields` description no longer calls that key "the submitted set" (objectui#11114). (objectui `3fa193856`) -- **patch** — A `record:path` block that sets `statusField` and leaves out `stages` now shows the status field's picklist as its stages, instead of the "record:path — no stages configured" plac… (objectui `3f61eef1b`) -- **patch** — fix(app-shell): Studio's app designer canvas shows a locale-map nav label and renames only the current locale's entry (objectui#11128) (objectui `9babfa433`) -- **patch** — fix(app-shell): the flow designer stops warning on an edge guard that reads its source node's own outputs (objectui `f75e1f7e0`) -- **patch** — fix(plugin-form,fields,i18n): the record page's line-items panel and the line-items grid speak the user's language (objectui `385ebc5c6`) -- **patch** — fix(plugin-form): the master-detail form's Subtotal / Tax / Total show the amount's currency, not a hard-coded yen sign (objectui#11132) (objectui `1923d35d2`) -- **patch** — fix(core,app-shell): Undo of a lookup update restores the stored id, not the expanded record (objectui#11122) (objectui `bf43afafa`) -- **patch** — fix(plugin-timeline,types): a gantt-variant timeline draws a date-only end through the end of that day (objectui#11112) (objectui `c27b575ed`) -- **patch** — A hand-authored form field `{ type: 'select', multiple: true }` now renders the multi-value select and submits an array (objectui#11116). The form renderer's built-in `select` bra… (objectui `84b275c01`) -- **patch** — fix(app-shell): Studio's Automations pillar honours a read-only package on its Enabled switch, flow inspector and canvas (objectui `9fd6c2c6f`) -- **patch** — fix(app-shell,i18n): the designer's flow, federated-panel and field-stub chrome speak the user's language (objectui#10862, slice 4) (objectui `78abf3021`) - -⚠️ 5 of these carry a breaking change: 5 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 1 commit in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared it, so it appears in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ docs: re-fence the plugins and core remainder of objectui#5867 as ts, 17 blocks across 8 pages (batch 6) (#11176) (objectui `340dc718f`) - - - -objectui range: `db11afd4967c...e420df310f5b` diff --git a/.changeset/js-yaml-5-4-1-osv-2026-09-29.md b/.changeset/js-yaml-5-4-1-osv-2026-09-29.md deleted file mode 100644 index bd3dd8ee201..00000000000 --- a/.changeset/js-yaml-5-4-1-osv-2026-09-29.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/metadata": patch ---- - -`@objectstack/metadata` declares `js-yaml` `^5.4.1` (was `^5.2.3`), clearing GHSA-r3ph-w7gj-g6xm, which affects js-yaml releases before 5.4.1. The lockfile now resolves 5.4.2. `js-yaml` is the YAML parser behind the metadata loader, and it was this package's only importer of it. - -Clause-②: no - -No exported symbol, option key or accept/reject verdict of ours moves; the published surface is unchanged and grades `patch`. The change is a dependency-range floor, so a fresh install can no longer resolve a vulnerable 5.x copy. - -`osv-scanner.toml` keeps zero exemptions and is untouched. diff --git a/.changeset/lint-provenance-anchors.md b/.changeset/lint-provenance-anchors.md deleted file mode 100644 index 661b5fa245d..00000000000 --- a/.changeset/lint-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -Provenance comments in `@objectstack/lint` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no rule id, finding message, hint, severity, type or runtime behaviour -changes. diff --git a/.changeset/nav-item-label-resolution-order.md b/.changeset/nav-item-label-resolution-order.md deleted file mode 100644 index 89a1de30ac8..00000000000 --- a/.changeset/nav-item-label-resolution-order.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): a navigation entry's `label` describe states the one order the label resolves in - -Clause-②: no - -The `label` of a navigation entry (`BaseNavItemSchema`) said a present label "renders -verbatim and is never overwritten", which, read literally, forbids the id-keyed localization -`translateApp` already performs at the `/meta` boundary. Its describe and JSDoc now state one -order: the bundle entry `apps..navigation..label` for the active locale chain, keyed -by the entry's `id` and applied by `translateApp` over the app's `navigation` tree (not -`areas`); else a present label as authored — its inline locale map's value for that locale, -else its text; else, when absent, the current label of what the entry opens, at render time, -localized by the target's own translation. A present label is never replaced by its target's -label and never translated by matching its text. No accepted shape changes: `label` stays an -optional `I18nLabel`. diff --git a/.changeset/objectui-pin-citations-31971ff1e28f.md b/.changeset/objectui-pin-citations-31971ff1e28f.md deleted file mode 100644 index 0a44fd01eff..00000000000 --- a/.changeset/objectui-pin-citations-31971ff1e28f.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `31971ff1e28f`. - -Clause-②: no - -Several records were corrected rather than moved, because objectui changed what they describe on this hop: `object-calendar` and `object-timeline` now read `navigation` with no cast, and `object-kanban`'s read compiles through a declared member, since objectui declared the key on all three blocks (objectui#8652, objectui#8654); `object-timeline` also publishes a `navigation` input (objectui#8654); and the `action:button` registration now publishes the five `size` values its row declares (objectui#11168). Two stale readings are also corrected: the `object-timeline` start/end binding anchor began one line early at the previous pin as well, and the `object-tree` optionality count now records the comment that names the gate in `plugin-map`'s shell. No key, default, enum member or export moves. diff --git a/.changeset/objectui-pin-citations-db11afd4967c.md b/.changeset/objectui-pin-citations-db11afd4967c.md deleted file mode 100644 index 8da4b991bdf..00000000000 --- a/.changeset/objectui-pin-citations-db11afd4967c.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `db11afd4967c`. - -Clause-②: no - -One claim was falsified rather than moved: `ObjectMapConfigSchema` is `.strict()` at the new pin (objectui#5157), so the `ListMapConfigSchema` record now says the renderer's schema warns on an undeclared key instead of parsing it clean. No key, default, enum member or export moves. diff --git a/.changeset/objectui-pin-citations-e420df310f5b.md b/.changeset/objectui-pin-citations-e420df310f5b.md deleted file mode 100644 index 122253a50ba..00000000000 --- a/.changeset/objectui-pin-citations-e420df310f5b.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `e420df310f5b`. - -Clause-②: no - -Several records were corrected rather than moved, because objectui changed what they describe on this hop: the four `action:*` registrations now publish the keys their rows declare, `endpoint` and `undoable` aside (objectui#11168), `action:group` and `action:menu` now apply a host-evaluated `disabled` that their rows do not declare (objectui#11182, recorded, not declared), the `object-kanban` default row cap is named `DEFAULT_KANBAN_FETCH_BATCH_SIZE` (objectui#9853), the board no longer forwards `quickAdd` (objectui#8285, objectui#11234), the `object-tree` ladder now judges `data` on the `view-data` arm its row declares (objectui#8348), and `object-map` / `object-gantt` / `object-timeline` now publish `filter` and `sort` inputs (objectui#8220). Two older statements that were already stale are also corrected: an authored `data` array on `object-map` no longer reaches the renderer through the React props channel (objectui#9571), and the `quickAdd` retirement record now notes that the schema-only `kanban-ui` block it points to is retired in objectui (objectui#8257). No key, default, enum member or export moves. diff --git a/.changeset/page-component-slot-positions.md b/.changeset/page-component-slot-positions.md deleted file mode 100644 index e4481f79f9e..00000000000 --- a/.changeset/page-component-slot-positions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': patch -'@objectstack/cli': patch ---- - -feat(spec): one list of page-component slot positions, derived from the component rows and read by every page walk — `page:card`'s `footer` is now walked by all three (#20940) - -The platform has three walks that descend into a page component's `properties` bag, and each kept its own list of where child components hang: the ADR-0087 conversion walker (`children`, `body`, `footer`, `items[].children`), `@objectstack/lint`'s `walkPageComponents` (the same four) and the exported `walkAddressedPageComponents` (`children`, `items[].children`). So a node in a card's `footer` — a declared, rendered slot ("Card footer components (slot)") — was judged by `os lint` and skipped by every consumer of the exported walk: `translatePage` left its copy untranslated, `os i18n extract` offered no key for it, and objectui's validator passed it unjudged. - -**`@objectstack/spec` — new exports `pageComponentSlotPositions()` and `PageComponentSlotPosition` (`@objectstack/spec/ui`).** The component rows now mark each composition slot at its declaration, and `pageComponentSlotPositions()` derives the one list from `ComponentPropsMap`: `children`, `footer` and the panel position `items[].children`, plus the tombstoned `body` flagged `retired: true`. The marker changes nothing about the schema it marks — the parse, the JSON Schema and the authorable surface are unchanged. The list is derived on first call and memoized, never at import. `minor` because the package's public surface grows by these two exports. - -**`walkAddressedPageComponents` descends `properties.footer`.** It reads the list's authorable entries, in the list's order (`children`, `footer`, then `items[].children`); signature and return shape are unchanged. What follows from it: - -- `translatePage` translates the copy of a component in a card footer through `pages..components.`, like any other nested component. -- `os i18n extract` offers those keys, and `os i18n check` counts them, for a stack whose card footers hold components with an `id` and copy. -- objectui's validator, which judges the nodes this walk visits, now judges a card footer's nodes. - -`page:card.body` stays undescended, as #5775 ruled: it is not an authorable spelling. - -**The conversion walker reads every entry, the retired one included.** Its reach does not change: it descends `children`, `body`, `footer` and `items[].children`, as before. Stored documents still carry `body`, the renderers still draw it, and a conversion that runs before `page-card-body-to-children` meets the sub-tree there. Within one component the visit order is now `children`, `body`, `footer`, then the panels. That order is observable only as the order of the notices for a component that carries both a direct slot and panels. - -**`@objectstack/lint` — `walkPageComponents` reads the list's authorable entries.** It walks `footer` as before, and it stops walking the retired `body` spelling. The walk matches by shape, so this drops a `body` array on any component, not only on `page:card`. #5775 (maintainer ruling 2026-08-06, direction A) made `children` the one composition key. The renderers keep reading `body` only as a back-compat fallback for stored documents. On `page:card` the tombstone's rename prescription still refuses `body`, and so does the thin containers' guidance; the sub-tree is judged once it sits under `children`. So the rules built on this walk no longer report findings about nodes under any component's `body` array. The conversion walker keeps reaching them for stored documents. - -**`@objectstack/cli`:** no code change. `os i18n extract` and `os i18n check` pick up the `footer` component keys through the shared walk. The extractor's object-section pass stops reading `record:details` sections under a retired `body`, through lint's walk. - -**Why no ADR-0087 ledger entry.** Nothing an author writes moves: no spec key is retired or renamed, no stored `sys_metadata` shape changes, and no conversion or migration id is touched. `objectstack migrate meta` has nothing to act on. diff --git a/.changeset/plugin-dev-provenance-anchors.md b/.changeset/plugin-dev-provenance-anchors.md deleted file mode 100644 index deeb47ee908..00000000000 --- a/.changeset/plugin-dev-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-dev': patch ---- - -Provenance comments in `@objectstack/plugin-dev` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no service, boot-log line, warning text, type, export or runtime -behaviour changes. diff --git a/.changeset/plugin-hono-server-provenance-anchors.md b/.changeset/plugin-hono-server-provenance-anchors.md deleted file mode 100644 index a46aa118718..00000000000 --- a/.changeset/plugin-hono-server-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-hono-server': patch ---- - -Provenance comments in `@objectstack/plugin-hono-server` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no route, error code, refusal text, type, export or runtime behaviour -changes. diff --git a/.changeset/report-chart-matrix-placement-comment.md b/.changeset/report-chart-matrix-placement-comment.md deleted file mode 100644 index 0c634c2d8b1..00000000000 --- a/.changeset/report-chart-matrix-placement-comment.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the `ReportSchema.chart` doc comment says the chart is drawn below the table of a `matrix` report with `columns` - -Clause-②: no - -The doc comment on `ReportSchema.chart` said the embedded chart is plotted above the report's -table. That holds for a `tabular` or `summary` report, and for a `matrix` report without -`columns`, which renders as a grouped table. A `matrix` report with `columns` renders as a -cross-tab, and objectui's `DatasetReportRenderer` draws the chart below it. The comment now -says so. It also drops a clause saying a chart on a `joined` report "parsed and plotted -nothing": the schema refuses that key today, so the clause no longer described it. - -Doc comment only: the schema accepts and refuses the same reports, and no `.describe()` text -or export changes. diff --git a/.changeset/rest-provenance-anchors.md b/.changeset/rest-provenance-anchors.md deleted file mode 100644 index 41679c5a768..00000000000 --- a/.changeset/rest-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -Provenance comments in `@objectstack/rest` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no route, error code, refusal text, type, export or runtime behaviour -changes. diff --git a/.changeset/runtime-provenance-anchors.md b/.changeset/runtime-provenance-anchors.md deleted file mode 100644 index 4786cf0cd53..00000000000 --- a/.changeset/runtime-provenance-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -Provenance comments in `@objectstack/runtime` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no route, error code, refusal text, type, export or runtime behaviour -changes. diff --git a/.changeset/sdui-parser-retire-inert-quick-add.md b/.changeset/sdui-parser-retire-inert-quick-add.md deleted file mode 100644 index b36899b01c4..00000000000 --- a/.changeset/sdui-parser-retire-inert-quick-add.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/sdui-parser': minor ---- - -fix(sdui-parser)!: the interim `inert-quick-add` diagnostic is retired, mirroring objectui `6f864cf62` (objectui#8285) - -Clause-②: no (narrowing) - - - -**BREAKING**: shipped as `minor` under the launch-window convention. The save-time parser stops emitting the interim `inert-quick-add` warning for an authored `quickAdd` on ``. The prop walk's own `unknown-prop` warning replaces it, so severity is unchanged and no page that saves today stops saving. The package's barrel no longer exports `checkKanbanQuickAdd`, `INERT_QUICK_ADD`, `QUICK_ADD_HOST_TYPES` or `QUICK_ADD_KEY`. A caller that matched on the `inert-quick-add` code should match `unknown-prop` on the `quickAdd` key instead, and an importer of those four names should delete the import: nothing replaces them. On the authored side, delete the `quickAdd` key: the spec refuses it by name. diff --git a/.changeset/service-messaging-provenance-anchors.md b/.changeset/service-messaging-provenance-anchors.md deleted file mode 100644 index 1d72c6ad969..00000000000 --- a/.changeset/service-messaging-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/service-messaging': patch ---- - -Provenance comments in `service-messaging` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no type, schema, export, refusal text or runtime behaviour changes. diff --git a/.changeset/spec-conversion-summaries-decision-in-words.md b/.changeset/spec-conversion-summaries-decision-in-words.md deleted file mode 100644 index d0c7ce3e86b..00000000000 --- a/.changeset/spec-conversion-summaries-decision-in-words.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Four conversion summaries now state their decision in words instead of citing a tracker number - -Clause-②: no - -The `summary` of four ADR-0087 conversions (`datasource-driver-mongo-to-mongodb`, -`translation-component-submit-label-removed`, `mapping-lookup-params-removed` and -`connector-error-mapping-removed`) cited a GitHub issue that no longer exists. That -text is what `os migrate meta`, `spec-changes.json` and the protocol upgrade guide show -an author, so each now says what was decided and why: one driver id for driver and -config contract, retire rather than re-anchor `submitLabel`, remove rather than -implement the import lookup params, and delete `errorMapping` to end its `userMessage` -collision without a rename. Wording only: no conversion id, surface, retirement state, -transform, schema, export or runtime behaviour changes. diff --git a/.changeset/spec-conversions-connector-provenance-anchors.md b/.changeset/spec-conversions-connector-provenance-anchors.md deleted file mode 100644 index 7ff2d21a65f..00000000000 --- a/.changeset/spec-conversions-connector-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Provenance comments in `conversions/registry.ts` and `integration/connector.zod.ts` were re-anchored - -Clause-②: no - -Thirteen comment and docblock sites in `src/conversions/registry.ts` and -`src/integration/connector.zod.ts` cited tracker numbers that no longer resolve on -GitHub. They now cite the record that decided the matter: ADR-0087's 2026-09-13 -addendum for the data-at-rest seams `retiredFromLoadPath` does not hold back, and -otherwise the commit in this repository's history. Comments only: no type, schema, -export, `describe()` text, conversion `summary` or runtime behaviour changes. diff --git a/.changeset/spec-migration-entries-provenance-anchors.md b/.changeset/spec-migration-entries-provenance-anchors.md deleted file mode 100644 index d62834d677c..00000000000 --- a/.changeset/spec-migration-entries-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The ADR-0087 migration entries cite the commit that decided each retirement instead of a tracker number that no longer resolves - -Clause-②: no - -The source comments of the migration registry's retired-key, retired-def and semantic -entries named GitHub issues that no longer exist, so a reader could not tell a rule kept -on purpose from one nobody could explain. Each of those comments now names the commit -that made the decision and, where the number alone carried the meaning, says what was -decided. The compiled `@objectstack/spec/migrations` entry carries these comments, which -is why this is a release note at all. Comment text only: no entry id, retired key or def, -prescription, projected upgrade-guide text, schema, export or runtime behaviour changes. diff --git a/.changeset/spec-remainder-provenance-anchors.md b/.changeset/spec-remainder-provenance-anchors.md deleted file mode 100644 index 87ff5bef46c..00000000000 --- a/.changeset/spec-remainder-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Provenance comments in the rest of `src/` were re-anchored - -The remaining comment and docblock lines in 21 files under `src/` (among -them `api/rest-server.zod.ts`, `system/i18n-resolver.ts`, -`system/operation-message.ts`, `shared/identifiers.zod.ts`, the root -`index.ts` and `data/driver/turso.zod.ts`) cited tracker numbers that no -longer resolve on GitHub. They now cite the commit in this repository's -history that decided the matter, or the ADR amendment that records the -ruling, and say in their own words what was decided. Comments only: no type, -schema, export, message-catalog string or runtime behaviour changes. diff --git a/.changeset/spec-stack-analytics-provenance-anchors.md b/.changeset/spec-stack-analytics-provenance-anchors.md deleted file mode 100644 index 9c5db2741df..00000000000 --- a/.changeset/spec-stack-analytics-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Provenance comments in `stack.zod.ts` and `data/analytics.zod.ts` were re-anchored - -Twelve comment and docblock lines in `src/stack.zod.ts` and -`src/data/analytics.zod.ts` cited tracker numbers that no longer resolve on -GitHub. They now cite the commit in this repository's history that decided -the matter: the `themes` carrier retirement, the lowered-handler array form of -`functions`, the closed `ManifestSchema`, the same-key action refusal in -`defineStack` and its cross-stack twin in `composeStacks`, and the -`analytics_cube` binding at the `/meta` write door. Comments only: no type, -schema, export, `describe()` text or runtime behaviour changes. diff --git a/.changeset/spec-ui-provenance-anchors.md b/.changeset/spec-ui-provenance-anchors.md deleted file mode 100644 index 481d7837223..00000000000 --- a/.changeset/spec-ui-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Provenance comments in `ui/` were re-anchored - -Comment and docblock lines under `src/ui/`, and in -`src/data/filter-subtree-provenance.ts` and -`src/meta-spelling/manifest-collection-spelling.ts`, that cited tracker numbers -which no longer resolve on GitHub now cite the commit in this repository's -history that decided the matter, or the ADR amendment they quote, and say in -their own words what was decided. Two references to objectui numbers now name -objectui on each number. Comments only: no type, schema, export or runtime -behaviour changes. diff --git a/.changeset/types-provenance-anchors.md b/.changeset/types-provenance-anchors.md deleted file mode 100644 index 240ee520105..00000000000 --- a/.changeset/types-provenance-anchors.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/types': patch ---- - -Provenance comments in `@objectstack/types` were re-anchored - -Comment and docblock lines under `src/` that cited tracker numbers which no -longer resolve on GitHub now cite the commit in this repository's history that -decided the matter, and say in their own words what was decided. Comments -only: no error code, refusal text, type, export or runtime behaviour changes. diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index f945f8724cc..145b5bcbeb2 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -75,7 +75,7 @@ docker run -p 8080:8080 \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET \ -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.5.0 + ghcr.io/objectstack-ai/objectstack:17.6.0 ``` (`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -93,7 +93,7 @@ docker run -p 8080:8080 \ -e OS_ARTIFACT_URL="https://releases.example.com/hotcrm-2.2.2.json#sha256=<64 hex chars>" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.5.0 + ghcr.io/objectstack-ai/objectstack:17.6.0 ``` Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly @@ -144,7 +144,7 @@ COPY . . RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── -FROM ghcr.io/objectstack-ai/objectstack:17.5.0 +FROM ghcr.io/objectstack-ai/objectstack:17.6.0 COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json ``` @@ -162,7 +162,7 @@ image)? The official image is nothing more than: ```dockerfile title="Dockerfile (self-built runtime, equivalent)" FROM node:22-slim -RUN npm install -g @objectstack/cli@17.5.0 +RUN npm install -g @objectstack/cli@17.6.0 WORKDIR /srv/app RUN chown node:node /srv/app diff --git a/content/docs/releases/index.mdx b/content/docs/releases/index.mdx index cac90c15144..011f08e1ea9 100644 --- a/content/docs/releases/index.mdx +++ b/content/docs/releases/index.mdx @@ -18,7 +18,7 @@ migration steps, then covers new capabilities and notable fixes. ## Versions -- [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (current series: 17.5.0, released 2026-09-29). +- [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (current series: 17.6.0, released 2026-10-02). - [v16.0.0](/docs/releases/v16) — One org identifier (`organizationId`) across hooks and actions, quorum + per-group sign-off (会签) approvals with metadata-declared decision actions, time-relative automations, filtered roll-ups, strict dashboard widgets, an identity-scoped MCP stdio transport, and a platform-wide enforce-or-remove sweep that makes dead metadata loud; 16.1 adds a `requires` capability-provider preflight, two more dashboard build gates, and `runAs:'user'` automations that run with the triggering user's real grants (final release: 16.1.0). - [v15.0.0](/docs/releases/v15) — Explain record access layer by layer, a docked AI workspace in the Console, project-ready Gantt charts, and phone sign-in; 15.1 adds permission-following attachments, no-code third-party connectors, dashboard-wide filters, pinyin search, and whole-record inline editing — with materially safer multi-tenant and write-path defaults (final release: 15.1.1). - [v14.0.0](/docs/releases/v14) — ADR-0090 vocabulary convergence completed, object `enable.*` flags become real gates, admin user management, phone/SMS auth, book-audience enforcement, data-lifecycle contract, and effective-dated grants (final release: 14.8.0). diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index 0e69fb16876..9d343e7154b 100644 --- a/content/docs/upgrading.mdx +++ b/content/docs/upgrading.mdx @@ -52,7 +52,7 @@ The official image is `ghcr.io/objectstack-ai/objectstack`, and its tags mirror ```bash # docker-compose.yml, or your orchestrator's manifest -image: ghcr.io/objectstack-ai/objectstack:17.5.0 +image: ghcr.io/objectstack-ai/objectstack:17.6.0 ``` On a host running the artifact directly under systemd, the same move is a file diff --git a/docker/README.md b/docker/README.md index 80da9e3880e..79b5bc6ec26 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,7 +29,7 @@ Multi-arch: `linux/amd64` + `linux/arm64`. [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-hosting)): ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:17.5.0 +FROM ghcr.io/objectstack-ai/objectstack:17.6.0 COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json ``` @@ -40,7 +40,7 @@ docker run -p 8080:8080 \ -v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.5.0 + ghcr.io/objectstack-ai/objectstack:17.6.0 ``` `OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -72,7 +72,7 @@ for a `file:…` path — one box only, wrong for multi-node) and MongoDB (`libsql://…` / Turso). Add one by extending the image: ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:17.5.0 +FROM ghcr.io/objectstack-ai/objectstack:17.6.0 USER root RUN npm install -g tedious USER node @@ -100,5 +100,5 @@ reverse-proxy / multi-node guidance: ## Local build of this image ```bash -docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.5.0 docker/ +docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.6.0 docker/ ``` diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 1ca7030f7d2..0f414c2adb0 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,145 @@ # @objectstack/example-crm +## 4.0.98 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [7c5a311] +- Updated dependencies [f10d802] +- Updated dependencies [76bd58f] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [cb4c31d] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [2bddb19] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [a186aea] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/service-i18n@17.6.0 + ## 4.0.97 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index b8486472992..85ff8289a80 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.97", + "version": "4.0.98", "description": "Minimal CRM example \u2014 a smoke-test workspace that exercises the metadata loading pipeline (objects \u2192 views \u2192 app \u2192 dashboard \u2192 hook \u2192 flow \u2192 seed). For a full-featured enterprise CRM see https://github.com/objectstack-ai/hotcrm.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-multi-package/CHANGELOG.md b/examples/app-multi-package/CHANGELOG.md index 6b427c69b97..7965921a458 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,128 @@ # @objectstack/example-multi-package +## 0.0.5 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 0.0.4 ### Patch Changes diff --git a/examples/app-multi-package/package.json b/examples/app-multi-package/package.json index 107d3a5f269..86cb6e580e2 100644 --- a/examples/app-multi-package/package.json +++ b/examples/app-multi-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-multi-package", - "version": "0.0.4", + "version": "0.0.5", "description": "One release artifact carrying TWO packages that share a namespace (ADR-0130 D4) — the producer-side fixture for `packages[]`", "license": "Apache-2.0", "private": true, diff --git a/examples/app-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index b7749d89dc4..cb972f7ab8b 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,170 @@ # @objectstack/example-showcase +## 0.3.20 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [df67985] +- Updated dependencies [42d78b9] +- Updated dependencies [5a23096] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [0e9ad74] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [7c5a311] +- Updated dependencies [f10d802] +- Updated dependencies [76bd58f] +- Updated dependencies [810d42b] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [cf0346e] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [cb4c31d] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [c6954d6] +- Updated dependencies [2bddb19] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [e35c40a] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [c6b6889] +- Updated dependencies [ebdb6f2] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [be5a83c] +- Updated dependencies [7923c8e] +- Updated dependencies [95b91cc] +- Updated dependencies [cfa9315] +- Updated dependencies [f927864] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [a186aea] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/driver-sql@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/service-datasource@17.6.0 + - @objectstack/connector-mcp@17.6.0 + - @objectstack/cloud-connection@17.6.0 + - @objectstack/connector-openapi@17.6.0 + - @objectstack/connector-rest@17.6.0 + - @objectstack/connector-slack@17.6.0 + - @objectstack/service-i18n@17.6.0 + ## 0.3.19 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 6c42dd27652..518180969a3 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.19", + "version": "0.3.20", "description": "Kitchen-sink showcase workspace — exercises every metadata type, every view type, every chart type, and the major end-to-end capability chains (security, automation, analytics). Built for demonstration, debugging, and coverage-driven verification.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-todo/CHANGELOG.md b/examples/app-todo/CHANGELOG.md index c4eae438930..ecf452780e3 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,179 @@ # @objectstack/example-todo +## 4.0.98 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [e952cff] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [7c5a311] +- Updated dependencies [f10d802] +- Updated dependencies [76bd58f] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [cb4c31d] +- Updated dependencies [d67b942] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [12fbb2f] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [2bddb19] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [3ddd3d0] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [e4e5222] +- Updated dependencies [61455de] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [a186aea] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/metadata@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/service-knowledge@17.6.0 + - @objectstack/client@17.6.0 + - @objectstack/driver-sqlite-wasm@17.6.0 + - @objectstack/mcp@17.6.0 + - @objectstack/knowledge-memory@17.6.0 + - @objectstack/service-i18n@17.6.0 + ## 4.0.97 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index b3fab1266ca..16e67190488 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.97", + "version": "4.0.98", "description": "Example Todo App using ObjectStack Protocol", "license": "Apache-2.0", "private": true, diff --git a/examples/embed-objectql/CHANGELOG.md b/examples/embed-objectql/CHANGELOG.md index 8a8fd25fa6d..b7f3dea8d71 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,159 @@ # @objectstack/example-embed-objectql +## 0.0.38 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [d3f88fa] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [8fec76a] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [95fed33] +- Updated dependencies [26437ae] +- Updated dependencies [d67b942] +- Updated dependencies [f8178ff] +- Updated dependencies [32d3b3c] +- Updated dependencies [a3dc817] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [45ce12a] +- Updated dependencies [682873d] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/driver-memory@17.6.0 + ## 0.0.37 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 1b908522550..7b68084c37b 100644 --- a/examples/embed-objectql/package.json +++ b/examples/embed-objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-embed-objectql", - "version": "0.0.37", + "version": "0.0.38", "private": true, "description": "Embed the ObjectQL engine as a plain library via @objectstack/objectql/core — no kernel, no plugins, no metadata protocol (ADR-0076).", "type": "module", diff --git a/packages/adapters/hono/CHANGELOG.md b/packages/adapters/hono/CHANGELOG.md index 16c40a4c5dc..9a1b59fcc7d 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/hono +## 17.6.0 + +### Patch Changes + +- Updated dependencies [5a23096] +- Updated dependencies [c96beb2] +- Updated dependencies [3f45b6c] +- Updated dependencies [96e7244] +- Updated dependencies [4b45afa] +- Updated dependencies [c8111a5] +- Updated dependencies [b9087d7] +- Updated dependencies [7c5a311] +- Updated dependencies [49d2a24] +- Updated dependencies [76bd58f] +- Updated dependencies [cb4c31d] +- Updated dependencies [8368f1c] +- Updated dependencies [514001a] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [2bddb19] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [454bbb6] +- Updated dependencies [01e78dc] +- Updated dependencies [a186aea] +- Updated dependencies [00f045d] + - @objectstack/runtime@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/plugin-hono-server@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index d2ca2e55788..8b1b492f6a6 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index 7287dd71bb0..6727c023522 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/account +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index 53febee2d41..8cd4bd4a926 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack Account — the end-user account/self-service console app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index 0af0d3e11b8..2ba07a3f1be 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/setup +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index d635b3b1659..c800df240f8 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index 7f1c5054e1b..73241a63108 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/studio +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 106d86063c0..9e24c3ce6d2 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 0487eb841ca..34db7f065b7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,1068 @@ # @objectstack/cli +## 17.6.0 + +### Minor Changes + +- 9b402db: fix(cli)!: a project with no `sdui.manifest.json` of its own has its `kind: 'html'` pages checked against the manifest `@objectstack/console` ships, so `div` and every other undeclared tag or prop is refused (#19922) + + Clause-②: no (narrowing) + + + + **BREAKING for `kind: 'html'` pages in projects without their own manifest.** + + **What changed.** `objectstack validate`, `objectstack compile` / `build` and + `objectstack lint` check the `source` of a `kind: 'html'` page against an SDUI + component manifest. (`dev` and `start` run `compile` before they boot when + `dist/objectstack.json` is missing or `--compile` is passed, and `dev`'s default + watch mode reruns it when a watched file changes.) They look first for the + `sdui.manifest.json` in the directory the command runs in, then for the copy + `@objectstack/console` ships as `dist/sdui.manifest.json`. The second lookup asked for that file by a subpath + the console package does not export, so it always failed, and a project with no + manifest of its own had its html pages checked at parse level only: syntax and + structure, never which components and props they use. The lookup now reaches the + shipped copy, and those pages get full component and prop validation. A tag or + prop the manifest does not declare is refused (`jsx-forbidden-tag`, + `jsx-unknown-component`, `jsx-unknown-prop`), naming the page and the tag, and + the command exits 1. + + **What was refused before, and what is new.** The console has refused a `div` on + a `kind: 'html'` page since `@objectstack/console` 17.5.0: when the page renders, + its in-browser html compile answers `forbidden-tag`, naming `box`. These commands + now give that answer while you author. What they refuse that nothing refused + before is every other tag the manifest does not declare. The console's html + compile accepts every component its registry knows that is not deprecated there, + while the published manifest declares only the public component contract and the + html tier's intrinsic tags. So a page using, for example, `avatar` or `checkbox` + renders in the console and is refused here. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `
` … `
` in a `kind: 'html'` page | `` … ``, which takes the same `className` and children | + | any other tag or prop the command names | a component and prop the manifest declares | + + **What is not affected.** A project that keeps its own `sdui.manifest.json` is + checked against that file, as before. `kind: 'react'` pages and pages authored + as regions are not read by this gate. With no manifest reachable at all, the + pages are still checked at parse level, and the notice that says so is + unchanged. + + **A damaged install is refused, not skipped.** A shipped copy that is present + but cannot be read or parsed stops the command with exit 1, naming the file, + with the remedy: reinstall `@objectstack/console`. + + **A correction to the 17.5.0 note on this manifest.** The `@objectstack/console` + 17.5.0 patch entry `28ce612`, the one that says the prebuilt Console dist now + ships `dist/sdui.manifest.json`, ends with a paragraph that this release changes, + sentence by sentence: + + - "For now the file is only present in the tarball." No longer true: the CLI + reads it, as described above. + - "This package's `exports` map exposes `./package.json` and nothing else, so + resolving `@objectstack/console/dist/sdui.manifest.json` through `exports` + fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`." Still true: the `exports` map is + unchanged. + - "Anything that resolves through `exports` cannot read the file yet." Still + true. To read the file, resolve `@objectstack/console/package.json` and join + `dist/sdui.manifest.json` to its directory. + - "That includes the CLI's JSX-page manifest fallback, which catches the error + and keeps parse-level validation, as before." True of the 17.5.0 CLI, false + from this release: the fallback now reads the file that way, so a project + without its own manifest is checked against the shipped copy. +- 6981abf: fix(cli)!: `objectstack validate`, `objectstack build` and `objectstack lint` read the project's `sdui.manifest.json` beside the config they were given, not in the directory they were run from (#20166) + + Clause-②: no (narrowing) + + + + **BREAKING for runs given a config path in another directory.** + + **What changed.** These commands check the `source` of each `kind: 'html'` page + against an SDUI component manifest: the project's own `sdui.manifest.json` first, + then the copy `@objectstack/console` ships. They located everything else about a + project from the directory of its config, but looked for the project's own + manifest in the directory the command was run from. So + `objectstack validate path/to/app/objectstack.config.ts`, run from anywhere else, + never read `path/to/app/sdui.manifest.json`, and a manifest that happened to sit in + the directory it was run from judged a project it does not belong to. They now read + the manifest beside the config. + + ## Which manifest each run reads + + | the run | the project manifest it read | the project manifest it reads now | + |:--|:--|:--| + | `objectstack validate` / `build` / `lint` with no config path, in the project's directory | `./sdui.manifest.json` | `./sdui.manifest.json` (unchanged) | + | the same commands given `path/to/app/objectstack.config.ts`, run from another directory | that other directory's `sdui.manifest.json` | `path/to/app/sdui.manifest.json` | + + When the project carries no manifest of its own, both rows then fall back to the + copy `@objectstack/console` ships, as before. + + **Which runs change, and which way.** Only runs whose config path names a directory + other than the one they run in. For those, the verdict can move in both directions: + + - A page the project's own manifest does not declare is now refused + (`jsx-forbidden-tag`, `jsx-unknown-component`, `jsx-unknown-prop`, exit 1), where + the other directory's manifest, or the console's copy, used to admit it. + - A project manifest that is present but not usable is now refused (exit 1), naming + that file, where the run used to read some other file. + - A project with no manifest of its own is now checked against the console's copy, + where the other directory's manifest used to decide. + - In the other direction, a page the other directory's manifest refused, and that + the project's own manifest (or the console's copy) declares, is now admitted. + + If such a run now fails, the manifest that belongs to the project is the one to + keep beside its config. + + **What is not affected.** A run in the project's own directory, with or without a + config path, reads the same file as before. `objectstack init`'s check of a freshly + generated scaffold keeps reading the directory it was run from. + + **A correction to this release's console-fallback entry.** That entry says these + commands "look first for the `sdui.manifest.json` in the directory the command runs + in". From this release they look first beside the config the command was given, + which is the same directory whenever the command runs in the project. +- b531c7b: `os serve` hands the runtime metadata save door the deployment's SDUI component manifest, and the save door compiles an html page's `source` against it: an unknown component or a `requires` that disagrees with the source is refused with a `422`, and `requires` is stamped from the compiled source (ADR-0080 §5). + + Clause-②: yes (narrowing — on a host that registers a manifest, the runtime metadata save door newly refuses an html page whose source uses a component the manifest does not declare, or whose `requires` disagrees with its source; the new exported `SDUI_MANIFEST_SERVICE` widens `@objectstack/metadata-protocol`) + + + + **BREAKING** — an accept-set narrowing on the runtime metadata save door, shipped + as `minor` under the launch-window convention (`check-changeset-no-major` refuses + `major` until GA; breaking-ness is carried by this banner and the ADR-0087 + disposition above, not by the level). On a server that has a manifest, a + `PUT /api/v1/meta/page/NAME` (and the draft publish) of a `kind: 'html'` page used + to store the source unjudged; it now answers `422 INVALID_METADATA` when the source + uses a component the deployment's console does not provide, naming the component in + each issue's `where` and `message`, or when a hand-written `requires` lists a + namespace the source does not use, omits one it does, or names one no component in + the manifest carries. **One-line fix:** use a component the manifest declares (or + install the plugin that provides it in the console the deployment serves), and omit + `requires` — it is derived from the source. + + **The channel.** `@objectstack/metadata-protocol` exports `SDUI_MANIFEST_SERVICE` + (`'sdui-manifest'`), a plain service key. `os serve` resolves the manifest once at + boot through the same resolver `os validate` uses — the project's own + `sdui.manifest.json` beside the served config, then the copy `@objectstack/console` + ships — and registers it under that key. The save door reads the key on every + publish, so a host that registers or replaces it later is seen by the next save. + + **The compile.** The save door runs `@objectstack/sdui-parser`'s `compile()`, the + compiler behind `os validate`'s JSX page gate, against the registered manifest. Its + diagnostics carry the same rule ids the CLI reports (`jsx-forbidden-tag`, + `jsx-unknown-component`, …); errors refuse the publish, warnings ride the response's + `advisories`. A disagreeing `requires` is refused under + `page-requires-disagrees-with-source`. A page that compiles is stored with the + `requires` its source yields, on a draft save too; a draft that does not compile, or + whose `requires` disagrees, is stored as written (drafts are not gated) and its + publish refuses it. + + **Without a manifest nothing changes.** A host that resolves no manifest registers + nothing and prints one boot line — `Page source and \`requires\` not validated at + save`, naming every place looked — and the save door stores html pages exactly as + before. A registered value with no `components` map is warned about once and never + compiled against. + + Measured before the refusal shipped: the three html pages in this repository + (`examples/app-showcase`: `showcase_capability_map`, `showcase_command_center_jsx`, + `showcase_start_here`) all compile against the pinned console's manifest with no + diagnostic and yield `requires: ['ui']`; none authors `requires`. +- 7a1faf1: **`objectstack validate` and `objectstack build` now report the ADR-0087 conversions `defineStack` applied, and `objectstack validate --strict` fails on them.** + + `defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, in either mode, and prints one `defineStack: PATH: 'OLD' → 'NEW' (converted at load; conversion 'ID', retires in protocol N)` line on stderr. The two commands received that already-converted stack, so their own conversion pass found nothing to convert: `--json` answered `conversions: []` for every `defineStack` config, and `objectstack validate --strict` exited 0 on a spelling that stops loading in a named protocol major. A CI job gating on either could not see the retirement coming. + + - `@objectstack/spec`: `defineStack` (both modes) records every conversion notice it applied on the stack it returns, beside the provenance mark and stamped in the same act. The record is non-enumerable and frozen, so the schema, `Object.keys` and `JSON.stringify` never see it and no compiled artifact changes. `composeStacks` records its inputs' records in input order, counting the same built stack passed twice once. **New export:** `stackConversionsOf(value)` returns the `ConversionNotice[]` a producer recorded, the same element the commands' `conversions` field publishes, and `[]` for a value no producer returned. Like the mark, the record does not survive a spread or JSON copy. + - `@objectstack/cli`: the config loader reads the record off the default export before it merges named exports into it (that merge is a spread, which drops the record as it drops the mark). `objectstack validate` and `objectstack build` add it to their `conversions` list right after the config loads. Their own conversion pass still runs, and still reports what it converts on a key merged in from a named export of the config module, which `defineStack` never saw. The `--json` envelope keeps its shape (`valid` / `success`, `errors`, `warnings`, `conversions`): `conversions` now lists each conversion once. + + **What a CI job sees:** `objectstack validate --strict` and `objectstack validate --json --strict` now exit 1 for a config whose only advisory is a live conversion, which is what `--strict` ("treat warnings as errors") documents. Without `--strict` the exit stays 0. The fix is the one the notice names: author the canonical spelling it prints, for example `subtitle` instead of `description` on a `page:header` component. The text face lists the conversion in its warning block. The stderr line from `defineStack` is unchanged and still printed once per conversion. + + Clause-②: yes (widening) — one new export, `stackConversionsOf`, on the spec package root. Nothing `objectstack build`, or `objectstack validate` without `--strict`, accepted before is refused. `--strict` now applies its documented meaning to the conversions a `defineStack` config carries. It does not add a new rule. +- 2821e9f: feat(cli)!: `objectstack validate` and `objectstack build` refuse a `picklistExtensions` entry whose `extend` names no picklist the stack declares (#20825) + + Clause-②: no (narrowing — `objectstack validate` / `objectstack build` newly refuse a `picklistExtensions[].extend` that names no picklist the stack declares; nothing is accepted that was refused before) + + + + **BREAKING** — an accept-set narrowing on two authoring commands, shipped as + `minor` under the launch-window convention. A stack with a `picklistExtensions` + entry whose `extend` names no picklist the stack declares — `extend: 'industy'` + beside a `picklists: [{ name: 'industry', … }]` — used to pass `objectstack + validate` and `objectstack build` (which wrote the artifact). Both now exit 1 and + name the extension and the list it names (`picklist-reference-unknown`, the rule a + field's dangling `picklist` already gets). + **One-line fix:** correct `extend` to the picklist the entry adds options to, declare + the list it names (`picklists: [{ name, label, options }]`, or a `*.picklist.ts` file + the stack imports), or remove the entry. + + **Which extensions are judged.** The ones the load path registers: the top-level + `picklistExtensions` of a one-package stack, or each `packages[]` entry's own. An + `extend` resolves against every picklist the stack declares, including one a sibling + package in the same artifact owns. + + **A list from a package outside the stack is reported, not refused.** When the + package declaring the extension lists a `manifest.dependencies` entry the stack does + not carry, the list may live there, and these commands cannot read it. That + extension is an `info` notice (`picklist-reference-unverified`) in `warnings` and on + the console, naming the extension, the list and the dependencies — never a failure, + not even under `--strict`. +- b84b240: feat(cli)!: `objectstack validate` and `objectstack build` refuse a field whose `picklist` names no picklist the stack declares, and lint R8 counts `picklist` as an options source (#20825) + + Clause-②: no (narrowing — `objectstack validate` / `objectstack build` newly refuse a field `picklist` that names no picklist the stack declares; the R8 change removes a false-positive warning and widens no accept set of its own) + + + + **BREAKING** — an accept-set narrowing on two authoring commands, shipped as + `minor` under the launch-window convention. A stack with a select field whose + `picklist` names no picklist the stack declares — `picklist: 'industy'` beside a + `picklists: [{ name: 'industry', … }]` — used to pass `objectstack validate` and + `objectstack build` (which wrote the artifact). Both now exit 1 and name the field + and the list it names (`picklist-reference-unknown`). + **One-line fix:** correct `picklist` to a list the stack declares, or declare the + list it names (`picklists: [{ name, label, options }]`, or a `*.picklist.ts` file the + stack imports). + + **Which references are judged.** The ones the load path registers: the top-level + `objects` and `objectExtensions` of a one-package stack, or each `packages[]` + entry's own. A reference resolves against every picklist the stack declares, + including one a sibling package in the same artifact owns. + + **A list from a package outside the stack is reported, not refused.** When the + package declaring the field lists a `manifest.dependencies` entry the stack does not + carry, the list may live there, and these commands cannot read it. That reference is + an `info` notice (`picklist-reference-unverified`) in `warnings` and on the console, + naming the field, the list and the dependencies — never a failure, not even under + `--strict`. + + **Lint R8 (`field/select-missing-options`)** no longer reports a select, multiselect + or radio field that names a `picklist`: the picklist is its options source. The + warning it used to give pointed at `options`, which a field naming a `picklist` + cannot add — the field schema refuses the two together. A select with neither still + warns, and its fix now names both sources as alternatives. +- 12fbb2f: fix(cli)!: `os environments create` no longer takes `--clone-from`, and the client's `environments.create` request no longer declares `clone_from_environment_id` (#21028) + + Clause-②: no (narrowing) + + + + **BREAKING for scripts that pass `--clone-from`, and for TypeScript callers that pass `clone_from_environment_id`.** + + **What changed.** `os environments create --clone-from ` used to be accepted. It + sent `clone_from_environment_id` on the create request, and the control plane never + read that key: its create schema does not declare it, and an undeclared key is + stripped unread. So the command answered success and created an EMPTY environment, + with nothing saying the clone had not happened. Cloning an environment is not + implemented, so the flag is gone, and so is the key on the request type. + + - `os environments create --clone-from ` (also spelled `--clone-from=`) is + now refused by the argument parser (`Nonexistent flag: --clone-from`, exit 2) before + any request is made. Before, it created an empty environment and exited 0. + - A create without the flag is unchanged: the request body never carried the key. + - `@objectstack/client`: `client.environments.create({ … })` no longer types + `clone_from_environment_id`, so a call that passes it fails to compile, naming the + key. At runtime the request was never different, because the server dropped it. + + **The one-line fix.** Remove `--clone-from ` from the command, or + `clone_from_environment_id` from the object you pass to `client.environments.create`. + The environment it creates is the same empty one the old call created. + + **What is not affected.** Every other flag of `os environments create`, and every + other field of `client.environments.create`. Starter content is still installed into + a new environment afterwards, from the App Marketplace (`os package install`). +- 336e191: feat(spec,cli): shared seam for projecting stored metadata bodies, and the audit rewrite command + + Clause-②: no + + `@objectstack/spec/kernel` gains the family-wide primitives for the + stored-metadata-body security invariant, beside the per-type redactor registry + they build on: `STORED_METADATA_BODY_OBJECTS` / `isStoredMetadataBodyObject`, + the `STORED_METADATA_BODY_COLUMN` / `STORED_METADATA_TYPE_COLUMN` names, and + `redactStoredMetadataBody` / `redactStoredMetadataRow` / `redactStoredMetadataRows`. + These project a stored row's body through the one `getMetadataTypeRedactor` + definition, so every surface that serves, copies or evaluates such a body shares + one rule rather than a copy per package. Additive — no existing export changes. + + `@objectstack/cli` gains `os migrate audit-metadata-bodies`, the one-off rewrite + of at-rest metadata-body copies in `sys_audit_log` / `sys_activity` (dry run by + default, `--apply` to write, idempotent). + +### Patch Changes + +- addbbf0: feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518) + + Clause-②: yes (widening) + + - **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused. + - **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source. + - **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. The runtime resolves the reference onto that served field; see the picklist runtime entry of this release. + - **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package. + - **Translation.** `TranslationData` gains `picklists..{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them. + - **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES). + - **Extraction.** `os i18n extract` walks `picklists.NAME.{label, options.VALUE}`, including an extension's options under the list it extends, and `os lint` reports an untranslated option under its own rule, `i18n/missing-picklist`. + - **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column. +- ed6f734: **`objectstack lint --json` now reports the ADR-0087 conversions `defineStack` applied, as `objectstack validate` and `objectstack build` already do.** + + `defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads and prints one `defineStack: PATH: 'OLD' → 'NEW' (converted at load; conversion 'ID', retires in protocol N)` line on stderr. `objectstack lint` filled its `conversions` list only from its own conversion pass over the loaded config, which a `defineStack` default export hands over already converted. So `--json` answered `conversions: []` for a config carrying a retiring spelling, such as `description` on a `page:header` component, and the notice reached stderr alone. + + `objectstack lint` now adds the conversions the stack producer recorded on the default export to that list right after the config loads, and its own pass still reports what the producer never saw: an unbuilt default export, or a key merged in from a named export of the config module. Each conversion is listed once. The text face prints the same notices in its warning block. + + What `objectstack lint` accepts does not change: it still lints an unbuilt default export (a plain object literal), whose conversions come from its own pass as before. The exit code, `passed`, `issues` and the counts are unchanged, because a conversion notice is not a lint finding. + + Clause-②: no +- 87847a2: **When a `defineStack` or `composeStacks` call converts a deprecated spelling and then refuses the config, `objectstack validate --json`, `objectstack build --json` and `objectstack lint --json` now report both the refusal and the ADR-0087 conversions it applied.** + + `defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, such as `description` on a `page:header` component (canonical `subtitle`). When the same call then refused the config, for example on an unknown `requires` token (`STACK_CAPABILITY_UNKNOWN`), each of the three commands exited 1 with the refusal's `error` and `code` and with `conversions: []`. The conversion reached stderr only, as a warn-once line. + + The refusal now carries the conversions the producer applied before it refused (`stackConversionsOf(error)` in `@objectstack/spec`), and each command adds them to the `conversions` list of its failure payload, beside the refusal. Each conversion is listed once. A refusal whose source needed no conversion, and any other failure at load, still answers `conversions: []`. + + Nothing is accepted or refused differently: the exit code, `error`, `code` and every other key of each payload are unchanged, and no key is added. The text face is unchanged. + + Clause-②: no +- dfe5a08: A docblock line in `@objectstack/cli`'s `bin/run.js` no longer cites a tracker number + + The docblock above `bin/run.js`'s `process.stderr` `error` listener ended a + sentence with a tracker number that no longer resolves on GitHub. The number is + gone and the sentence stays: `files` names only `dist`, but npm packs a `bin` + target regardless, which is the measured fact the number was pointing at. The + file ships because of that same packing rule, which is why this is a release + note at all. Comment only: no command, flag, exit code, error code, export or + runtime behaviour changes. +- 5ad8488: Provenance comments in `@objectstack/cli`'s `bin/run.js` were re-anchored + + Three docblock lines above `bin/run.js`'s `process.stderr` `error` listener + cited a tracker number that no longer resolves on GitHub. They now cite the + commit in this repository's history that made a failed stderr write non-fatal + on the dev shim. The file ships because npm packs a `bin` target regardless of + `files`, which is why this is a release note at all. Comment only: no command, + flag, exit code, error code, export or runtime behaviour changes. +- 6073bb9: fix(cli): `os migrate meta --from N` prints the manual change that judges an applied edit beside that edit, marked review + + Clause-②: no + + Some applied mechanical edits are not the end of the job. A default flip such as + `flow-decision-mode-inclusive-explicit` writes `mode: 'inclusive'` onto a decision so + the flow keeps its old behaviour, and the manual change that judges it says the right + edit is usually none: delete the key where the branch conditions partition. That + judgment used to print hundreds of lines below the edit, among every other manual + change of the major. + + A manual change can now declare which conversions' edits it judges. In the + `Applied N mechanical change(s):` group, each run of edits by such a conversion is + followed by one line: + + ↳ review the N edits above against the manual change [protocol M] surface → replacement + + The line copies the headline the manual change prints in its own group, so its `why` + and `verify` lines are found there under the same text. Two pairs are declared today: + `flow-decision-mode-inclusive-explicit` with the decision-mode entry, and + `time-default-utc-suffix-dropped` with the time-default entry. Only declared links + pair; a manual change that merely mentions a conversion in its prose does not. + + Nothing else moves. The `N manual change(s) require your judgment:` group still lists + every manual change, byte for byte, with the same count. An edit no manual change + judges prints exactly as before. `--json`, `--out`, the exit code and the loader's + stderr are unchanged, and `--stored` is not affected. +- 3b47a69: fix(cli): `os migrate meta --from N` prints the schema verdict and the refusals first, then the applied mechanical edits, then the manual changes + + Clause-②: no + + `os migrate meta --from N` prints every semantic entry of every protocol major the + chain crosses, whatever the stack uses: a `--from 17` run prints 242 manual changes. + Those used to come before the schema verdict, which was the last line of the report + and said "resolve the manual changes above". The refusals that keep the migrated + stack from parsing were not listed at all. The only refusal list was the one printed + while the config loaded, and that list names the stack as authored, including the + keys the chain goes on to convert. + + The human-readable report now prints three groups, each opened by one header line + that counts it: + + 1. the verdict: `Migrated stack is schema-valid`, or + `Migrated stack does not yet pass schema validation — N refusals left after the chain` + followed by one `✗ path: message` line per refusal of the migrated stack; + 2. `Applied N mechanical change(s):`; + 3. `N manual change(s) require your judgment:`. + + No manual change is dropped, merged or reworded. Every applied edit and every + manual change prints byte for byte as before, in the same order within its group. + The data-migration advice is still the last thing printed. A range that holds no + migration step also leads with the verdict, which now lists the source's refusals, + and the note naming the range to use follows it. + + `--json` is unchanged: same keys, same values, same array order. The exit code is + unchanged too: a run whose migrated stack does not parse still exits 0. +- bae3859: Upgrade note for a lockfile-preserving upgrade, and the changes 17.5.0 shipped without release notes (#20622) + + Clause-②: no + + **Upgrading with a scanner.** The raised dependency floors (`hono ^4.13.5` in `@objectstack/plugin-hono-server`) cover the `hono` that objectstack loads. A lockfile-preserving upgrade can keep an older `hono` copy under `@modelcontextprotocol/sdk` (reached through `@objectstack/cli` → `@objectstack/mcp`). objectstack never loads that copy: `@objectstack/mcp` imports only the SDK's `server/mcp`, `server/stdio`, `server/webStandardStreamableHttp` and `types` modules, none of which imports `hono`. A scanner still reports it. Run `pnpm update hono` (or your package manager's equivalent) to move it to the patched line. + + **Shipped in 17.5.0 without notes.** The changesets below were in the tree 17.5.0 was published from, but its version commit did not consume them, so they shipped inside 17.5.0 without release notes. Their notes appear in this release for the first time. Breaking entries come first. + + Breaking: + + - #20458 feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name + - #20504 fix(spec,driver-turso)!: refuse a forced mode replica with no syncUrl at authoring and at construction + - #20567 feat(spec)!: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (carries two changesets) + + Also shipped: + + - #20568 fix(spec): os migrate meta guidance for fourteen more migration-entry families states each lesson in words, not tracker numbers (stage 7) + - #20572 refactor(spec): step 18 rationale as key-sorted fragments, conversionIds derived, so two retirements merge clean + - #20576 docs(spec): re-anchor the dead tracker citations in ui/ and two freed sites to the commits that decided them (stage 5) + - #20577 fix(spec,objectql): name the aggregated column at `having`, PostgreSQL only at `where` + - #20579 fix(spec,cli): os validate / os build read the ADR-0087 conversions defineStack applied — --json conversions and --strict see the producer's record + - #20582 feat(sdui-parser): the manifest marks the html tier's intrinsic tags `tier: 'html'`, ported from objectui's lockstep copy + - #20584 fix(plugin-security): an organization-less permission-set read resolves organization-less rows only + - #20585 fix(service-automation,metadata-protocol,metadata,runtime): withhold a flow's inbound-hook secret from every served definition, and keep it on a round trip + - #20591 fix(spec): nextUtcCalendarDay and utcInstantMs read years 0001..0099 as written, not as 1900..1999 + - #20598 fix(plugin-security): security/explain answers enforcement's refusal for a row-level policy comparing two fields of no shared comparison class + - #20605 fix(plugin-audit): describe sys_comment reactions and mentions by the shape they store + - #20606 docs(spec): re-anchor the dead tracker citations in the packages/spec/src remainder to the commits and ADRs that decided them (stage 6) +- fbec216: fix(cli): `os migrate meta` takes the migration chain from `@objectstack/spec/migrations` (#20646) + + `@objectstack/spec` moved the ADR-0087 migration chain and change-manifest names (`applyMetaMigrations`, `composeSpecChanges`, `MigrationFloorError`, `MIGRATION_MAJORS`, `MIGRATION_SUPPORT_FLOOR`, …) off the package root into the new `@objectstack/spec/migrations` entry, so the command now imports them from there. It replays the same chain and prints the same guidance; nothing a user types or reads changes. +- c90f9fb: fix(cli): `os dev --no-watch` turns watch mode off, and `os dev` fails when its PACKAGE argument selects no workspace package + + Clause-②: no + + `os dev` watches `objectstack.config.ts` and `src/` by default, and it already + had a branch for running without that watcher. No argument reached it: + + - `os dev --no-watch` was refused as a nonexistent flag (exit 2). + - `os dev --watch=false` is not a form the CLI reads for a boolean flag. It + parsed as `--watch` plus the PACKAGE argument `false`, so the command switched + to monorepo mode, ran `pnpm --filter false dev`, printed "No projects found", + and exited 0 with nothing started. + + What changes: + + - `os dev --no-watch` boots the environment with the watch-and-rebuild loop off. + Plain `os dev` keeps it on, as before. + - In monorepo mode, a PACKAGE argument that selects no workspace package now + exits 1, and the failure line names the value. The command passes + `--fail-if-no-match` to pnpm, so pnpm decides whether the filter matched, for + every filter form it accepts. `os dev --watch=false` is one such run: it now + fails instead of reporting success. Write `--no-watch` instead. + - `os dev --no-watch` in monorepo mode (a PACKAGE argument, or a workspace root + with no `objectstack.config.ts`) exits 1 and names the flag. In that mode each + package's own `dev` script decides whether it watches, so the CLI cannot turn + watching off. Run it in the project directory, or pass `--artifact`. + + Monorepo mode now needs pnpm 8.13.1 or later, the release that added + `--fail-if-no-match`. +- d2b188f: fix(cli): `os migrate meta` converts an object built with `ObjectSchema.create(…)` instead of stopping at load when the object carries a retired key + + Clause-②: no + + `os migrate meta` reads a config the current schema refuses, so it can rewrite the + retired keys in it. It did that for artifacts built with a `define*` helper and for + plain object literals. It did not do it for artifacts built with a factory such as + `ObjectSchema.create(…)`, which validates when it is called. An object like this: + + ```ts + ObjectSchema.create({ + name: 'ticket', + fields: { title: { type: 'text' } }, + tenancy: { enabled: true, organizationField: 'organization_id' }, + }) + ``` + + stopped `os migrate meta --from 17` at load with exit 1 and a raw JSON array of + validation issues. The message in that array told the author to run + `os migrate meta --from 17`. + + The command now loads it, applies the conversion (here + `object-tenancy-organization-field-removed`), and reports `schemaValid` for the + migrated stack, exactly as it does for the same object written as a plain literal. + This covers the five factories in `@objectstack/spec` that validate when called: + `ObjectSchema.create` (`@objectstack/spec/data`) and `App.create`, + `Dashboard.create`, `Report.create` and `Action.create` (`@objectstack/spec/ui`). + The other `create` factories spec exports return their argument unchanged and + never refused anything, so nothing changes for them. + + A schema problem the migration cannot fix is still reported: it is listed among + the refusals under the verdict, and `schemaValid` is `false`. A check that only + the factory makes when it is called, such as `ObjectSchema.create` refusing a + `managedBy: 'system-data'` object that grants no create, edit or delete, is not + part of the stack schema. It is reported on the stderr line described below and + does not change `schemaValid`, the same as `defineStack`'s own call-time checks. + `os validate` still refuses it. + + While the config loads, `os migrate meta` prints one stderr line for each + artifact the current schema refused. A raw validation error on that line is now + printed as a block, for example `ObjectSchema.create validation failed (1 issue):` + followed by one `✗ path: message` line per issue, instead of a raw JSON array. + This also applies to `define*` helpers that throw a raw validation error, such + as `defineAgent`. + + Nothing else changes. `os validate`, `os build` and every other command still + refuse the retired key at load, with the same message. `ObjectSchema.create` and + the other factories stay strict everywhere outside `os migrate meta`. The keys + of the `--json` payload are unchanged, and a run whose migrated stack does not + parse still exits 0. +- b9087d7: CLI help, warnings and refusals no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Several lines the CLI prints sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `os build` / `os validate`: the provider preflight step now reads "Checking that every required capability has a provider installable in this edition...", and the undeclared-key header reads "Undeclared authoring keys (N) — dropped at load; reported here, never refused". + - `os doctor`: the retired `referenceFilters` row now says the key was removed from FieldSchema as a key no runtime read; the `NODE_ENV` and config-load rows drop their citations. + - `os serve`: the no-auth refusal says anonymous data access is always denied with no setting that turns that off; the organizations remedies drop their citations. + - `os meta resync`, `os db clean` and the `os migrate duplicates` / `multi-value-columns` / `recorded-by` / `summary-nulls` descriptions, the `os dev --restart` flag help, the `os storage orphans` closing line and the storage-driver refusals each say what was decided instead of citing it. + - `os serve`'s unknown-hostname 404 page spells its three short grey colours in six hex digits; they render the same. + - `@objectstack/types`: the host importer's undeclared-package message says the fallback resolves from the caller once `fallbackImport` is passed, and drops the citation beside "Being merely REACHABLE is not enough". + + Text only: no exit code, error code, flag, field or control flow moves. A script that matches the old CLI text (for example the "Checking capability providers" step line) needs the new spelling. +- 32d3b3c: fix(cli): `os plugin publish` sends `visibility` only when `--visibility` is passed, so re-publishing no longer moves a `marketplace` plugin to `private` + + The `--visibility` flag of `os plugin publish` defaulted to `private`, and the CLI always + sent it in the package upsert (`POST /api/v1/cloud/packages`). That upsert is also the + re-publish path, and the control plane updates an existing package's visibility whenever + the request carries one. So re-publishing a new version of a `marketplace` plugin without + repeating `--visibility marketplace` silently set it to `private`. `os package publish` + already works this way; both commands now behave the same. + + The flag no longer has a default, and an omitted flag is an omitted key: + + - **Re-publish without the flag:** the package keeps its current visibility. + - **First publish without the flag:** the control plane applies its own default (`org` on + ObjectStack Cloud), the default `PackageSchema.visibility` declares. Before this change + `os plugin publish` sent `private` here; pass `--visibility private` to keep that. + - **With the flag:** unchanged. `--visibility private|org|marketplace` is sent and applied. + + The publish summary gains a `Visibility` line showing the control plane's answer. When the + control plane does not report it and no flag was given, the line says + `not reported by the control plane`. The "Re-run with --submit" hint now follows that + answer too, so it also fires when a `marketplace` plugin is re-published without the flag. +- def279a: fix(cli): `os package publish` sends `visibility` only when `--visibility` is passed, so re-publishing no longer moves a `marketplace` package to `org` + + Clause-②: no + + The `--visibility` flag defaulted to `org`, and the CLI always sent it in the package + upsert (`POST /api/v1/cloud/packages`). That upsert is also the re-publish path, and + the control plane updates an existing package's visibility whenever the request carries + one. So re-publishing a new version of a `marketplace` package without repeating + `--visibility marketplace` silently set it to `org` and took it out of the marketplace. + + The flag no longer has a default, and an omitted flag is an omitted key: + + - **Re-publish without the flag:** the package keeps its current visibility. + - **First publish without the flag:** the control plane applies its own default + (`org` on ObjectStack Cloud), so a new package gets the same visibility as before. + - **With the flag:** unchanged. `--visibility private|org|marketplace` is sent and + applied, as before. + + The publish summary's `Visibility` line shows the control plane's answer. When the + control plane does not report it and no flag was given, the line says + `not reported by the control plane`. The "visibility is marketplace but the version is + still draft" hint now follows that answer too, so it also fires when a `marketplace` + package is re-published without the flag. The `--submit` help text now states its + precondition as the package's visibility being `marketplace` (already stored, or set + with `--visibility marketplace`), since the flag is no longer sent on every publish. +- f20f669: fix(cli): `os migrate plan` / `apply` no longer run the app's `onEnable` or a host plugin's post-declaration hooks during their boot + + Clause-②: yes (widening) + + The two schema commands boot the host's stack to read what it declares. That boot ran the + config's `onEnable`, and every `kernel:bootstrapped` / `kernel:listening` hook a host plugin + registered from `init()`. A hook that reads a table the plan does not declare then failed on + every plan. On `examples/app-crm`, whose `onEnable` binds positions to permission sets, each + plan printed six `[sql-driver] DATABASE_ERROR` lines and six `position binding lookup failed` + warnings, on a database `apply` had just migrated as well as on an absent one. + + The boot now composes host code for its declarations only: + + - `AppPlugin` takes a new `skipOnEnable` option. When it is set, `start()` does not run the + bundle's `onEnable`, logs that it withheld it, and reports it through `onEnableWithheld`. The + migrate commands set it on the app they compose from `objectstack.config.ts`. + - A host plugin's `init()` gets a context that does not register `kernel:bootstrapped` or + `kernel:listening` hooks. The kernel contract defines those phases as work after registration + ends: reconcile/backfill, and opening listeners. `kernel:ready` hooks still run, and the + write guard still refuses their row writes. `kernel:shutdown` hooks and data hooks register + as before. + - The plan's notes, and the `--json` payload's `composition.notes`, carry one line naming what + was not run. + + The plan itself is unchanged: the same tables, the same pending DDL, the same drift. `apply` + still flushes the DDL the operator confirms and still runs the coverage pass. The platform's own + plugins are untouched, so the value-shape gate announcement still prints. + + `@objectstack/runtime` widens its public surface, additively: `AppPlugin`, exported from the + package root, gains the optional constructor option `skipOnEnable` (default `false`) and the + read-only getter `onEnableWithheld`. A composition that does not pass the option gets exactly + the behaviour it had, `onEnable` included. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- d6d6e87: feat(spec,client)!: `ActionSchema` gains `outcomeMessages` — success copy per closed handler `outcome`, interpolating `${result.*}` — and `client.environments.delete` no longer guarantees `message` on its archive answer (#21095) + + Clause-②: yes (narrowing) + + + + **BREAKING for TypeScript readers of `client.environments.delete`'s archive answer**: `message` is now `message?: string`. Code that assigns it to a `string` stops compiling at that read. Nothing else in the SDK changes, and the request is unchanged. + + **`ActionSchema.outcomeMessages`** (`@objectstack/spec/ui`). A server-executing action can succeed in more than one way: an environment delete archives, finds the environment already archived, defers a purge, or destroys it. One static `successMessage` cannot say which of these happened. The handler now reports a closed `outcome` fact in its success payload, and the action declares the copy for each outcome: + + ```ts + outcomeMessages: { + archived: 'Environment ${result.environmentId} archived.', + already_archived: 'Environment ${result.environmentId} was already archived.', + } + ``` + + - Keys are snake_case outcome names; values are `I18nLabel`s. A key that is not snake_case is refused at its own path (`invalid_key`). + - The key is valid on `type: 'api'` and `type: 'script'` actions only, the two types with a success payload that can carry an `outcome`. It is refused with a prescription on `url` / `modal` / `flow` / `form`, beside `resultDialog` (which suppresses the success toast), and beside `operation: 'update'` (no handler, so no outcome). + - `successMessage` and each outcome message may interpolate `${result.*}`, the server-response scope `onSuccess.navigate` already declares. This is not a new dialect. + - The console picks `outcomeMessages[result.outcome]`, falls back to `successMessage`, and then to its default text. The console does not read it yet. Until it does, the key is accepted, validated, translated and extracted, and the liveness ledger grades it `planned`, so `os lint` tells an author who writes it that it is not shown yet. + + **Translation.** `TranslationData` carries the copy beside `successMessage`: `objects.OBJECT._actions.ACTION.outcomeMessages.OUTCOME` and `globalActions.ACTION.outcomeMessages.OUTCOME`. Outcome keys there are snake_case too. `translateAction` overlays them per outcome (object-scoped first, then global) and only for outcomes the action declares. `os i18n extract` emits one key per declared outcome. + + **`client.environments.delete`** (`@objectstack/client`). The control plane is replacing its English `message` with the closed `outcome` fact: the server returns facts, and the console composes the message in the user's locale. The archive answer declares `outcome?: 'archived' | 'already_archived' | 'purge_deferred'`, and the teardown answer declares `outcome?: 'destroyed'`. Both `outcome` and `message` are optional, because a 200 may carry either one or both depending on which control-plane release answers. To learn what happened, read `deleted` and `purgeDeferred`, which every answer still carries, or `outcome` when it is present. +- 7164587: fix(cli): `os start` forwards SIGTERM and SIGINT to its `serve` child and takes the child down when it exits + + Clause-②: no + + `os start` runs the server as a separate `serve` child process. It used to + listen for that child's exit and nothing else. A SIGTERM or SIGINT sent to the + `start` process alone (a plain `kill`, `docker stop`, a systemd stop, a CI step) + ended `start` and left `serve` running with no parent. The port stayed bound, + `/health` kept answering 200, and the next start on that port collided with it. + + `os start` now supervises its child the way `os dev` already does, through the + same mechanism: + + - SIGTERM or SIGINT to `start` is forwarded to the child. `start` waits for the + child to shut down, then exits with the child's exit code, which is 0 after a + graceful shutdown. It used to die on the signal at once (shell status 143 for + SIGTERM, 130 for SIGINT) while the child kept running. + - Whatever else ends `start`, the child is sent SIGTERM on the way out. + - A child that exits on its own still ends `start` with the child's exit code, + as before. + + One visible side effect, the same one `os dev` already has: Ctrl-C at a terminal + signals `start` and the child together, so the child now receives SIGINT twice + and logs one `Shutdown already in progress, ignoring SIGINT` warning. The + terminal prompt also returns only after the server has stopped, not before. + + No flag, port, environment variable, banner line or `os dev` behaviour changes. +- e2ed61a: `@objectstack/cli` moves to the `@oclif/core` 5 line: its `@oclif/core` dependency goes from `^4.13.3` to `^5.1.2`. The Command classes the package exports (`CompileCommand`, `ValidateCommand`, `ServeCommand` and the rest) now build on `@oclif/core` 5, so code that extends one of them or runs it beside its own oclif setup should use `@oclif/core` 5 as well. + + Node.js 22 or later is required. Every `@oclif/core` 5 release declares `engines.node` `>=22.0.0`, the same floor this package already declared. + + The `os` commands, flags and output are unchanged. On 5.1.2 every help page renders byte-for-byte as it did on 4.13.3. The published entry also answers `--version`, `--help`, an unknown command, an unknown flag and a refused `--port` with the same exit codes and the same bytes. +- 6bff748: Provenance comments in `@objectstack/cli` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Two strings + move with them: the `os i18n extract --source-hashes` help text now says what + the provenance companion records instead of citing a number, and the header + that flag writes into each `.source-hashes.generated.ts` cites the + commit that introduced the companion. No command, flag, exit code, error code, + type, export or runtime behaviour changes. +- 315888d: feat(spec): one list of page-component slot positions, derived from the component rows and read by every page walk — `page:card`'s `footer` is now walked by all three (#20940) + + The platform has three walks that descend into a page component's `properties` bag, and each kept its own list of where child components hang: the ADR-0087 conversion walker (`children`, `body`, `footer`, `items[].children`), `@objectstack/lint`'s `walkPageComponents` (the same four) and the exported `walkAddressedPageComponents` (`children`, `items[].children`). So a node in a card's `footer` — a declared, rendered slot ("Card footer components (slot)") — was judged by `os lint` and skipped by every consumer of the exported walk: `translatePage` left its copy untranslated, `os i18n extract` offered no key for it, and objectui's validator passed it unjudged. + + **`@objectstack/spec` — new exports `pageComponentSlotPositions()` and `PageComponentSlotPosition` (`@objectstack/spec/ui`).** The component rows now mark each composition slot at its declaration, and `pageComponentSlotPositions()` derives the one list from `ComponentPropsMap`: `children`, `footer` and the panel position `items[].children`, plus the tombstoned `body` flagged `retired: true`. The marker changes nothing about the schema it marks — the parse, the JSON Schema and the authorable surface are unchanged. The list is derived on first call and memoized, never at import. `minor` because the package's public surface grows by these two exports. + + **`walkAddressedPageComponents` descends `properties.footer`.** It reads the list's authorable entries, in the list's order (`children`, `footer`, then `items[].children`); signature and return shape are unchanged. What follows from it: + + - `translatePage` translates the copy of a component in a card footer through `pages..components.`, like any other nested component. + - `os i18n extract` offers those keys, and `os i18n check` counts them, for a stack whose card footers hold components with an `id` and copy. + - objectui's validator, which judges the nodes this walk visits, now judges a card footer's nodes. + + `page:card.body` stays undescended, as #5775 ruled: it is not an authorable spelling. + + **The conversion walker reads every entry, the retired one included.** Its reach does not change: it descends `children`, `body`, `footer` and `items[].children`, as before. Stored documents still carry `body`, the renderers still draw it, and a conversion that runs before `page-card-body-to-children` meets the sub-tree there. Within one component the visit order is now `children`, `body`, `footer`, then the panels. That order is observable only as the order of the notices for a component that carries both a direct slot and panels. + + **`@objectstack/lint` — `walkPageComponents` reads the list's authorable entries.** It walks `footer` as before, and it stops walking the retired `body` spelling. The walk matches by shape, so this drops a `body` array on any component, not only on `page:card`. #5775 (maintainer ruling 2026-08-06, direction A) made `children` the one composition key. The renderers keep reading `body` only as a back-compat fallback for stored documents. On `page:card` the tombstone's rename prescription still refuses `body`, and so does the thin containers' guidance; the sub-tree is judged once it sits under `children`. So the rules built on this walk no longer report findings about nodes under any component's `body` array. The conversion walker keeps reaching them for stored documents. + + **`@objectstack/cli`:** no code change. `os i18n extract` and `os i18n check` pick up the `footer` component keys through the shared walk. The extractor's object-section pass stops reading `record:details` sections under a retired `body`, through lint's walk. + + **Why no ADR-0087 ledger entry.** Nothing an author writes moves: no spec key is retired or renamed, no stored `sys_metadata` shape changes, and no conversion or migration id is touched. `objectstack migrate meta` has nothing to act on. +- Updated dependencies [b616c0a] +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [6f1f1c1] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [a093ce3] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [b531c7b] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [7001918] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [df67985] +- Updated dependencies [42d78b9] +- Updated dependencies [3572916] +- Updated dependencies [fe463b4] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [eb4b17c] +- Updated dependencies [24d521e] +- Updated dependencies [19fc8d6] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [e651556] +- Updated dependencies [6e3aa75] +- Updated dependencies [ba4648d] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [14f80e2] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [a7d9768] +- Updated dependencies [4bf4e7e] +- Updated dependencies [cbaf04c] +- Updated dependencies [4dfff17] +- Updated dependencies [4d04b6b] +- Updated dependencies [cba417a] +- Updated dependencies [9a4b2bb] +- Updated dependencies [b80ab57] +- Updated dependencies [d282087] +- Updated dependencies [73155fe] +- Updated dependencies [0e9ad74] +- Updated dependencies [697845d] +- Updated dependencies [bbe03f4] +- Updated dependencies [9b384f6] +- Updated dependencies [8acdae9] +- Updated dependencies [91e8fa1] +- Updated dependencies [f29c83d] +- Updated dependencies [c6b37cd] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [67c1b11] +- Updated dependencies [72f8c38] +- Updated dependencies [cd901d7] +- Updated dependencies [31ed067] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [10c36cc] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [7184436] +- Updated dependencies [ed54768] +- Updated dependencies [d3f88fa] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [defc7f7] +- Updated dependencies [36d043b] +- Updated dependencies [679f95e] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [165c1d4] +- Updated dependencies [d7b9817] +- Updated dependencies [f80e2a6] +- Updated dependencies [22e584c] +- Updated dependencies [0d9349f] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [7afdc5c] +- Updated dependencies [9ad6544] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [f115b1f] +- Updated dependencies [7c5a311] +- Updated dependencies [49d2a24] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [76bd58f] +- Updated dependencies [810d42b] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [00a92e1] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [793fb83] +- Updated dependencies [4d0b9cd] +- Updated dependencies [cf0346e] +- Updated dependencies [8fec76a] +- Updated dependencies [53ed3d1] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [b84b240] +- Updated dependencies [f750119] +- Updated dependencies [6f57888] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [95fed33] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [cb4c31d] +- Updated dependencies [b1aee33] +- Updated dependencies [27bf358] +- Updated dependencies [525b813] +- Updated dependencies [05be352] +- Updated dependencies [250dec8] +- Updated dependencies [d67b942] +- Updated dependencies [f8178ff] +- Updated dependencies [8d329f0] +- Updated dependencies [d1633f3] +- Updated dependencies [5e470f8] +- Updated dependencies [5e470f8] +- Updated dependencies [32d3b3c] +- Updated dependencies [8f78495] +- Updated dependencies [a3dc817] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [0c5a71b] +- Updated dependencies [bb2eccf] +- Updated dependencies [75519e1] +- Updated dependencies [75519e1] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [1571aed] +- Updated dependencies [5dbeb7d] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [3693a1b] +- Updated dependencies [de8cd58] +- Updated dependencies [5f6b63a] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [25f2e64] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [e161ad3] +- Updated dependencies [2f2fa11] +- Updated dependencies [ae1e950] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [9b81314] +- Updated dependencies [58a77db] +- Updated dependencies [a9d36d5] +- Updated dependencies [b3d7a70] +- Updated dependencies [94990a2] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [12fbb2f] +- Updated dependencies [097ef80] +- Updated dependencies [39ab294] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [c6954d6] +- Updated dependencies [d34aa58] +- Updated dependencies [2bddb19] +- Updated dependencies [bafb8c9] +- Updated dependencies [9c8b65a] +- Updated dependencies [665cab3] +- Updated dependencies [665cab3] +- Updated dependencies [45ce12a] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [2488b98] +- Updated dependencies [cfad7de] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [327391c] +- Updated dependencies [df1feae] +- Updated dependencies [ef96c9e] +- Updated dependencies [e35c40a] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [3a7b6eb] +- Updated dependencies [ce8a6d2] +- Updated dependencies [24c554d] +- Updated dependencies [f0cc16e] +- Updated dependencies [1ecb871] +- Updated dependencies [fbcc05f] +- Updated dependencies [0b12b9e] +- Updated dependencies [c6b6889] +- Updated dependencies [ebdb6f2] +- Updated dependencies [55012df] +- Updated dependencies [30c530e] +- Updated dependencies [ce4e205] +- Updated dependencies [862f12c] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [3ddd3d0] +- Updated dependencies [c7396f1] +- Updated dependencies [ee42f00] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [be5a83c] +- Updated dependencies [4727fcb] +- Updated dependencies [d2bc644] +- Updated dependencies [7923c8e] +- Updated dependencies [2791138] +- Updated dependencies [95b91cc] +- Updated dependencies [cfa9315] +- Updated dependencies [e4e5222] +- Updated dependencies [f927864] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [61455de] +- Updated dependencies [aa23e2c] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [01e78dc] +- Updated dependencies [1741c5d] +- Updated dependencies [04b202e] +- Updated dependencies [a186aea] +- Updated dependencies [422db78] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/lint@17.6.0 + - @objectstack/rest@17.6.0 + - @objectstack/plugin-security@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/driver-sql@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/metadata@17.6.0 + - @objectstack/metadata-protocol@17.6.0 + - @objectstack/console@17.6.0 + - @objectstack/plugin-audit@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-analytics@17.6.0 + - @objectstack/service-realtime@17.6.0 + - @objectstack/service-automation@17.6.0 + - @objectstack/driver-turso@17.6.0 + - @objectstack/driver-memory@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/driver-mongodb@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/observability@17.6.0 + - @objectstack/plugin-approvals@17.6.0 + - @objectstack/plugin-auth@17.6.0 + - @objectstack/plugin-email@17.6.0 + - @objectstack/plugin-sharing@17.6.0 + - @objectstack/service-datasource@17.6.0 + - @objectstack/service-package@17.6.0 + - @objectstack/service-settings@17.6.0 + - @objectstack/service-storage@17.6.0 + - @objectstack/trigger-record-change@17.6.0 + - @objectstack/trigger-schedule@17.6.0 + - @objectstack/service-messaging@17.6.0 + - @objectstack/plugin-webhooks@17.6.0 + - @objectstack/client@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/verify@17.6.0 + - @objectstack/plugin-hono-server@17.6.0 + - @objectstack/driver-sqlite-wasm@17.6.0 + - create-objectstack@17.6.0 + - @objectstack/mcp@17.6.0 + - @objectstack/plugin-pinyin-search@17.6.0 + - @objectstack/cloud-connection@17.6.0 + - @objectstack/account@17.6.0 + - @objectstack/setup@17.6.0 + - @objectstack/service-cache@17.6.0 + - @objectstack/service-job@17.6.0 + - @objectstack/service-queue@17.6.0 + - @objectstack/service-sms@17.6.0 + - @objectstack/trigger-api@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 9d84cdcf337..6e2a8752670 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.5.0", + "version": "17.6.0", "description": "Command Line Interface for ObjectStack Protocol", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/client-react/CHANGELOG.md b/packages/client-react/CHANGELOG.md index 1bd5748e49f..9a75e217b4c 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,153 @@ # @objectstack/client-react +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [12fbb2f] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [e4e5222] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/client@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index d32b0781f2c..1bb9ec8b20b 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "main": "dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index b20c40db60c..3e6f9251f9b 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,216 @@ # @objectstack/client +## 17.6.0 + +### Minor Changes + +- 12fbb2f: fix(cli)!: `os environments create` no longer takes `--clone-from`, and the client's `environments.create` request no longer declares `clone_from_environment_id` (#21028) + + Clause-②: no (narrowing) + + + + **BREAKING for scripts that pass `--clone-from`, and for TypeScript callers that pass `clone_from_environment_id`.** + + **What changed.** `os environments create --clone-from ` used to be accepted. It + sent `clone_from_environment_id` on the create request, and the control plane never + read that key: its create schema does not declare it, and an undeclared key is + stripped unread. So the command answered success and created an EMPTY environment, + with nothing saying the clone had not happened. Cloning an environment is not + implemented, so the flag is gone, and so is the key on the request type. + + - `os environments create --clone-from ` (also spelled `--clone-from=`) is + now refused by the argument parser (`Nonexistent flag: --clone-from`, exit 2) before + any request is made. Before, it created an empty environment and exited 0. + - A create without the flag is unchanged: the request body never carried the key. + - `@objectstack/client`: `client.environments.create({ … })` no longer types + `clone_from_environment_id`, so a call that passes it fails to compile, naming the + key. At runtime the request was never different, because the server dropped it. + + **The one-line fix.** Remove `--clone-from ` from the command, or + `clone_from_environment_id` from the object you pass to `client.environments.create`. + The environment it creates is the same empty one the old call created. + + **What is not affected.** Every other flag of `os environments create`, and every + other field of `client.environments.create`. Starter content is still installed into + a new environment afterwards, from the App Marketplace (`os package install`). +- d6d6e87: feat(spec,client)!: `ActionSchema` gains `outcomeMessages` — success copy per closed handler `outcome`, interpolating `${result.*}` — and `client.environments.delete` no longer guarantees `message` on its archive answer (#21095) + + Clause-②: yes (narrowing) + + + + **BREAKING for TypeScript readers of `client.environments.delete`'s archive answer**: `message` is now `message?: string`. Code that assigns it to a `string` stops compiling at that read. Nothing else in the SDK changes, and the request is unchanged. + + **`ActionSchema.outcomeMessages`** (`@objectstack/spec/ui`). A server-executing action can succeed in more than one way: an environment delete archives, finds the environment already archived, defers a purge, or destroys it. One static `successMessage` cannot say which of these happened. The handler now reports a closed `outcome` fact in its success payload, and the action declares the copy for each outcome: + + ```ts + outcomeMessages: { + archived: 'Environment ${result.environmentId} archived.', + already_archived: 'Environment ${result.environmentId} was already archived.', + } + ``` + + - Keys are snake_case outcome names; values are `I18nLabel`s. A key that is not snake_case is refused at its own path (`invalid_key`). + - The key is valid on `type: 'api'` and `type: 'script'` actions only, the two types with a success payload that can carry an `outcome`. It is refused with a prescription on `url` / `modal` / `flow` / `form`, beside `resultDialog` (which suppresses the success toast), and beside `operation: 'update'` (no handler, so no outcome). + - `successMessage` and each outcome message may interpolate `${result.*}`, the server-response scope `onSuccess.navigate` already declares. This is not a new dialect. + - The console picks `outcomeMessages[result.outcome]`, falls back to `successMessage`, and then to its default text. The console does not read it yet. Until it does, the key is accepted, validated, translated and extracted, and the liveness ledger grades it `planned`, so `os lint` tells an author who writes it that it is not shown yet. + + **Translation.** `TranslationData` carries the copy beside `successMessage`: `objects.OBJECT._actions.ACTION.outcomeMessages.OUTCOME` and `globalActions.ACTION.outcomeMessages.OUTCOME`. Outcome keys there are snake_case too. `translateAction` overlays them per outcome (object-scoped first, then global) and only for outcomes the action declares. `os i18n extract` emits one key per declared outcome. + + **`client.environments.delete`** (`@objectstack/client`). The control plane is replacing its English `message` with the closed `outcome` fact: the server returns facts, and the console composes the message in the user's locale. The archive answer declares `outcome?: 'archived' | 'already_archived' | 'purge_deferred'`, and the teardown answer declares `outcome?: 'destroyed'`. Both `outcome` and `message` are optional, because a 200 may carry either one or both depending on which control-plane release answers. To learn what happened, read `deleted` and `purgeDeferred`, which every answer still carries, or `outcome` when it is present. + +### Patch Changes + +- c8111a5: docs(client): `automation.toggle` says it switches packaged flows only, and names a customer flow's switch (#20726) + + `client.automation.toggle` had no docblock of its own: its one line had drifted above an unrelated member. It now says that it switches packaged flows only, and that a flow authored in the deployment is refused with 409 `RESOURCE_CONFLICT`. Such a flow's switch is its `status`, sent with the complete definition through `automation.update`. This is prose only: the method's signature and behaviour are unchanged. +- e4e5222: Provenance comments in `@objectstack/client` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no request, route, error code, type, export or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index b4980a42c3a..89017710a29 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "main": "dist/index.js", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index 6fb8427ed27..c80a157c086 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,176 @@ # @objectstack/cloud-connection +## 17.6.0 + +### Patch Changes + +- f927864: Provenance comments in `@objectstack/cloud-connection` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no route, error code, refusal text, type, export or runtime behaviour + changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [7c5a311] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [76bd58f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [cb4c31d] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [2bddb19] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [a186aea] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 986337ee3fd..214ae204eec 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Runtime-side client for an ObjectStack cloud control plane — marketplace browse proxy, install-local, device-code binding, org catalog and installed views, and the /api/v1/runtime/config discovery endpoint. Open mechanism (cloud ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index 513405a925c..3f349faf7e3 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,163 @@ # @objectstack/connector-mcp +## 17.6.0 + +### Patch Changes + +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index c47942a272e..bf0b5fcc1f3 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).", "main": "dist/index.js", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index a58306aa00f..6089c09110e 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/connector-openapi +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index bbb93e11f1d..00b69dd360d 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).", "main": "dist/index.js", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index 92e9b5abd9c..642aee072bd 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/connector-rest +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 19e564fbb3e..e0d8715107a 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).", "main": "dist/index.js", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index 0162f9c2397..e6ff67cf442 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/connector-slack +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 557f9407eaa..3c4df33a812 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Slack Web API connector for ObjectStack — registers `chat.postMessage` / `chat.update` / `call` actions on the automation engine's connector registry (ADR-0018 §Addendum, ADR-0022).", "main": "dist/index.js", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 12feb8659e6..a5e17a78869 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,383 @@ # @objectstack/console +## 17.6.0 + +### Minor Changes + +- a093ce3: The SDUI manifest now marks the html tier's intrinsic tags `tier: 'html'`, and this copy of the parser carries that marker the way the renderer's copy does. + + objectui's copy of `packages/sdui-parser` gained the marker when its registry started declaring the intrinsic HTML tags a `kind:'html'` page may author (`h1`–`h6`, `p`, `a`, `code`, `span`, `table`, the sectioning tags and the rest of the roster; never `div`). This copy still declared only `'public' | 'internal'` and dropped the key. The repo-root `sdui.manifest.json` is serialised through this copy, and `@objectstack/console` ships it in its `dist`, so the published manifest listed those tags with no marker. A reader could not tell them from curated blocks. The port is byte-faithful to objectui at the console pin `dd3f7e1be356`: + + - `RegistryConfigLike.tier` accepts `'html'`, the stamp objectui's `getPublicConfigs()` puts on the roster in its projection. + - `ManifestComponent.tier?: 'html'` is new. `manifestFromConfigs` writes exactly `'html'` or omits the key, so every other entry serialises byte-identically to before. + - `generateBlockList` counts only curated blocks in its title and lists the html tier in a section of its own. + + **What moved in the published manifest:** `"tier": "html"` on 48 entries, and nothing else. It still has the same 107 components in the same order, and no other field on any entry changed. + + **What did not move:** `compile()` and `validateTree()` read the manifest as a whitelist of keys and never read `tier`. So no page's verdict changes. Measured on the three shipped `kind:'html'` pages of `examples/app-showcase`: the full `compile()` result is identical against the old and the new manifest. +- 0d42104: Console (objectui) refreshed to `31971ff1e28f`. This pin carries objectui#11353 (objectui `31971ff1e`): the console bundles one zod instance again, so the Studio's spec-derived forms render their fields — the New Package dialog creates a package and the dashboard and report inspectors show the spec schema. The previous pin shipped objectui's zod 4.4.3 beside the injected spec's 4.6.1. That commit carries no objectui changeset, so it is listed under "declared nowhere" below. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 56 releasing of 58 changesets added across 44 non-merge commits; omitted: 2 release-nothing changesets, 3 commits carrying no changeset (they ship no package code). + + - **minor** — `FormulaFieldMetadata` declares `@objectstack/spec`'s `expression` in place of `formula`, and three readers of a lookup's display pointer read the spec's `displayField` alone (obj… (objectui `19f484f54`) + - **minor** — **BREAKING** — feat(types): an authored `object-gantt` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 6) (objectui `db0beb2aa`) + - **minor** — `object-form.layout` publishes only `vertical` and `horizontal`. This is objectui#11168 slice 3 and delivers objectui#7759 group C. `@objectstack/spec` 17.5.0 retired `inline` and… (objectui `17dc16793`) + - **minor** — The form layout mirrors state the two values the spec and the renderers honour. This is objectui#11168 slice 3 and delivers objectui#7759 group C. (objectui `17dc16793`) + - **minor** — **BREAKING** — feat(layout): a navigation label written as an inline locale map renders in the viewer's locale; three inert resolver props retire (objectui#11299) (objectui `770cc5ba4`) + - **minor** — fix(types): a navigation entry's `label` accepts an inline locale map, as the spec does (objectui#11299) (objectui `770cc5ba4`) + - **minor** — **BREAKING** — feat(types): an authored `object-chart` takes its props in the `properties` bag; the flat spelling is refused by name (objectui#11276) (objectui `5262f7dd3`) + - **minor** — `navigation` is declared on the `object-timeline` block, by reference to `@objectstack/spec` (objectui#8654). This is the timeline arm of the objectui#8652 maintainer ruling: the… (objectui `95bd23c90`) + - **minor** — feat(types): a node bound through `dataSource.object` needs no `objectName` on the validator (objectui#11117) (objectui `0e6e76bc4`) + - **minor** — feat(types)!: retire `data-table`'s `selectionStyle` and `chatbot`'s `floatingConfig` on both faces, and stop teaching `data-table`'s inline-edit flags as authored keys (objectui#… (objectui `b5b928ab0`) + - **minor** — fix(components): a related list's row menu shows an action whose `visible` is blank, as its toolbar does (objectui `be5211522`) + - **minor** — **Breaking (types only):** the `ActionGroup` interface is removed from `@object-ui/types` (objectui#11168, slice 2). Nothing in this repository imported it. (objectui `cd5b19a7e`) + - **minor** — `element:definition-list`, `element:repeater` and `action:button` publish their inputs as the `@objectstack/spec` 17.5.0 rows declare them and as their renderers read them (object… (objectui `cd5b19a7e`) + - **minor** — **BREAKING (authoring, TypeScript only):** `chartConfig.aria` on a dashboard widget is now a compile error, the same verdict the validator already gives (objectui#4044). (objectui `f3c2bb0f9`) + - **minor** — **BREAKING** — feat(types): an authored `object-map` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 5) (objectui `997ce38cb`) + - **minor** — The text-family field types and every length reader use `@objectstack/spec`'s own `minLength` / `maxLength`, and the snake_case `min_length` / `max_length` are retired at once, wi… (objectui `dd5ff190e`) + - **minor** — `element:text` takes the nine `variant` values `ui:text` publishes (`h1`-`h6`, `body`, `caption`, `overline`) and renders each one the way `ui:text` does (objectui#7450). (objectui `caa0cd392`) + - **minor** — **BREAKING** — `showFilters` is retired on `object-grid` (objectui#11068). (objectui `582edef1c`) + - **minor** — fix(plugin-dashboard): a served dashboard draws its own title, description and sub-caption, not the packaged catalog's (objectui#11295) (objectui `b28bde8a4`) + - **minor** — `navigation` is declared on the `object-kanban` and `object-calendar` blocks, by reference to `@objectstack/spec` (objectui#8652). This is the objectui half of the maintainer ruli… (objectui `d79f525d9`) + - **minor** — **BREAKING** — The object-metadata write guard now holds a `select` / `radio` field that has no option source. (objectui `1563d3e10`) + - **minor** — **BREAKING** — The Field Designer's drawer no longer offers `select` as a field type for a new field, or as a type to change an existing non-choice field into. (objectui `1563d3e10`) + - **minor** — A related list inside a record now places its child object's `record_related` actions on each row (objectui#11270; the renderer half of the enforce answer on objectstack-ai/object… (objectui `a8b988933`) + - **minor** — **BREAKING** — feat(types): an authored `object-form` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#10859, batch 4) (objectui `e3782d26e`) + - **minor** — **BREAKING (authoring):** `assignedProfiles` on a `page` node is now refused on both published faces (objectui#9409). (objectui `02f1813f8`) + - **minor** — `JoinedReportBlock` is now the spec's own type, derived from the installed `@objectstack/spec` instead of hand-written (objectui#10940). It is `JoinedReportBlock` from `@objectsta… (objectui `92970c4d6`) + - **minor** — **BREAKING** — fix(core): `ActionDef` no longer declares `aria`, which `@objectstack/spec` 17.5.0 retired on an action (objectui `e2271568f`) + - **minor** — feat(types): four declared keys nothing honoured are retired on both faces, and two chatbot keys gain their zod mirror (objectui#6152, round 4) (objectui `3e4fa2cfb`) + - **minor** — A blank gate predicate is diagnosed on the three paths that still drew it in silence, and objectui's two gate mirrors whose protocol key refuses a blank now refuse it too (objectu… (objectui `58da8aeca`) + - **patch** — fix(plugin-detail): `record:quick_actions.requiredPermissions` publishes what the gate does — the contract's shared record-block describe, verbatim (objectui `fd6f5da44`) + - **patch** — perf(console): the first screen no longer downloads the spec entries only the metadata designers' validation uses (objectui `993f27e60`) + - **patch** — docs(plugin-gantt): authored `object-gantt` examples write their props in the `properties` bag (objectui#10859, batch 6) (objectui `db0beb2aa`) + - **patch** — The page-block inspector no longer offers `Inline` and `Grid` for an `object-form`'s `layout` (objectui#11168 slice 3, objectui#7759 group C). `@objectstack/spec` 17.5.0 refuses b… (objectui `17dc16793`) + - **patch** — fix(app-shell): a Studio pillar whose draft load is cancelled no longer leaves its canvas on "Loading…" (objectui#11331) (objectui `bef9f204a`) + - **patch** — fix(app-shell): a navigation label written as an inline locale map shows in the viewer's language across the console (objectui#11299) (objectui `770cc5ba4`) + - **patch** — chore(plugin-designer): follow `@object-ui/layout`'s and `@object-ui/types`' navigation label changes (objectui#11299) (objectui `770cc5ba4`) + - **patch** — fix(runner): the sidebar no longer reads the app's retired `version` key (objectui `47e3ce008`) + - **patch** — fix(plugin-gantt, plugin-calendar): the `objectName` input description names the `dataSource.object` binding (objectui#11117) (objectui `0e6e76bc4`) + - **patch** — fix(plugin-grid): a grid row shows an action whose `visible` is blank, as the action's toolbars do (objectui `be5211522`) + - **patch** — `object-calendar` is taught with its `calendar` block, not with flat field-name keys (objectstack-ai/objectui#8831). (objectui `50583364a`) + - **patch** — docs(plugin-map): authored `object-map` examples write their props in the `properties` bag, and the `objectName` input names the `dataSource` binding (objectui#10859, batch 5) (objectui `997ce38cb`) + - **patch** — Message text only: the refusal an `element:text` node draws for an authored `body` or `children` describes what the block renders in the converged `variant` vocabulary, the headin… (objectui `caa0cd392`) + - **patch** — The Studio page-block inspector's `element:text` **Variant** select offers the nine values `ui:text` publishes (Heading 1 to Heading 6, Body, Caption, Overline) in place of Headin… (objectui `caa0cd392`) + - **patch** — The `page` preview sample authors its "Quick links" section heading as `variant: 'h3'`, replacing the pre-convergence spelling `subheading` that the ruling retires (objectui#7450)… (objectui `caa0cd392`) + - **patch** — fix(app-shell): Studio's Automations rail stops telling a flow with a declared trigger that it has "no trigger" (objectui `9cfe9977f`) + - **patch** — fix(app-shell): a served dashboard and a served list view are named as served, on the dashboard page, the view tab and the breadcrumb (objectui#11295) (objectui `b28bde8a4`) + - **patch** — fix(layout): a present navigation label renders as written, with no exception, and an inline locale-map label renders its text (objectui#11201) (objectui `1ccb5ba7d`) + - **patch** — fix(app-shell): a Studio pillar never saves one item's document into another while the second is loading, after a package switch, or after visiting a non-editable leaf (objectui `5f2d9676c`) + - **patch** — docs(plugin-form): the README's authored `object-form` examples write their props in the `properties` bag (objectui#10859, batch 4) (objectui `e3782d26e`) + - **patch** — The flow designer's edge `condition` type now mirrors the server's edge slot, the spec's `EvaluatedExpressionInput` (objectui#8946). (objectui `48401689f`) + - **patch** — docs(core): the `ElementDataSourceConfig.filter` note now separates what an author may write from what a renderer may still receive (objectui#8945) (objectui `969d4f291`) + - **patch** — fix(app-shell): the generic metadata editor renders a stored `view`, and a string-array repeater edits strings (objectui `1b30c0fe0`) + - **patch** — `object-grid` re-reads its rows when an input its query reads changes: a `conditionalFormatting` rule, a row or bulk action def, or the view's `searchableFields` (objectui#10689). (objectui `be0ad007b`) + - **patch** — `list-view` re-reads its rows when a `conditionalFormatting` rule, a row action def or a bulk action def adds a field its predicates read (objectui#10689). (objectui `be0ad007b`) + - **patch** — fix(app-shell,components,console): the three remaining select placeholders show the locale's own "Select…" word (objectui#11252) (objectui `7fed09d07`) + - **patch** — fix(plugin-dashboard): a dimensionless table renders a row of every measure, and a dimensionless chart one mark per measure (objectui `b4333ab8a`) + + ⚠️ 12 of these carry a breaking change: 12 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 3 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ fix(console): an injected spec shares the console's one zod instance (objectui#11327) (#11353) (objectui `31971ff1e`) + - _(no changeset)_ ci(half-state-patrol): call objectstack's composite action pinned to a sha, with the no-anchor opt-in (objectui#11174) (#11332) (objectui `2c274e3a8`) + - _(no changeset)_ docs: layout.md names the object-grid object with objectName, and flex.mdx teaches the four direction values (objectui#11298) (#11314) (objectui `0c6f9bbd7`) + + + + objectui range: `e420df310f5b...31971ff1e28f` +- a3d7588: Console (objectui) refreshed to `db11afd4967c`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 89 releasing of 99 changesets added across 113 non-merge commits; omitted: 10 release-nothing changesets, 20 commits carrying no changeset (they ship no package code). + + - **minor** — An `object-grid` now honours `description` and `emptyState`, and four keys it declared but never read are retired (objectui#11068). (objectui `db11afd49`) + - **minor** — fix(app-shell,core): the record page's Undo captures only what the record carries, through core's one capture rule (objectui#11082) (objectui `76e9df06c`) + - **minor** — The generated JSX types let a declared input win with its type for every base prop, so `sdui-intrinsics.d.ts` compiles as generated (objectui#11075). (objectui `4d377ed65`) + - **minor** — A `record:path` whose stage labels are per-locale maps now shows the viewer's language instead of failing to render, and five label inputs on `object-metric`, `object-grid` and `r… (objectui `27ae63279`) + - **minor** — fix(plugin-form): `object-form`'s `modalCloseButton: false` hides the modal's close button (objectui `559a2e207`) + - **minor** — A number field's authored `useGrouping` now decides whether its value renders with thousands separators (objectui#11026). This is the renderer half of `FieldSchema.useGrouping`, t… (objectui `ae582b70a`) + - **minor** — `element:text`, `element:button`, `element:image` and `element:number` now render the accessible name an author declares in their `aria` prop (objectui#11051). (objectui `a4b017eda`) + - **minor** — An `object-view`'s `table` now hands the grid it draws every grid key it types, and stops typing the grid keys that had nothing to act on there (objectui#10976). (objectui `24a0f146e`) + - **minor** — The base-prop list is declared once, and the renderer's `visibleWhen`, `hiddenOn` and `testId` join it (objectui#11044). (objectui `c80236ec8`) + - **minor** — fix(app-shell): the exported sign-in page says why a sign-in is refused, in the reader's language (objectui#11058) (objectui `827550193`) + - **minor** — An `object-form` whose `title`, `description`, `submitText`, `cancelText`, `nextText`, `prevText` or `successMessage` is a per-locale map now shows the viewer's language instead o… (objectui `9b85600b0`) + - **minor** — fix(app-shell): the exported register page says why a sign-up is refused, in the reader's language (objectui#11030) (objectui `180a34a47`) + - **minor** — **BREAKING** — feat(core)!: bare-string `globalFilters[].options` is no longer lifted; use `{ value, label }` objects, the spec's form (objectui#4356). (objectui `a51fa0cca`) + - **minor** — The strict authoring face accepts a correctly authored `metric-card` in a dashboard's widget slot (objectui#11022). This widens a published accept set; nothing that parsed before… (objectui `0eb9f36ac`) + - **minor** — **BREAKING (shipped as `minor` — see below):** nineteen ADR-0080 public-block arms and the dashboard widget slot's `metric-card` node now refuse an authored `children` by name. No… (objectui `f6fb83f0c`) + - **minor** — New SDUI widget `cloud:plan-status`: a "Current plan" badge for one plan card on the Cloud pricing page, shown when that card's plan is the organization's plan (objectui#10919). (objectui `24d3e6562`) + - **minor** — **Narrowing — `ObjectView`'s props are the declared `ConsoleObjectViewProps`, not `any`.** (objectui `ad0310fb5`) + - **minor** — **BREAKING (shipped as `minor` — see below):** thirteen node types now refuse both content channels by name. Each one's renderer reads neither `body` nor `children`, so an authore… (objectui `2049b03df`) + - **minor** — feat(types): `ObjectMapConfigSchema` is `.strict()`, so `objectui validate` refuses an undeclared key in an `object-map` node's `map` block (objectui#5157) (objectui `d6d8fb9d7`) + - **minor** — A master-detail form whose `title`, `submitText` or `cancelText` is a per-locale map now shows the viewer's language instead of crashing or toasting "[object Object] saved" (objec… (objectui `8aa68b159`) + - **minor** — feat(components): a `kind: 'react'` page's author scope injects `useDataInvalidation` (objectui `deca847a8`) + - **minor** — **BREAKING** — A spec-shape conditional-formatting rule's `condition` and a bulk action's `visible` now declare the named view's own expression slots, read by reference from `@objectstack/spec`… (objectui `d570eaa59`) + - **minor** — **BREAKING** — `drillDown` is retired on the bare `pivot` node: author `object-pivot` to drill (objectui#10932) (objectui `cc4e47638`) + - **minor** — The console's assistant dock binds its build conversation to the app you are in (objectui#10926). (objectui `fe563943b`) + - **minor** — Four Console surfaces that read English under a zh-CN session now read the session's language (objectui#10900). English stays the default. (objectui `328abeb55`) + - **minor** — `safeValidateSchema` — and so `objectui validate` — accepts `element:number`, the ADR-0080 public block held back from batch 1, in both of the binding forms `@objectstack/spec` ac… (objectui `b45d463a9`) + - **minor** — `element:number` reads its object from the node-level `dataSource` binding, as the spec declares it (objectui#10909). (objectui `dd0d78f74`) + - **minor** — `safeValidateSchema` — and so `objectui validate` — accepts three more registered node types: `pivot`, `object-metric` and `object-master-detail-form` (objectui#10859, batch 2). (objectui `3b469c8ea`) + - **minor** — **BREAKING (shipped as `minor` — see below):** the `input` node type now refuses both content channels by name. Its renderer reads neither `body` nor `children`, so an authored ch… (objectui `7e8b3c033`) + - **patch** — fix(console,plugin-form,i18n): the public form page and the master-detail form chrome speak the user's language (objectui `54997fffa`) + - **patch** — fix(app-shell): Studio's app preview resolves a locale-map app, nav-item and group label (objectui#11100) (objectui `8a5ae3d63`) + - **patch** — Nine blocks now render the accessible name an author declares in their nested `aria` bag (objectui#11083, batch 2). (objectui `0ecaa7dbb`) + - **patch** — The dashboard's refresh button now speaks the session language (objectui#11097). `DashboardRenderer` and `DashboardGridLayout` hard-coded "Refresh All", "Refreshing…" and the acce… (objectui `cff8641e2`) + - **patch** — Grouped grid lists whose columns are objects load their rows again, instead of showing INVALID_FIELD in every group (objectui#11105). (objectui `3100bef65`) + - **patch** — An `object-grid` whose deprecated `title` is a per-locale map now shows the viewer's language in the table caption and the export file name, instead of failing to render (objectui… (objectui `0bc5c5a01`) + - **patch** — A page now renders the accessible name an author declares in its `aria` bag, and the list view and the `record:*` blocks read the same bag through the one shared reader (objectui#… (objectui `b24f93a72`) + - **patch** — fix(plugin-timeline): a gantt-variant timeline draws its headers and bars on one continuous axis, so each bar lines up under its header (objectui#11079) (objectui `2fb0f9ab8`) + - **patch** — fix(react): one Ctrl+Z undoes one record write, and Ctrl+Z inside a text field is the field's own undo (objectui#11081) (objectui `06451334b`) + - **patch** — feat(app-shell): Studio's app, permission and view previews show the authored area descriptions, RLS policy labels and view label (objectui `5b2ea1757`) + - **patch** — The console now honours a dashboard's authored `refreshIntervalSeconds` (objectui#11062). `DashboardView` used to render `DashboardRenderer` with no `onRefresh`, and the renderer'… (objectui `88fbd793d`) + - **patch** — fix(app-shell): Studio's flow start node stops offering 「Platform event」, a trigger no engine routes (objectui `17fc68873`) + - **patch** — fix(plugin-calendar): a week or day resize of an overnight event keeps its other edge (objectui#11060) (objectui `3e71a9825`) + - **patch** — feat(app-shell): the flow designer shows a connector action's description and offers its output references (objectui `a5841be35`) + - **patch** — The console's undo confirmation toast reads the session's language (objectui#11056). (objectui `873284657`) + - **patch** — fix(plugin-gantt): a gantt move shifts a task's end by the calendar days it shifts the start, keeping each value's time of day across a DST change (objectui#10866, slice 6) (objectui `e119f120c`) + - **patch** — fix(app-shell): a record page's delete asks through the console's own confirm dialog (objectui#11001) (objectui `981389ba3`) + - **patch** — The form family's own feedback chrome now reads the locale packs (objectui#11039). The default success toast, the thank-you heading, the loading line, the load-failure heading and… (objectui `51c294958`) + - **patch** — fix(app-shell): Studio's flow start node writes the `api` trigger the engine routes, and can set its secret (objectui `b31591b4a`) + - **patch** — fix(plugin-gantt): a zoned chart reads and writes a stored day as that day on a DST change (objectui#10866, slice 5) (objectui `f6ae5e22d`) + - **patch** — fix(plugin-dashboard): the auto-refresh interval reads its handler through a ref, not a memoised identity (objectui `6466a09df`) + - **patch** — `objectui check` prints the key and path a file was refused for, and no longer ends with 「✓ All checks passed」 over files it never validated (objectui#11007). (objectui `37a19d4e0`) + - **patch** — fix(plugin-calendar): a week or day view move keeps the event's own length and grab point (objectui#11037) (objectui `2a1779f96`) + - **patch** — The console strings objectui#10900 left English under zh-CN now read the session's language (objectui#10969). (objectui `6cd8f66e8`) + - **patch** — fix(plugin-view): `ObjectView` fetches only for views that draw the rows, and a re-read keeps them on screen (objectui `30f912a0d`) + - **patch** — The Add reaction button renders only on a comment row of a record's discussion feed (objectui `62d6f56bb`) + - **patch** — A record form whose fields are all locked because the user may not create (or edit) records of its object now says so (objectui#11000). The ADR-0092 D4 lock disables every field w… (objectui `bf7ab35ce`) + - **patch** — Citations of objectstack cards now name their repository (objectui#11016). (objectui `63ab76112`) + - **patch** — A `matrix` report that declares `columns` now draws its declared `chart` below the cross-tab (objectui#10964). `ReportSchema` has always accepted a matrix report carrying both `co… (objectui `49f76725a`) + - **patch** — `validateTree` no longer calls a declared `bind` or `hidden` unknown (objectui#11008). (objectui `99878d8e3`) + - **patch** — `WizardForm`'s own chrome now reads the locale packs (objectui#10999). The default Cancel, Back, Next, Submitting, Create and Update labels, the "Step x of y" counter, the step in… (objectui `2eaf5be27`) + - **patch** — fix(plugin-calendar): a month-grid move keeps the wall-clock time across a DST change (objectui#11005) (objectui `f9b6dfe5a`) + - **patch** — With a `page` record surface and no `onNavigate` handler, `ObjectView`'s New button, a row click and Edit now open the record form on the drawer (objectui#11015). (objectui `3ad61001b`) + - **patch** — A reaction click on a record page's discussion keeps every other user's stored reaction (objectui `4e5cb61a9`) + - **patch** — `record:alert`: a `body` written directly on the node, rather than inside `properties`, is now refused with a pointer to `properties.body`, where the banner's message text lives (… (objectui `b3c96d6bc`) + - **patch** — fix(console): a sign-up refused by the server's audience gate reads in the session's language (objectui#10998) (objectui `e327c8998`) + - **patch** — fix(app-shell): a page action refreshes a custom page's data in place instead of remounting the page (objectui#10519) (objectui `a33cf7e92`) + - **patch** — Correct the last four published "no error, no warning" clauses that the parser tier contradicts (objectui#10981, closing the family of objectui#10928 and objectui#10959). (objectui `797a30f48`) + - **patch** — fix(app-shell): a failed reaction write takes the reaction back and says so, instead of staying shown as applied (objectui#10899) (objectui `e2dffc9d1`) + - **patch** — Under a `split` or `popover` navigation, `ObjectView`'s New button now opens a create form (objectui#10975). (objectui `e9ca14ca9`) + - **patch** — fix(plugin-calendar): a day event moved across a DST change writes the days it was dropped on (objectui#10866, slice 4) (objectui `665025908`) + - **patch** — `DashboardGridLayout` and `DashboardRenderer` now share one auto-refresh timer (objectui#8820). (objectui `1f5a6445c`) + - **patch** — fix(components): an `element:number` that asks for an aggregate and names no object says so instead of painting a silent dash (objectui `4b742f41d`) + - **patch** — The metadata form's code editor reads and writes an expression slot through the ADR-0089 envelope (objectui#10963). (objectui `79a935c87`) + - **patch** — fix(app-shell, plugin-detail): the unmapped activity type warnings no longer point at an objectstack issue that answers 404 (objectui `285e36bd1`) + - **patch** — Correct a false clause in the `header-bar` refusal messages and in nine `body?: never` docblocks (objectui#10959). (objectui `42687baf2`) + - **patch** — A named view's `navigation`, `fieldOrder` and `inlineEdit` now reach the registered `object-view` renderer's grid (objectui#10885, member 4). (objectui `4d22e3351`) + - **patch** — fix(plugin-timeline,core): the timeline's gantt axis draws a stored date-only day on that day in every viewer zone, and the formula date functions do their day arithmetic on the U… (objectui `9e6619ffa`) + - **patch** — fix(plugin-chatbot): the confirm-changes card's actions wait until no turn is in flight (objectui `be66b5621`) + - **patch** — Correct a false clause in the content-channel refusal messages (objectui#10928). (objectui `95a7c8d38`) + - **patch** — fix(plugin-chatbot): the proposed-plan card's actions wait until no turn is in flight (objectui `7d82957b1`) + - **patch** — fix(app-shell): console actions supply the spec-declared `${ctx.org.*}` scope (objectui#10918) (objectui `06a96e948`) + - **patch** — The registered `object-view` renderer reads a named grid view's own grid members off the named view (objectui#10885). (objectui `b73e15bf7`) + - **patch** — fix(plugin-designer): editing an app keeps its stored navigation (objectui#10894) (objectui `44b67dabd`) + - **patch** — fix(app-shell): the assistant FAB is hidden while the chat dock is open (objectui#10899) (objectui `ac15833eb`) + - **patch** — fix(plugin-chatbot): a reloaded multi-step build no longer shows an empty 「执行过程」 block under every step (objectui#10899) (objectui `ac15833eb`) + - **patch** — fix(data-objectstack, plugin-dashboard): a dataset tile the viewer may not read shows a localized "no access" state (objectui#10899) (objectui `ac15833eb`) + - **patch** — fix(app-shell, plugin-detail): a record comment whose write fails is never shown as sent (objectui#10899) (objectui `ac15833eb`) + - **patch** — fix(app-shell): the marketplace offers an "update" only for a HIGHER version (objectui#10899) (objectui `ac15833eb`) + - **patch** — fix(app-shell): the Studio edit/design affordances follow the server's authoring capability (objectui#10899) (objectui `ac15833eb`) + + ⚠️ 6 of these carry a breaking change: 6 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 20 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ docs(plugin-gantt): the drag's time-of-day sentence names the shift-band exception (objectui#10866) (#11110) (objectui `d1e683fa1`) + - _(no changeset)_ fix(ci): the Test aggregator re-reads a present shard the jobs API answered with no conclusion (objectui#10931) (#11108) (objectui `80c2d5e61`) + - _(no changeset)_ docs(changeset): date-note nine pending entries whose repository count moved (objectui#10979) (#11046) (objectui `480de81fd`) + - _(no changeset)_ docs(changeset): date-note three pending entries on the pivot node's drillDown that PR #10972 made false (objectui#10974) (#11045) (objectui `338482fe7`) + - _(no changeset)_ fix(ci): re-pin the console eager-closure baseline and ceiling on main's own reading (objectui#10996) (#11018) (objectui `6c57c779e`) + - _(no changeset)_ fix(scripts): parse a `json` doc fence strictly with parseJsonFence (objectui#10943) (#10985) (objectui `f667c1df9`) + - _(no changeset)_ docs(agents): split the ruleset bullet, and date the governed guard's enrolment as a required context (objectui#9520) (#10984) (objectui `a097316a2`) + - _(no changeset)_ docs(contributing): a repository count in a changeset is a reading at a named commit (#10978) (objectui `a2de9e943`) + - _(no changeset)_ docs(changeset): date the spec-symbol gate's export filter in the #6286 release note (objectui#9528) (#10970) (objectui `7a9db9148`) + - _(no changeset)_ docs(agents): a force-push is governed by the landing repo's AGENTS.md (objectui#9666) (#10958) (objectui `4dc491a12`) + - _(no changeset)_ fix(scripts): walk both sides of a pipe in the strict-face measurement twin (objectui#10076) (#10966) (objectui `c32890016`) + - _(no changeset)_ docs(adr): ADR-0057 Amendment A2 — the dock binds its build thread to the current app (objectui#10926) (#10947) (objectui `1345e182d`) + - _(no changeset)_ fix(scripts): correct the RETIRED_FIELD_TYPES `excluded` sentence that dropping the line refutes (objectui#10070) (#10961) (objectui `b120b6607`) + - _(no changeset)_ fix(ci): label PRs touching the published docs tree `documentation` (objectui#10012) (#10960) (objectui `97dabdc5b`) + - _(no changeset)_ docs(changeset): date the three named-view census entries that PR objectui#10884 made false (objectui#10885, member 3) (#10955) (objectui `462c85868`) + - _(no changeset)_ docs(changeset): the read-rate banner and its hook are not exported from the package entry (objectui#10913) (#10954) (objectui `a4fb7082a`) + - _(no changeset)_ docs(skills): testing.md Pattern 5 publishes `userRole` through the scope channel the renderer reads (objectui#9380) (#10941) (objectui `7e1a8d098`) + - _(no changeset)_ fix(scripts): the polarity census pins stop asserting what the live `.changeset/` holds, so the post-version tree validates (objectui#10010) (#10933) (objectui `a93ba8792`) + - _(no changeset)_ fix(console): drop the two optimizeDeps.include entries the dev server cannot resolve (objectui#10865) (#10938) (objectui `51401e63c`) + - _(no changeset)_ docs(agents): port objectstack's model-free commit-trailer rule into the multi-agent section (objectui#9441) (#10922) (objectui `af2221d45`) + + + + objectui range: `dd3f7e1be356...db11afd4967c` +- b8191f7: Console (objectui) refreshed to `e420df310f5b`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 87 releasing of 93 changesets added across 90 non-merge commits; omitted: 6 release-nothing changesets, 1 commit carrying no changeset (they ship no package code). + + - **minor** — `flex`, `object-grid` and `object-chart` accept the node-level `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares on every page component, and judge it a… (objectui `f3135a4d1`) + - **minor** — Setup › Packaged automation shows the platform's reason for a packaged flow the engine has not armed, verbatim and in muted text under the flow name (objectui#9217). (objectui `1cb3732ce`) + - **minor** — **BREAKING (scored `minor` per this repo's version-alignment convention)** — `ObjectKanbanSchema.onQuickAdd` is retired on `object-kanban` (objectui#11234). This completes ruling… (objectui `52aad5cef`) + - **minor** — fix: a row-menu action gated through `disabled` stays disabled until the permissions payload has loaded, in a grid and in a related list's table, and an `` answers… (objectui `18d1a0abd`) + - **minor** — `safeValidateSchema` — and so `objectui validate` — and `StrictAnyComponentSchema` judge a component nested in a page container's props bag (objectui#11223). (objectui `7d074baae`) + - **minor** — `reference` is now the only spelling ObjectUI writes or reads for a relational field's target object (objectui#11070, round 4, under the objectui#6837 ruling: 「objectui不是前端的项目吗?后端… (objectui `f61dab169`) + - **minor** — `record:related_list` now renders the actions its `actions` key names (objectui#11163; ENFORCE ruling on objectstack#20665). (objectui `f4ed2387e`) + - **minor** — feat(app-shell): the sidebar and `nav:menu` hide a `doc` entry the member may not read (objectui#10188) (objectui `d6a1a80d5`) + - **minor** — feat(console): the docs portal refuses a doc or book the member may not read, and opens a doc in the book that claims it (objectui#10188) (objectui `d6a1a80d5`) + - **minor** — feat(layout): a host can hide a `doc` navigation entry the member may not read (objectui#10188) (objectui `d6a1a80d5`) + - **minor** — Every ADR-0080 public-block arm accepts the node-level `responsiveStyles` that `@objectstack/spec`'s `PageComponentSchema` declares, and judges it as the spec does (objectui#10872… (objectui `54a78308a`) + - **minor** — fix(types,components,plugin-form,console,core): a faulted `visibleWhen` refuses the submit, naming the field and the rule; a blank field rule is refused; blank gates are diagnosed… (objectui `af9e9572c`) + - **minor** — `safeValidateSchema` — and so `objectui validate` — accepts one more registered node type: `object-timeline` (objectui#10859, batch 3). (objectui `ae0b9d390`) + - **minor** — feat(app-shell): the flow node inspector marks the config keys the installed spec refuses the node without (objectui#10948). (objectui `68c9ca721`) + - **minor** — **BREAKING** — The formula and summary field widgets read `@objectstack/spec`'s own spellings, `returnType` and `summaryOperations`, and the snake_case spellings they used to read are retired at… (objectui `615346d61`) + - **minor** — feat(app-shell): Studio has a Markdown editor for `doc` items, with a live preview and book placement (objectui#10188) (objectui `02fe8ca8a`) + - **minor** — Declare the `filter` and `sort` inputs on the `object-map`, `object-gantt`, `object-timeline` and `view:timeline` registrations (objectui#8220) — the html tier stops reporting `un… (objectui `233a1b318`) + - **minor** — fix: an action gated on `current_user.can(object, verb)` stays hidden until the permissions payload has loaded on every action `visible` surface, a `page:header` action gated thro… (objectui `8bab1571d`) + - **minor** — fix(fields): a currency grid column's width is its currency's minor unit, never an authored `scale`; a hydrated currency column's `scale` is reported (objectui#10783) (objectui `b32e7debc`) + - **minor** — **BREAKING** — `quickAdd` is retired on `object-kanban` (objectui#8285, ruling B of the director seat's decision batch #91: the board does not grow an inline record-creation write… (objectui `6f864cf62`) + - **minor** — feat(types): four zod mirrors declare members their TypeScript twins already declared, and `pagination` retires its `page` spelling (objectui#6152, round 3) (objectui `0c95d3d8d`) + - **minor** — The four `page:` containers take their child list in `properties.children`, the member their `@objectstack/spec` row declares, and refuse a node-level `children` by name: `page:ca… (objectui `dded788ad`) + - **minor** — fix(app-shell,plugin-detail): the record feed says "no permission" when its read is refused, instead of showing an empty list (objectui `1263e405d`) + - **minor** — `ObjectGrid` takes `onNavigate` as a component prop, and the list channel's navigation callback takes one closed mode token, `'view' | 'new_window'`. (objectui `c3df43a42`) + - **minor** — feat: a `doc` navigation entry (ADR-0046) validates and draws as a link into the docs portal (objectui `e6bc087a3`) + - **minor** — **BREAKING** — **The console reads a saved view by the spellings `@objectstack/spec` declares, and stops reading the keys nothing writes (objectui#11013).** This is the console end of the ruling… (objectui `3c13675e5`) + - **minor** — feat: an action's `visible` / `disabled` predicate can ask `current_user.can(object, verb)` — the caller's object permissions, from the payload the built-in Edit / Delete buttons… (objectui `9cebfca5a`) + - **minor** — A filter on a record page can now be scoped to the record the page shows (objectui#7297). Write `{record_id}` as a filter value, for example `{ "assignee": "{record_id}" }` on an… (objectui `cfc9b6db9`) + - **minor** — A page size with nothing declared is now the one `@objectstack/spec` declares for `pagination.pageSize`, on every surface that has a pager; a fetch that has no pager keeps its own… (objectui `de5d400bf`) + - **minor** — The four `action:*` blocks now publish the `@objectstack/spec` keys their renderers honour, and stop publishing what the spec refuses (objectui#11168, slice 1). Each key was decid… (objectui `3cc4fe567`) + - **minor** — **Breaking behaviour change — `object-tree` now honours only the `data` spelling its published row declares.** (objectui `846cec0ef`) + - **minor** — feat(types): the `object-form` zod mirror declares the members its TypeScript twin already declared (objectui#6152, round 1) (objectui `3a3db763b`) + - **minor** — `record:details`, `record:highlights` and `record:related_list` declare the field-security triple — `enforceFieldSecurity`, `redactFields` and `requiredPermissions` — on their pub… (objectui `647908686`) + - **minor** — The six public blocks that objectui#10872 batch 4 armed now refuse an authored `children`, and name the right remedy for a flat `body`: `action:button`, `action:icon`, `action:gro… (objectui `3f9d9263e`) + - **minor** — feat(types,layout,app-shell): a navigation entry with no `label` shows its target's current label, resolved at render time (objectui#9868) (objectui `a8198de22`) + - **minor** — `safeValidateSchema`, and so `objectui validate`, accepts the six ADR-0080 public blocks held back until `@objectstack/spec` carried a `ComponentPropsMap` row for each: `action:bu… (objectui `e978ed5ea`) + - **minor** — The strict authoring face accepts keys a registered renderer reads, which it used to refuse as undeclared (objectui#11070). Each key below is now declared on the TypeScript face a… (objectui `b0a05dda1`) + - **minor** — feat(cli): `objectui check` refuses a `${…}` on a text key its node never evaluates (objectui `33da643e9`) + - **minor** — objectui now resolves `@objectstack/*` 17.5.0 and `zod` 4.6.5, and follows every contract move that release makes (objectui#11073). `@objectstack/spec` 17.5.0 and `@objectstack/co… (objectui `81f849852`) + - **minor** — fix(plugin-gantt,core,plugin-timeline): both gantt surfaces read a date-only end inclusively through one core rule, and a drag writes the same day back (objectui#11141) (objectui `858eafb4f`) + - **patch** — fix(plugin-detail): a related list's `list_toolbar` action authored `visible: false` is hidden (objectui `e420df310`) + - **patch** — fix(console): the docs portal's book sidebar keeps a doc placed by its own `group` key from outside the book's package (objectui#11245) (objectui `f16c01e90`) + - **patch** — fix(app-shell): switching Studio to another flow, page or package no longer saves the previous item's unsaved edit into the one just opened (objectui `fc650380d`) + - **patch** — fix(app-shell): the Studio surface, its nav-item inspector and a new canvas entry inherit a label-less entry's label, the way the console does (objectui#11196) (objectui `02a22957c`) + - **patch** — fix(fields,plugin-detail,plugin-grid): every percent face reads its width through the spec's `resolveFieldScale`, so an undeclared percent renders the same everywhere (objectui `741864f7b`) + - **patch** — fix(app-shell): a flow screen select with no placeholder shows the locale's own "Select…" word (objectui#11220) (objectui `f523bd684`) + - **patch** — fix(plugin-list,plugin-grid): a grouped list view under a toolbar search groups on the server, and each group counts all its matching rows (objectui `d0ae5d025`) + - **patch** — fix(app-shell,plugin-designer): standard navigation entries are written with no `label`, never with a copy of their target's text (objectui#11201) (objectui `cb2f6fb5b`) + - **patch** — fix(app-shell,plugin-designer): the designer's nav surfaces name a nav entry with no `label` the way the console draws it (objectui#11196) (objectui `8741cb71a`) + - **patch** — fix(app-shell): Studio's draft autosave saves an edit made while a save is in flight, in every editor that uses it (objectui `395f4fa51`) + - **patch** — fix(components): a `kind:'react'` page no longer writes the host adapter under each block's `dataSource` (objectui `c021b3529`) + - **patch** — fix(components): an `action:menu` trigger stays disabled while its action runs, and a disabled `action:group` disables its buttons (objectui#11182) (objectui `2e3da72ad`) + - **patch** — fix(app-shell): the Studio flow screen preview draws a screen field's `options`, `placeholder` and `defaultValue` as the runtime dialog does (objectui `ed498ac91`) + - **patch** — fix(app-shell): the page designer stops offering the retired `page:header` breadcrumb toggle (objectui#11173) (objectui `52bf34824`) + - **patch** — fix(app-shell): the permission editor's row-level-security policy list draws each policy's label and description (objectui `f8334f877`) + - **patch** — fix(app-shell): Studio's nav autosave sends every nav edit it has shown — "Done" sends the edit, and a completing save clears only what it sent (objectui `0389650f3`) + - **patch** — fix(app-shell): the save warning has a sentence of its own for a formula field the server ignored (objectui `9419df198`) + - **patch** — fix(data-objectstack): the rule-entry filter form refuses an empty or non-string `icontains` comparand (objectui `0b8c63831`) + - **patch** — fix(app-shell): Studio prints an app's own locale-map label in the designer locale instead of `[object Object]` (objectui `39e625de2`) + - **patch** — feat(app-shell): the screen-flow dialog renders a screen field's declared bound, help text and lookup target (objectui `81778b955`) + - **patch** — fix(app-shell): Studio's Interfaces pillar closes nav editing when the package answers read-only, so no edit is taken on screen that its autosave will refuse (objectui `37d166280`) + - **patch** — fix(plugin-grid): a search reaches the grouped grid's header query and every group's row query, so the groups are the searched ones (objectui `7e4fa1bb2`) + - **patch** — Studio's Interfaces pillar no longer loses its nav rail when a nav item's label is a locale map (objectui#11158). (objectui `3ab51503b`) + - **patch** — fix(app-shell): the metadata form routes a condition builder to a member the served derivation marks as an erased string arm (objectui `32b131017`) + - **patch** — The ingestion choke point's diagnostic for a stored `id_field` now carries the reason `@objectstack/spec` publishes for that key (objectui#7650). (objectui `1e215c40b`) + - **patch** — `page:header` no longer draws an empty breadcrumb slot, and an authored `breadcrumb` is ignored (objectui#11166). (objectui `0ffc423b1`) + - **patch** — fix(app-shell): the report and dashboard-widget pickers show a dataset member's label and the dataset's description (objectui `cecd6a031`) + - **patch** — fix(app-shell): a shared `?sel=nav:ID` link survives the designer's mount and opens that nav item, without entering editing on a read-only package (objectui#11153) (objectui `957f69520`) + - **patch** — fix(fields,plugin-form,i18n): the line-items grid's required-cell text and the master-detail form's config hints read the locale packs (objectui `c2a8d23c6`) + - **patch** — fix(plugin-form,fields,i18n): the line-items panel, the grid field and the master-detail heading finish speaking the user's language (objectui `a8c550938`) + - **patch** — fix(app-shell): Studio's nav-item inspector shows a locale-map label and edits only the designer locale's entry (objectui `9a45088c4`) + - **patch** — An unlabelled undoable action's Undo and Redo toast names the object and carries no English verb (objectui#11080). (objectui `a782fa732`) + - **patch** — fix(fields): a multi-value select shows its placeholder while nothing is selected (objectui `3a0e7ab05`) + - **patch** — fix(app-shell): Studio's Interfaces pillar hands its page inspectors, canvas and source editor the package's real read-only flag (objectui `d71d972ae`) + - **patch** — fix(plugin-form): a master-detail child with authored inline columns derives its sort field and amount field, so line order persists and the total shows (objectui#11144) (objectui `263dcd77f`) + - **patch** — The console's global Undo and Redo toasts read the session's language (objectui#11080). (objectui `1d6a23d60`) + - **patch** — The `object-master-detail-form` registration's `fields` description no longer calls that key "the submitted set" (objectui#11114). (objectui `3fa193856`) + - **patch** — A `record:path` block that sets `statusField` and leaves out `stages` now shows the status field's picklist as its stages, instead of the "record:path — no stages configured" plac… (objectui `3f61eef1b`) + - **patch** — fix(app-shell): Studio's app designer canvas shows a locale-map nav label and renames only the current locale's entry (objectui#11128) (objectui `9babfa433`) + - **patch** — fix(app-shell): the flow designer stops warning on an edge guard that reads its source node's own outputs (objectui `f75e1f7e0`) + - **patch** — fix(plugin-form,fields,i18n): the record page's line-items panel and the line-items grid speak the user's language (objectui `385ebc5c6`) + - **patch** — fix(plugin-form): the master-detail form's Subtotal / Tax / Total show the amount's currency, not a hard-coded yen sign (objectui#11132) (objectui `1923d35d2`) + - **patch** — fix(core,app-shell): Undo of a lookup update restores the stored id, not the expanded record (objectui#11122) (objectui `bf43afafa`) + - **patch** — fix(plugin-timeline,types): a gantt-variant timeline draws a date-only end through the end of that day (objectui#11112) (objectui `c27b575ed`) + - **patch** — A hand-authored form field `{ type: 'select', multiple: true }` now renders the multi-value select and submits an array (objectui#11116). The form renderer's built-in `select` bra… (objectui `84b275c01`) + - **patch** — fix(app-shell): Studio's Automations pillar honours a read-only package on its Enabled switch, flow inspector and canvas (objectui `9fd6c2c6f`) + - **patch** — fix(app-shell,i18n): the designer's flow, federated-panel and field-stub chrome speak the user's language (objectui#10862, slice 4) (objectui `78abf3021`) + + ⚠️ 5 of these carry a breaking change: 5 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 1 commit in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared it, so it appears in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ docs: re-fence the plugins and core remainder of objectui#5867 as ts, 17 blocks across 8 pages (batch 6) (#11176) (objectui `340dc718f`) + + + + objectui range: `db11afd4967c...e420df310f5b` + ## 17.5.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index c64eb6b0681..383160943ce 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.5.0", + "version": "17.6.0", "description": "Prebuilt Console SPA pinned to this framework release, installed as a dependency of @objectstack/cli. Source of truth: @object-ui/console (https://github.com/objectstack-ai/objectui).", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/console", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 2ad0aa89d1e..657b793fbc2 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,602 @@ # @objectstack/core +## 17.6.0 + +### Minor Changes + +- 05a7547: fix(core,objectql)!: a `datetime` value names a year from 1000 to 9999 at both engine doors, so one before year 1000 is refused as a written value and as a filter comparand; a `date` keeps 0001 to 9999 + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the engine accepts as a `datetime`. The one range function both doors ask, `isOutsideTemporalYearRange` in `@objectstack/core`, now takes a lower bound per kind: a `datetime` starts at year 1000 (its UTC year), a `date` stays at 0001, and both still end at 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What is refused now.** A `datetime` whose UTC year falls in 0001..0999, which both doors accepted before: + + - **The write door** (the record validator), in every spelling it took: an ISO instant string, a bare `YYYY-MM-DD` (midnight UTC), a zone-naive `YYYY-MM-DD HH:MM`, and a `Date`. It answers `VALIDATION_FAILED` with the field's `invalid_date` code, on `engine.insert`, `engine.update` (one row or many) and the dry-run `engine.validate`, so on `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and each row of `POST /api/v1/data/:object/import` too. A number was already refused there, because a `datetime` is stored as text. + - **The comparand door** (the temporal-comparand door), in every spelling: an ISO string, a `Date` and epoch milliseconds as a number. It answers `INVALID_FILTER` / 400 at `where`, at a per-aggregation `filter` and at `having`, on the engine and on `POST /api/v1/data/:object/query`. The analytics native-SQL strategy asks the same predicate, so it declines such a comparand and the query reaches this door. + - The year is the instant's UTC year: `1000-01-01T00:00:00+08:00` is year 999 in UTC and is refused, and `0999-12-31T23:00:00-02:00` is year 1000 in UTC and is read. + + **What an author sees.** The comparand refusal names the field, the value, its position and the range: "an instant whose UTC year falls outside the years 1000 to 9999, the years a datetime value may name". It then says why the floor sits at 1000, instead of the misorder words that a year past 9999 still gets: MySQL documents its `DATETIME` from year 1000 only, and reads one stored in the years 0001 to 0099 back a century late. A comparand in those years that was already refused for its spelling or its day (a non-ISO spelling, a day that does not exist) is now named by its year first, as one past 9999 already was. The write refusal is the existing `invalid_date` sentence for a `datetime` field. + + **Why.** MySQL documents `DATETIME` from year 1000, and it reads a stored `DATETIME` in 0001..0099 back a century late through its client's instant parser (`0009-03-04 10:00` comes back as `2004-09-03T10:00Z`), which ADR-0053 D-F2 keeps. The range is the contract on every backend, so SQLite, PostgreSQL and the in-memory driver, which held these years, refuse them too. No writer or query of a `datetime` before year 1000 was found. + + **A `datetime` already stored before year 1000.** Nothing rewrites it, and nothing shifts it into the range. It reads back as before, and on MySQL a year in 0001..0099 still presents a century late. To find such rows, filter the field with `$lt` on `1000-01-01T00:00:00.000Z`, the floor's first instant, which both doors admit; the comparison runs on the stored value, so it finds them on MySQL as well. An update that leaves the field out is accepted. A write that carries a year below 1000 is refused, so the field can be written again with an instant from year 1000 on, or with `null`, and the author decides which. + + **Unchanged.** A `date` keeps 0001..9999, padded to four digits as before. A `time` column still reads the time of day of an instant in 0001..0999, and a `time` comparand refused for another reason keeps that reason's words. Every year from 1000 to 9999 on a `datetime`, and every refusal outside 0001..9999 on either kind, answers as before, apart from the range the words name. +- b785c3b: fix: `sum` / `avg` answer the same double on every face the platform owns, added with one compensated fold that `@objectstack/core` now exports as `compensatedSum` (#20544) + + Clause-②: yes + + **New export.** `@objectstack/core` exports `compensatedSum(nums)`: the sum of + `nums`, added in order with Kahan-Babuska-Neumaier compensation, which is the + summation SQLite (3.43 and later) uses for its own `sum` and `avg`. It moved + here from `@objectstack/objectql`'s rows path (`in-memory-aggregation.ts`), + which now imports it instead of keeping a private copy. + + **What changed.** Three folds still added a group's values naively, and now call + the same function: + + - `@objectstack/driver-memory`'s `aggregate()` and `find()` with aggregations, + the path `engine.aggregate` takes on an in-memory datasource; + - `@objectstack/driver-memory`'s analytics face (`MemoryAnalyticsService`), + whose `sum` / `avg` measures are now a `$group` `$accumulator` in place of + mingo's `$sum` / `$avg`; + - `@objectstack/service-analytics`' draft preview. + + Over a `number` column holding `0.1`, `0.2` and `0.3`, each of them answered + `0.6000000000000001` / `0.20000000000000004`. They now answer `0.6` / + `0.19999999999999998`, as SQLite and the engine's rows path do. Over + `1e16, 1, -1e16` they answered `0` and now answer `1`. On driver-memory, + `engine.aggregate` gave two answers depending on its path: `having { s: { $eq: + 0.6 } }` kept the group on the rows path and dropped it on the native path. It + now keeps it on both. + + **What did not move.** Two addends, integers whose running total stays within + 2^53, and a non-finite total give the same answer as before. Which values count + as addends did not change either: booleans as 1 / 0, and nulls and non-numeric + strings left out, as each face already had it. `count`, `min` and `max` are + untouched. The analytics face's pipeline dump (`result.sql`) now renders the + accumulator's functions by name, so a `sum` measure and an `avg` measure still + dump differently. + + **Residual.** PostgreSQL and MySQL add their doubles natively without + compensation, and the platform does not wrap that arithmetic. So over three or + more fractions their native path can still differ from these faces in the last + place. An exact `$eq` on a fractional sum compares doubles; compare with a range. +- 2473e26: fix(core,objectql)!: a temporal filter comparand is refused with `INVALID_FILTER` / 400 exactly when the same value is refused as a written value — a day that does not exist (`"2026-02-30"`) is no longer rolled over or compared as text, and a non-ISO `datetime` spelling (`"07/15/2026 10:00"`) is no longer read in the server's zone (#20549); and a `time` comparand whose instant has no four-digit UTC year (`"+010000-01-01T10:00:00Z"`) is refused rather than compared as text (#20480) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a `date`, a `datetime` and a `time` field accept as a filter comparand. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). + + The record validator already refused the `date` / `datetime` classes as written values (`VALIDATION_FAILED` / `invalid_date`). The comparand door was wider, so a filter admitted what a write refused and answered the wrong rows. The two predicates moved into `@objectstack/core`'s `isUninterpretableTemporalComparand`, and both doors now ask that one rule. A comparand is refused with `INVALID_FILTER` / 400, naming the field, before any driver read, at `where`, a per-aggregation `filter` and `having`: + + - **A day that does not exist**, on a `date` or as the day part of a `datetime`: `"2026-02-30"`, `"2026-02-29"` (2026 is not a leap year), `"2026-04-31"`, `"2026-02-30T10:00:00Z"`. `"2028-02-29"` is a real day and is read. + - **A `datetime` string in any spelling but the ISO 8601 ones the platform writes**, after trimming: `YYYY-MM-DD` (midnight UTC); `YYYY-MM-DDTHH:MM[:SS[.fraction]]` followed by `Z`, a `±HH:MM` or `±HHMM` offset, or nothing (a zone-naive wall clock is UTC, ADR-0074); and `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no zone. Refused now, for example: `"07/15/2026 10:00"`, `"2026/07/15 10:00"`, `"15 July 2026 10:00"`, `"07/08/2026"`, `"Wed, 15 Jul 2026 10:00:00 GMT"`, `"2026-07-15 10:00:00+08:00"` (write it with a `T`), and a bare integer string such as `"2026"` or `"1784109600000"`. + - **An instant on a `time` column in either class above.** A `time` column reads a comparand that is not a bare wall clock as an instant, by the `datetime` rule, and keeps its UTC time of day — so `"07/15/2026 10:00"` was the host zone's time of day, and `"1784109600000"` a string of epoch milliseconds. A wall clock (`"10:00"`, `"10:00:00.5"`), an ISO instant, a `Date` and an epoch-millisecond number are read as before, in a four-digit year (next). + - **An instant on a `time` column whose UTC year has no four-digit spelling**, in every spelling (#20480): `"+010000-01-01T10:00:00Z"`, `"-000001-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` (year 10000 in UTC), and the epoch-millisecond number or `Date` of any of them. A `time` column keeps the UTC time of day of an instant only when that instant spells a four-digit year; any other one reached the driver as written and was compared with the stored `HH:MM:SS` text. No time of day is read from an extended year. Year 0 (`"0000-06-15T10:00:00Z"`) spells four digits, and its time of day is read as before. + + Epoch milliseconds stay a `datetime` comparand as a JSON number: `{ "$gt": 1784109600000 }` is read exactly as before. As a string, a bare integer was read as epoch milliseconds, so `"2026"` meant two seconds after 1970 and matched every later row; send the number, or an ISO instant. + + What a caller sees through `POST /api/v1/data/:object/query`, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`: + + | `where` | memory | SQLite | PostgreSQL | now, on all three | + |:--|:--|:--|:--|:--| + | `datetime` `$eq "2026-02-30T10:00:00Z"` | 200, the row stored at `2026-03-02T10:00:00.000Z` | the same | the same | 400 `INVALID_FILTER` | + | `datetime` `$eq "07/15/2026 10:00"`, `"2026/07/15 10:00"` | 200, the row at `2026-07-15T14:00:00.000Z`, the server process's zone | the same | the same | 400 `INVALID_FILTER` | + | `date` `$eq "2026-02-30"` | 200 `[]`, compared as text | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | + | `datetime` `$gt "2026"` | 200, every row (read as 2026 epoch milliseconds) | the same | the same | 400 `INVALID_FILTER` | + | `time` `$gt "+010000-01-01T10:00:00Z"`, rows `09:00` / `10:30` / `12:00` | 200, 3 of 3 (compared as text) | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | + | `time` `$gt` the number of that instant | 200 `[]` | 200, 3 of 3 | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | + + The refusal names the field and the value, says the filter was not applied, and names the spellings that are read. The rows a non-ISO comparand matched were a property of the deployment host: the same request answered differently on two servers. + + **Who is affected.** A caller that filters a `date` or `datetime` field with a string: a REST or SDK client, a saved report or view filter, a dashboard's analytics query (the raw-SQL strategy declines such a comparand, and the engine refuses it), an MCP `query_records` call written by a model. A `{placeholder}` such as `{30_days_ago}`, the empty string, a JS `Date` and an epoch-millisecond number are unchanged. + + **Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL: + + - a real leap day: `date` `"2028-02-29"`, `datetime` `"2028-02-29T10:00:00Z"`; + - each ISO spelling above, compared as the same instant whatever the host's zone: `"2026-07-15T14:00:00Z"`, `"2026-07-15T22:00:00+08:00"`, `"2026-07-15 14:00"` (UTC, not the host zone); + - a `date` comparand with a leading real `YYYY-MM-DD`, still compared as that day (`"2026-07-15T10:00:00Z"` on a `date` is July 15); + - the same wall clock as a 2026 instant on a `time` column: `$gt "2026-07-15T10:00:00Z"` answers the `10:30` and `12:00` rows, as does its epoch-millisecond number or `Date`; + - the year range 0001..9999, and every written value (the record validator now asks the same rule it copied, and answers exactly as before). +- 889139c: fix(plugin-security): `security/explain` resolves the user it explains in the organization enforcement resolves them in, so a member whose membership in the caller's organization has ended is no longer explained holding that organization's grants (#20580) + + When an administrator explains another user, the explanation is computed in the administrator's own organization. Enforcement does one more thing for that same user first: under a walled tenancy posture (`isolated` or `group`), it drops an organization claim that no current membership backs, and the user resolves with no active organization, so only their global grants apply. The explainer skipped that check. For a user whose membership in the administrator's organization had ended, the explanation listed that organization's grants, and the verdicts they decide, while enforcement applied none of them. + + The explainer now asks the same check before it resolves the user, and resolves them where it says. `@objectstack/core` exports that check as `vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds, tenancyPosture)`. It returns the claimed organization while a current membership backs it or while no wall is enforced, and `undefined` once the claim is dropped. `resolveAuthzContext` asks the same function for a session's claim, so the two cannot disagree. This is a new export with no behaviour change to `resolveAuthzContext`. + + Unchanged: + + - Enforcement admits and refuses exactly what it did before. + - A current member's explanation. + - The `single` posture, where no claim is dropped on either side. + - Explaining yourself, and a caller with no active organization. +- a6866da: fix(core): a date or time in the years 0001..0099 is read as written, not as 1900..1999, wherever a UTC instant is built from year / month / day / time parts + + `Date.UTC(year, …)` and `new Date(year, …)` read a year from 0 to 99 as 1900 + year. Core built its instants from parts that way, so every day of the years 0001..0099 (inside the supported range 0001..9999) landed in the 1900s at the sites below, with no error. + + - `@objectstack/core`: **new export** `wallClockToUtcMs(parts)`, the epoch milliseconds of a `WallClockParts` read as UTC. It is `Date.UTC` without the two-digit-year remap: `month` is 1-12, omitted time components are 0, and every component rolls over past its end as `Date.UTC` rolls it (`month: 13` is next January, `day: 0` the previous month's last day, `hour: 24` the next midnight). A `NaN` component gives `NaN`. Every site below now builds through it: + - `zonedWallClockToUtcMs` and `zonedDateStartToUtcMs`, the wall clock and the zone-offset read. The offset read also takes the zone's era, so a wall clock early on 0001-01-01 in a zone west of UTC, whose offset probe lands in year 0, reads right. + - `bucketKeyToCalendarRange` (`0050` spans 0050-01-01..0051-01-01, not 1950..1951; `0050-01-01` as a `day` key is no longer `null`) and `bucketDateKey`'s ISO week (0050-01-01 is in week 52 of 0049, not of 1949). + - The date macros: `{1976_years_ago}` resolves to `0050-09-30`, not `1950-09-30`. A macro that lands in 0001..0999 is now spelled with a four-digit year, as the `date` storage form spells it (`0055-06-15`, not `55-06-15`, which names no day). + - `@objectstack/service-analytics`: the preview evaluator's `week` key and the `compareTo` bucket alignment build their days through `wallClockToUtcMs`. + - `@objectstack/trigger-schedule`: a time-relative window's day bounds build through `wallClockToUtcMs`. + + What an author sees: `POST /api/v1/data/:object/import` stores the `datetime` cell `0050-01-01 10:00` as `0050-01-01T10:00:00.000Z`, and in `Asia/Shanghai` as `0050-01-01T01:54:17.000Z` (the zone's local mean time for that year). Before, it stored `1950-01-01T10:00:00.000Z` and `1950-01-01T02:00:00.000Z` and reported the row `ok`. Measured through the import route and read back through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16; the `2026-07-15 10:00` control is stored the same before and after. Every year from 0100 on builds exactly as before. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- 89801cd: **A flow `http` node's `signingSecret` now signs the request on every arm, with one scheme, and a secret that does not resolve refuses the node instead of letting the request leave unsigned.** + + `signingSecret` is declared as "HMAC-SHA256 secret → X-Objectstack-Signature", with no arm named. Only the durable arm honoured it, because only the messaging outbox signed. The default inline request, and a `durable: true` node on a host with no messaging HTTP outbox (which degrades to that inline request), were sent without the header while the run reported success. + + - `@objectstack/core`: **new exports** `signHttpBody(body, secret)` and `HTTP_SIGNATURE_HEADER`, the outbound HTTP signature scheme: `X-Objectstack-Signature: sha256=`, where a request with no body is signed over the empty string. They were `@objectstack/service-messaging`'s own, and they moved here so a sender with no outbox can sign with the same code. + - `@objectstack/service-messaging`: `signHttpBody` and `HTTP_SIGNATURE_HEADER` are still exported under the same names. They are now re-exports of the `@objectstack/core` bindings, not a second implementation. Delivery rows and the headers the outbox sends are unchanged. + - `@objectstack/service-automation`: the `http` node's inline request carries `X-Objectstack-Signature` whenever `signingSecret` is set. It is computed over the exact body the node sends (its JSON serialization of `config.body`, or the empty string when there is none), so a receiver that verifies with `signHttpBody` over the bytes it received accepts it on every arm. + - A non-empty `signingSecret` that renders to nothing at run time now fails the node with a guard refusal naming `config.signingSecret`, and nothing is sent. This covers a `{token}` with no value in the run, or one that renders the empty string. The refusal is on every arm, including the outbox arm, which used to enqueue such a delivery unsigned. A fault edge does not route it. The fix is to give the run the value the template reads. + - An authored `signingSecret: ''` still sends unsigned on purpose, on every arm. + + Clause-②: yes (widening) — two new exports on `@objectstack/core`'s root. Nothing is removed or renamed on any package. The one newly refused case is a node whose authored secret did not resolve, which the published contract already said signs. +- bbcd20c: An import row now says which of its fields the write dropped, on the dry run and on the commit. A column mapped to a `formula` field, a static `readonly` field or a runtime-owned field is legally stripped by the engine: the row still succeeds, and the create door already reported the strip as `droppedFields`. The import row answered a bare `ok` / `created` on both halves, so a file whose formula column was ignored read exactly like a file that wrote it. `runImport` now copies the engine's own per-row report onto each `ok` row as `ImportRowResult.droppedFields`: from the `validateData` verdict on the dry run, from the row's `insertManyData` outcome, and from the `createData` / `updateData` response of a single-row write. The synchronous route, the async job's results and the job's dry run all carry it; no REST change was needed. + + Clause-②: yes (widening) + + - **Verbatim, in the engine's vocabulary.** The events are the engine's `DroppedFieldsEvent`s, one per reason (`computed`, `readonly`, `readonly_when`, `primary_key`). The import reads no reason and keeps no list of non-writable types, so a reason the engine adds later reaches the row unchanged. A reader that branches on `reason` must stay exhaustive. + - **Where the key is absent although something may have been dropped.** A create batched through `createManyData` (a protocol without `insertManyData`) is reported only as a batch-level union that names no row, so those rows carry no key. And a row the import would UPDATE is previewed in `update` mode, which runs no `readonlyWhen` or primary-key strip, so its dry run can name fewer fields than its commit. The `ImportRowResultSchema.droppedFields` describe now says both. + - **Unchanged:** `ok`, `action`, the counters, the failed rows and the async job's results cap. A clean row, a failed row and a skipped row carry no `droppedFields`. + + `ImportProtocolLike.insertManyData`'s declared outcome now names the optional `droppedFields` it already answered with. +- dcd3309: fix(core,objectql)!: a relative-date placeholder that resolves outside its field's years is refused `INVALID_FILTER` / 400, naming the placeholder and the year it resolved to, instead of reaching the driver and answering the wrong rows + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the engine answers for a filter carrying a relative-date placeholder. A date macro is resolved after the temporal-comparand door, which steps around a placeholder, so the year range that door asks of a literal never saw the value one resolved to. It does now, through the same function, core's `isOutsideTemporalYearRange`, by the column's kind: a `date` takes the years 0001 to 9999 and a `datetime` 1000 to 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What is refused now.** A date macro whose resolved value falls outside its column's years, on a declared `date` or `datetime` field (or, on a `time` field, one that resolves to an instant whose UTC year has no four-digit spelling), at `where` (on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`), at a per-aggregation `filter`, at `having` (by the aggregated column's kind), and through `judgeFilter`. On REST that is `POST /api/v1/data/:object/query` and every other door that reads through the engine. Measured before this on InMemoryDriver and SqlDriver on SQLite, over a `datetime` field with a row in 2026 and a row in 1500: + + - `$gt {8000_years_from_now}` answered both rows, and the right answer was none; + - `$lt {2027_years_ago}` answered the 1500 row, because the resolver spelled year -1 as `-1-10-01` and that text was read as a day in 2001, and the right answer was none; + - `$lt {1977_years_ago}` resolved to year 49, below the `datetime` floor of 1000, which now applies to a resolved placeholder as it does to a literal; + - on a `time` field, `$gt {8000_years_from_now}` answered every row: the `time` rule keeps no time of day from an instant whose UTC year has no four-digit spelling, so it compared as text. Such a placeholder is refused now in the words a literal of that instant gets. + + **What an author sees.** The refusal names the field, the placeholder as written, its position, the value it resolved to and that value's year, in the temporal-comparand door's words for the year class: `filter on 'opened_at' compares a declared datetime field against "{8000_years_from_now}" at where.opened_at.$gt, a relative-date placeholder that resolved to "+010026-10-01" (the year 10026), an instant whose UTC year falls outside the years 1000 to 9999 …`. It ends by asking for a placeholder whose offset lands inside those years. + + **The resolver's spelling** (`@objectstack/core`). A date macro that lands on a day outside 0001..9999 now resolves to that day in the expanded-year form of ECMAScript's date time string format, `+010026-10-01` or `-000001-10-01` (year 0 is `0000-10-01`). It used to take the storage rule's unpadded spelling, `10026-10-01` or `-1-10-01`, which `Date.parse` reads through the host's legacy parser in the host's zone, so a day in year -1 read as one in 2001 and could not be judged. Every consumer of `resolveFilterToken` and `resolveFilterTokens` sees the new spelling for such a day only. A day inside 0001..9999 and a sub-day placeholder's instant are spelled as before. + + **Unchanged.** A placeholder that resolves inside its column's years answers as before; a `date` keeps the years 0001 to 0999, which a `datetime` refuses, and a `time` field reads the time of day of any instant with a four-digit year, year 0 included. A placeholder on a column with no temporal kind (text, number) and a context placeholder such as `{current_user_id}` are not judged by this range. Every literal comparand answers as before. +- f6ccca4: fix(objectql,rest): a `date` or `datetime` value refused for its year says so — "must be a date in the years 0001 to 9999" / "must be a datetime whose UTC year falls in the years 1000 to 9999" — instead of "must be a valid date (ISO-8601)", which was false for a value such as `0500-07-15T10:00:00Z` (#20846) + + Clause-②: yes (widening) — one new export on `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`. No value's verdict moves and no wire key moves: the field code stays `invalid_date` and its `constraint` stays `{ type }`. + + `POST` / `PATCH /api/v1/data/:object` and each row of `POST /api/v1/data/:object/import` + refuse a `date` outside the years 0001 to 9999 and a `datetime` whose UTC year falls + outside 1000 to 9999. When the value itself is readable — an ISO 8601 string such as + `0500-07-15T10:00:00Z` or `+010000-01-01`, or a `Date` — the refusal's message now + names the kind's years. An author who read "not valid ISO" rewrote the spelling, and no + spelling of that year is admitted. + + - `@objectstack/spec`: the validation message catalog gains `invalid_date_range` and + `invalid_datetime_range` in `en`, `zh-CN`, `ja-JP` and `es-ES`. They are two more + sentences of the `invalid_date` code, never a wire value. The years are the template + parameters `{{firstYear}}` / `{{lastYear}}`. A deployment that overrides a message + under `validation.field.invalid_date` or `validation.field.invalid_datetime` does not + cover these values. To override their text, define + `validation.field.invalid_date_range` / `validation.field.invalid_datetime_range`. + - `@objectstack/core`: `SUPPORTED_TEMPORAL_YEARS` (`{ date: { first: 1, last: 9999 }, + datetime: { first: 1000, last: 9999 } }`, frozen) is the range + `isOutsideTemporalYearRange` judges by. It is exported so a refusal names the range + from the source the doors use, never a copy of its numbers. + - `@objectstack/objectql` and `@objectstack/rest`: the record validator and the import's + cell reader choose the range sentence for such a value. An import cell with more than + four year digits (`+010000-01-01`) is refused by the import's reader. It used to read + "is not a valid date" and now gets the same range sentence as the write door. + + **What is not affected.** Which values are refused is unchanged, and so is the refusal's + code (`invalid_date`) and `constraint`. A value that is not readable keeps its sentence: + "must be a valid date (ISO-8601)" at the write door, `"…" is not a valid date` at the import. + So does a number, which is never a written `date` or `datetime`. +- d1633f3: fix: the analytics native-SQL path answers a measure its response declares `number` as a number on every dialect, presented by the one rule `driver-sql`'s `aggregate()` applies, which `@objectstack/core` now exports as `AGGREGATE_ANSWER_KIND` and `presentAsNumber` (#20889) + + Clause-②: yes (widening) + + **New exports.** `@objectstack/core` exports two names, moved here unchanged + from `@objectstack/driver-sql`, which now imports them instead of keeping them + private: + + - `AGGREGATE_ANSWER_KIND`: what each declared aggregate function answers. + `count`, `count_distinct`, `sum` and `avg` answer `'number'`; `min` and `max` + answer `'column'`, a value of the aggregated column. + - `presentAsNumber(value)`: the `'number'` presentation. A string `Number()` + reads as a number becomes that number. Any other value is returned as given: + a number, `null`, a boolean, empty or blank text, or text that reads as NaN. + + **What changed.** On PostgreSQL, `POST /api/v1/analytics/query` and + `POST /api/v1/analytics/dataset/query` answered through `NativeSQLStrategy` + returned count, count_distinct, sum, avg, and min / max over a numeric column + as strings, such as `count: "2"` and + `sum: "500.000000000000000000000000000000"`, while `fields[]` declared + `number`. A dataset's `row_count` did the same, and a measure-scoped count + mixed `"1"` with the number `0` in one column. SQLite answered numbers. The + strategy now presents each measure column by its declared aggregate function, + through the same table and presenter as `SqlDriver.aggregate()`. `min` / `max` + are presented only when their column is declared numeric, so `max` over a text + column, every dimension, and expression measures keep the value the database + returned. + + **Precision.** The answer is one JS number, the policy `driver-sql`'s + `aggregate()` already applies. A total that needs more digits than a double + holds, such as `9007199254740993`, answers the nearest double + (`9007199254740992`), which is also what SQLite and the engine path answer. + + **What did not move.** `@objectstack/driver-sql`'s behaviour is unchanged: its + `aggregate()` reads the same table, and its read presenter calls the same + function. The answers on SQLite are byte-identical. The arithmetic of the + analytics native statement did not change either. On PostgreSQL its `sum` and + `avg` still add exact decimals, so `0.1 + 0.2` answers `0.3` where the engine + path answers `0.30000000000000004`. +- 8368f1c: feat(core): the bulk-import runner, its row coercion, the mapping apply and the field-meta map now live in `@objectstack/core`, beside `bulkWrite` (#20919) + + `runImport` (with `sanitizeRowError` and its option/result types), the cell + coercion (`coerceRow`, `coerceFieldValue`, `parseDateCell`, `parseNumberCell`, + `parseBooleanCell`, `matchOption`, `splitMulti`, `isBlank`), the `mapping` + artifact pipeline (`applyMappingToRows`, `refuseUnknownMappingTargets`, + `MappingArtifactLike`, `MappingFailure`, `ApplyMappingOptions`) and the field + metadata map (`buildFieldMetaMap`, `ExportFieldMeta`) are exported from + `@objectstack/core`. They moved here unchanged from `@objectstack/rest` so the + connector sync executor in `@objectstack/service-automation` writes through the + same runner as the HTTP import door without depending on the HTTP layer. Nothing + to change for consumers: `@objectstack/rest` re-exports every name it exported + before. +- 58a77db: fix(service-analytics)!: the analytics read scope and the native `where` answer `$contains` / `$notContains` on a multi-valued or JSON-stored field by membership, with the one construct `driver-sql` emits, now exported from `@objectstack/core` (#20987) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics doors answer for one class of read. A row policy (the read scope the analytics plugin compiles from the security service, or a host's own `getReadScope`) whose `$contains` or `$notContains` names a field declared multi-valued (`multiple: true` on a multi-capable type, or a multi-option type) or JSON-stored now selects the rows holding the comparand as an ELEMENT of the stored list. It used to select every row whose stored JSON text contained the comparand as a substring, so on SQLite a policy could admit rows outside it, and on PostgreSQL every query under such a policy answered `500` (MySQL was not measured). An analytics count under such a policy now equals what the same caller reads through the data door. On a datasource whose SQL dialect the analytics host cannot name, such a policy now refuses the query (`READ_SCOPE_COMPILE_FAILED` / `500`) instead of falling back to the substring reading. It ships as `minor` under the launch-window convention. + + **The `where`.** `POST /api/v1/analytics/query`, the dataset door and `/analytics/sql` on the native strategy render the same membership test for a `$contains` / `$notContains` in a query's `where` (or a dataset's `runtimeFilter`) on such a field: on PostgreSQL the query answers rows where it answered `500`, and on SQLite the count stops over-counting (`$contains`) and under-counting (`$notContains`). On a datasource whose dialect the host cannot name, the operator on such a field is refused `INVALID_FILTER` / `400`. The ObjectQL strategy already answered membership and is unchanged. + + **Unchanged.** On a scalar text field `$contains` stays the substring test, on every face. `$notContains` keeps its NULL rule: a row with no value satisfies it. A host that wires no field metadata keeps the substring reading, because it cannot tell a JSON column from a text one; the analytics plugin wires it from the data engine. + + **New export.** `@objectstack/core` exports `jsonMembershipPredicate(dialect, emitters, value)` and `jsonMembershipCandidates(value)`, with the `JsonMembershipDialect` and `JsonMembershipEmitters` types: the per-dialect membership construct (#17590) moved from `@objectstack/driver-sql`, where it was module-private, and made placeholder-agnostic. `@objectstack/driver-sql` imports it and emits byte-identical statements and bindings. + + **What to do after upgrading.** Nothing, unless a policy or a dashboard filter relied on the substring reading of a multi-valued or JSON-stored field: such a filter now selects members only, as the data door always did. A host whose analytics `sqlDialect` hook answers nothing for a SQL datasource should answer `'sqlite'`, `'postgres'` or `'mysql'`, or the operator on such a field is refused. +- a11faee: fix(objectql)!: a per-aggregation `filter` refuses `$in` / `$nin` / `$eq` / `$ne` / an ordering / `$between` / implicit equality on a declared JSON-stored field with `INVALID_FILTER` / 400, in the words `where` refuses them in, instead of counting rows the stored arrays cannot support + + Clause-②: yes (widening) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts in one position, `aggregations[i].filter`, on every driver and for every caller that reaches the engine: the REST query door (`POST /api/v1/data/:object/query`), a flow or hook, and the analytics strategy that lowers a dataset measure's filter onto `engine.aggregate`. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a per-aggregation `filter` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, and before any driver is asked for a row, so an empty table refuses it too. That is the set `driver-sql`'s `where` refuses on such a column, for the same reason. + + **What an author sees now.** The same 400 body the same filter gets as a `where`: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, as they are for `where`, and the full diagnostic, naming both and the aggregation position, goes to the server log. + + **Why a refusal.** The engine evaluates a per-aggregation filter itself, and it compared the whole stored array against a scalar. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $in: ['u1', 'u9'] } }` counted 0, `{ owners: { $nin: ['u1', 'u9'] } }` counted all 6, the two rows it was asked to exclude among them, `$gt` / `$lte` / `$between` counted 4 / 1 / 5, and `{ tags: { $eq: 'red' } }` counted the row holding `['red']` by JS loose equality. The same filters in `where` were 400 on both dialects. + + **Who is affected.** A dashboard, report, dataset measure or caller whose per-aggregation filter compares a JSON-stored field with one of those operators and read the count as a real answer. Also a host calling `applyInMemoryAggregation` directly with a `fields` map: it now judges each `aggregations[i].filter` against that map before any row (an empty `rows` array included) and throws the same `INVALID_FILTER` / 400. It takes an optional fifth argument, `reportWithheld(diagnostic)`, which receives the withheld field, operator and position; without it the diagnostic is dropped. A call without `fields` judges nothing, as before. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. + + **Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored; `having`; `where`; and a host whose engine has no declaration for the object, where nothing is judged. + + **`@objectstack/core`** (three new root exports): `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, `jsonColumnOperatorRefusalText(field, op, bare)` and its return type `JsonColumnOperatorRefusalText` (`{ message, diagnostic }`). They are the operator set and the two texts (the withheld message and the full diagnostic) of the JSON-column refusal, so `driver-sql`'s `where` and the engine's per-aggregation filter refuse with one set and one sentence. + + **`@objectstack/driver-sql`**: no behaviour change. Its JSON-column gate reads the set and the text from `@objectstack/core`; every refusal it prints is byte for byte what it printed before. +- 2c1cef3: fix(core)!: a filter that aims `$startsWith`, `$endsWith`, `$icontains`, `$like` or `$ilike` at a field stored as a JSON column is refused with `INVALID_FILTER` / 400, as `$eq` / `$in` / `$nin` already are, instead of matching the field's serialized text or failing at query time + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows which filters are answered on a field stored as a JSON column, on every face that reads `@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`: `driver-sql`'s `where` (and `driver-sqlite-wasm` and `driver-turso`'s local transport, which inherit it) on every read and write face that lowers a filter, and the engine's per-aggregation `filter`. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What is refused.** On a field declared multi-valued (an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`) or structured-JSON (`json`, `address`, …), a filter using `$startsWith`, `$endsWith`, `$icontains`, or the staged pattern pair `$like` / `$ilike`, is refused with `INVALID_FILTER` / 400, at any depth under `$and` / `$or` / `$not`. The per-aggregation `filter` refuses the three declared ones; it already refused `$like` / `$ilike` as operators it does not evaluate. Through the engine, a structured-JSON field was already refused all seven text operators by the text-operator declared-type door, which still answers first there, in its own words; what moves for it is a direct driver call. + + **What an author sees.** The body the equality family already gets there, byte for byte: the filter WAS NOT APPLIED, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message and named in the server-log diagnostic; a filter positively marked as the caller's own reads them named. + + **Why a refusal.** Such a column stores the serialization `["u1","u2"]`, and none of these five operators has a membership reading. Measured through `POST /api/v1/data/:object/query` on a multi-value lookup and a `tags` field: on SQLite `$startsWith: "["` and `$endsWith: "]"` matched every row with a value, `$startsWith: "u1"` matched none of the rows holding `u1`, and `$icontains: "U1"` also matched the row holding only `u10`; on PostgreSQL 16 every one failed at query time with a `500` `DATABASE_ERROR`, a `json` column having no `LIKE` operator; the per-aggregation `filter` counted 0 for each. No membership reading is invented for a prefix, suffix or case-folded test. + + **Who is affected.** A saved filter, list view, dashboard widget, report or caller that aims one of these operators at a multi-valued or JSON-stored field. On SQLite it read rows that matched the stored brackets and quotes; it now gets the 400. On PostgreSQL it already failed, with a 500. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. + + **`@objectstack/objectql`: `$search` over a multi-valued field answers by membership.** The search expander (`$search` on `find`, `findOne` and `aggregate`, the REST `search` / `$search` parameter included) used to emit `$in` for a term matching a `select` option label and `$icontains` for any other term, against every field in the resolved search set. On a multi-valued field both are refused by the gate above, so one such field in the set failed the whole search: a label term answered 400 on every dialect, and any other term answered 500 on PostgreSQL and, with this change, 400 on SQLite. The auto-default set includes a `select` declared `multiple: true`, as in `examples/app-todo`'s `todo_task.tags`, and `searchableFields` may name a `tags` field or a multi-valued lookup. Such a field is now matched by membership: a term matching option labels becomes one `$contains` per matched option value, and any other term, or any term on a field with no options, becomes `$contains` of the term. No search answers 400 or 500 for it any more. **The visible cost:** to hit a multi-valued field, a term must now equal one of its members or match one of its option labels; SQLite used to match substrings of the stored array's serialized text as well, so a term like `wood` found a row tagged `redwood`, and it no longer does. Scalar fields are searched exactly as before. + + **Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored, the scalar text column included; `driver-memory`; and `driver-turso`'s remote transport, which compiles its own filters. +- 097ef80: fix: the analytics native-SQL path aggregates with the engine's own aggregate policies, so one query answers one number whichever strategy serves it: `sum` / `avg` accumulate in double, a PostgreSQL boolean aggregand is cast, and an all-NULL `sum` answers `0`. The operand policies move from `@objectstack/driver-sql` to `@objectstack/core` (#21042) + + Clause-②: yes (widening) + + **New exports.** `@objectstack/core` exports the aggregate operand policies, moved here from `@objectstack/driver-sql`, where they were module-private. The driver now imports them and emits byte-identical statements. + + - `AGGREGATE_ACCUMULATION`: what each declared aggregate function accumulates in on PostgreSQL and MySQL. `avg` accumulates in double; `sum` accumulates in double over a fractional column; the counts, `min` and `max` take the column as stored. + - `aggregandColumnClass(shape)`: the one column-class predicate those policies read, over a column's declared `{ type, multiple }`. It answers `'fractional'`, `'integral'`, `'boolean'`, or `undefined` for every other column, a multi-valued one included. The type `AggregandColumnClass` names the three classes. + - `POSTGRES_BOOLEAN_AGGREGAND_CAST`: the functions whose boolean aggregand is cast to `int` on PostgreSQL. These are `sum`, `avg`, `min` and `max`; the two counts are never cast. + - `doubleAccumulationOperand(operand, dialect)`: the column's text, parsed as a double, spelled for `'postgres'` or `'mysql'`. + - `aggregandOperandSql(func, columnClass, dialect, operand)`: the operand an aggregate wraps, with the cast inside the double operand. The type `AggregandSqlDialect` names its dialects (`'sqlite'`, `'postgres'`, `'mysql'`, `'unknown'`). + + **What changed.** `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query` served by `NativeSQLStrategy` (the default on a SQL driver) skipped three policies `SqlDriver.aggregate()` applies. So the ObjectQL strategy and `engine.aggregate` answered differently for the same query. Measured on SQLite and PostgreSQL 16.13: + + - On PostgreSQL, `sum` / `avg` over an exact-decimal column, and `avg` over an integer one, added exact decimals. For example, `0.1 + 0.2` answered `0.3` and `11 / 9` answered `1.222222222222222`, where the engine answers `0.30000000000000004` and `1.2222222222222223`. The native statement now accumulates in double, as the driver does. + - On PostgreSQL, `sum` / `avg` / `min` / `max` over a boolean field answered `500` (`function sum(boolean) does not exist`). The native statement now casts the boolean aggregand to `int`, as the driver does, and answers the numbers the engine answers. + - On every dialect, a group whose aggregand is NULL in every row, and a measure-scoped `sum` that admits no row, answered `sum` `null` at the cube door. The strategy now folds a `null` answer to `emptyGroupValueFor` (`@objectstack/spec`) for every measure, so that `sum` answers `0`. `avg`, `min` and `max` over nothing stay `null`. The dataset door already answered `0`. + + This is no narrowing: each answer moves to the value the platform already declared for the same query. + + **What did not move.** `@objectstack/driver-sql`'s statements and answers are unchanged: a move-proof test compiles each aggregate function over each column class on SQLite, PostgreSQL and MySQL, and the statements equal the ones captured before the move. SQLite's native statement is unchanged, because neither operand policy applies there. A host that relays no field declarations to the analytics service, or names no SQL dialect, gets today's native arithmetic. +- 1bd14c9: fix(core)!: a date macro whose offset lands past every instant a JavaScript `Date` can hold is refused `INVALID_FILTER` / 400, naming the placeholder, instead of resolving to the text `Invalid Date` or throwing an uncoded `RangeError` + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `resolveFilterToken` and `resolveFilterTokens` answer for one class of inputs, and so what every door that resolves filter placeholders answers. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What is refused now.** A relative-date placeholder whose offset lands past the instants a JavaScript `Date` can hold names no day and no time. The resolver used to answer a day-or-coarser one (`{300000_years_ago}`) with the text `Invalid Date`, which compares as text, and to throw an uncoded `RangeError: Invalid time value` for a sub-day one (`{99999999999999999999_minutes_ago}`). Measured before this through `engine.find` on InMemoryDriver and `POST /api/v1/data/:object/query` on SqlDriver over SQLite, over a `datetime` field with a row in 2026 and a row in 1500: + + - `$lt {300000_years_ago}` answered both rows, and `judgeFilter` answered `{ ok: true }`; + - `$lt {99999999999999999999_minutes_ago}` threw the uncoded `RangeError` from `engine.find` and `judgeFilter`, and the REST door answered `500 INTERNAL_ERROR`. + + Both are refused now with `INVALID_FILTER` / 400 at every position the engine resolves (`where` on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`, a per-aggregation `filter`, `having`) and through `judgeFilter`, before any driver read. Every other caller of `resolveFilterToken` / `resolveFilterTokens` receives the same coded error in place of the text or the `RangeError`. The refusal is the same on every column, because the resolver does not know the column: such a placeholder names no value at all. + + **What an author sees.** `Relative-date placeholder "{300000_years_ago}" names no instant: its offset lands past every instant a JavaScript Date can hold, so it resolves to no day and no time, and it is refused rather than compared. A date value names a year from 0001 to 9999, and a datetime value a year from 1000 to 9999: use a relative-date placeholder whose offset lands inside those years.` The thrown error carries `code: 'INVALID_FILTER'`, `status: 400` and `token` (the placeholder's name), the code the engine already answers for a placeholder that resolves outside its column's years. + + **Unchanged.** A placeholder that names an instant resolves as before, including one past the years 0001..9999 that a `Date` still holds (`{273847_years_ago}` resolves to `-271821-09-30`); the engine's per-column year range judges that one, as before. Context placeholders and an unknown placeholder answer as before. +- d2bc644: fix(plugin-security): a row-level `check` judges a lone scalar written to a declared multi-valued field as the one-member list it is stored as, so the write and the read the same policy scopes give one answer for one row (#21238) + + Clause-②: yes (widening) + + The write door stores a lone scalar sent to a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `lookup` / `user` / `file` / `image` flagged `multiple: true`) as a one-member list: `tags: 'x'` is stored as `["x"]`. The row-level write `check` judged the value as sent on the insert and on a by-id update, because both images are formed before the write door runs. Measured through `ObjectQL.insert` with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set, with the same predicate as `using` and `check`: + + | `check` | written | write, before | stored | read | + |---|---|---|---|---| + | `record.tags.contains('x')` | `'x'` | 403 | `["x"]` | shown | + | `!record.tags.contains('x')` | `'x'` | admitted | `["x"]` | hidden | + | `record.tags.contains('x')`, a by-id update | `'x'` | 403 | `["x"]` | shown | + + Now the image's value on every field the object declares multi-valued goes through the same rule the write door stores it by, before the check is judged. The first and third rows are admitted. The second is refused: a policy that forbids a member from tagging a row `x` can no longer be passed by sending `'x'` instead of `['x']`. A lone scalar now gets exactly the verdict its stored list gets, on the insert, a by-id update and a predicate update. That includes a policy that compares such a field with a scalar comparison (`==`, `!=`, `in`, an ordering), which the read refuses with `INVALID_FILTER` / 400: there `'x'` used to get the opposite of the verdict `['x']` got, and now gets the same one. + + Unchanged: a field the object does not declare multi-valued is judged as written; a list, `null`, a blank string and an object are judged as written, as the write door leaves them; the check's comparands are left as written, since `contains` takes one member; and refusals keep their code and status (`PERMISSION_DENIED` / 403). + + **`@objectstack/core`** (one new root export, so `minor`; this export is the widening the `Clause-②: yes (widening)` line declares): `multiValueStorageForm(value)`, the rule itself. It wraps a string, a number or a boolean into a one-member list and returns every other value as the same value. `@objectstack/objectql`'s `normalizeMultiValueFields` now calls it, with no change in what the write door stores (`patch`). `@objectstack/plugin-security` is `minor` because the set of writes its check admits widens (the first and third rows above); that is a security-floor behaviour change, not the declared widening. + +### Patch Changes + +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- cd901d7: fix(plugin-security): `security/explain` answers enforcement's refusal at the object level too, and explains another user in the organization they are resolved in (#20604) + + Clause-②: no + + Two answers of `POST /api/v1/security/explain` disagreed with what the same principal's own request gets from enforcement. + + **A row-level policy that compares two fields of no shared comparison class** (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object). The SQL driver refuses to compile such a read, so the find answers `INVALID_FILTER` / 400. A by-id update or delete fails closed at its row-level gate, and an insert whose check judges the policy is refused with `INVALID_FILTER` / 400. An object-level explanation (no `recordId`) still answered `allowed: true`, the `rls` layer `narrows`, and the predicate as `readFilter`, for every operation. A `recordId` that no row carries was answered `visible: false` with no deciding layer. Both are now refused with the envelope a record-grained explanation already gives: `INVALID_FILTER` / 400, with the message that names the policy and both fields. A request that the capability gate or the CRUD grant denies is still explained as denied there. + + **Another user explained by an administrator.** The explanation now carries the organization the user is resolved in, as enforcement's context for that user does. Before, a current member of the administrator's organization was explained with no organization. Under `isolated`, that member was reported denied on a tenant object their own find reads. Under every posture, a permission set that their organization authored (a `sys_permission_set` row scoped to that organization) was missing from the explanation and from the verdicts it decides. + + `@objectstack/core`: the API-key arm of `resolveAuthzContext` asks `vetOrganizationClaim` for its membership rule, as the session arm does. This is a refactor with no behaviour change. A key whose owner is no longer a member of its organization is still refused. + + Unchanged: + + - Enforcement admits and refuses exactly what it did before. + - A comparison between two fields of one class keeps its verdicts, at the object level and per record. + - Explaining yourself. + - A removed member's explanation (no organization, as enforcement resolves them). +- 856321f: A date-bucket key spells its year with four digits at every granularity, as the SQL drivers' bucket expressions do, so the in-memory and pushed-down paths key a day in 0001..0999 alike and a drill-down from such a key finds its range. + + A `date` value names a year from 0001 to 9999, so these keys are reachable through a `date` field and through a stored `datetime` row. For 0050-06-15, `strftime('%Y-%m')` on SQLite and `to_char(…, 'YYYY-MM')` on PostgreSQL answer `0050-06`, while `bucketDateKey` answered `50-06`: the same `groupBy` keyed the same rows differently depending on which path ran it. + + - **`@objectstack/core` `bucketDateKey`** pads the year to four digits: `0050`, `0050-Q2`, `0050-06`, `0050-06-15`, and the ISO week key `0050-W24` (early January 0050 is `0049-W52`, its ISO week-year). The engine's in-memory `groupBy` and the memory cube face delegate to it, so both now answer the drivers' key. A year from 1000 to 9999 is spelled as before. + - **`@objectstack/core` `bucketKeyToCalendarRange`** reads exactly what `bucketDateKey` writes. Its week arm checked a key against the unpadded label, so a padded key such as `0050-W01` answered `null`; it now answers `{ start: '0050-01-03', end: '0050-01-10' }`. An unpadded key (`50-06`, `49-W52`) is not a bucket key and still answers `null`. + - **`@objectstack/service-analytics`** mints the `compareTo` alignment key through `bucketDateKey` instead of spelling it locally, so a comparison row in 0001..0999 merges onto its bucket (`0050-06`) instead of being appended under `50-06`. +- 6b004c0: `isUninterpretableTemporalComparand` reads a bare wall clock on a `time` column by the spec's `ClockTimeValueSchema` (`@objectstack/spec/data`), not by a private copy of it (#20771) + + Clause-②: no + + The wall-clock half of core's `time` rule (`HH:MM[:SS[.fraction]]`, hours 00 to 23, minutes and seconds 00 to 59, no time zone) was spelled twice: once as the spec's `ClockTimeValueSchema`, the stored form of a `time` value, and once as a private regex in `@objectstack/core`. The two admitted the same strings, but nothing tied them together, so an edit to either one changed one side only. Core now asks the spec schema. Every caller of `isUninterpretableTemporalComparand('time', …)` therefore answers from the rule the spec's `time` default gate uses: the engine's temporal-comparand door, the analytics comparand check, the record validator's `time` arm and the import's `time` coercion. + + Unchanged: + + - Every string gets the verdict it got before. Measured over 8,655,360 generated strings: 8,640 read by both the old regex and the schema, the rest refused by both, 0 answered differently. + - An instant, a number or a `Date` on a `time` column is judged as before. +- 682873d: fix(core): the refusal a filter gets for a scalar comparison or text operator on a multi-value or JSON field reads true on every backend that prints it, and reaches a REST caller whole + + Clause-②: no + + The `INVALID_FILTER` / 400 refusal `driver-sql`'s `where`, the engine's per-aggregation `filter` and `driver-memory` all print (`jsonColumnOperatorRefusalText`) explained itself with `driver-sql`'s storage ("a field this driver stores as a JSON TEXT column") and the two wrong answers SQL used to give. That is untrue on the engine and on `driver-memory`. The message was also 748 characters, and the REST envelope cuts a 4xx message at 499 plus an ellipsis, so callers on SQLite and PostgreSQL read `…Refused rather than compiled because the answ…` and never reached the sentence saying the field and the operator were withheld. + + The message now reads, on every backend, in 486 characters: `A constraint in this filter WAS NOT APPLIED: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member.`, then the same `$contains` / `$or` of `$contains` remedy, then `For no value, use "$null" or "$empty".` (a `null` comparand such as `{ f: null }`, `$eq: null` or `$ne: null` is refused too, and `$contains` could not express it), then `The field and the operator are withheld from the message; the full diagnostic is in the server log.` The diagnostic (the server-log text, and what a filter's own author is shown) gives the same reason with the operator named, names the field, and spells the remedy with the field's name. It drops the storage and the SQL history too, and is now whole on the wire for field names up to 26 characters (it was 643 characters or more and always cut). + + Code, status, the refused operator set and the `$contains` remedy are unchanged. A client that matched on the old words `JSON TEXT column` or `Refused rather than compiled` should match on `code: "INVALID_FILTER"` instead. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 193c9b24147..21f3587c79a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Microkernel Core for ObjectStack", "type": "module", diff --git a/packages/create-objectstack/CHANGELOG.md b/packages/create-objectstack/CHANGELOG.md index bd6d6bee072..92afe4de0e0 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,24 @@ # create-objectstack +## 17.6.0 + +### Patch Changes + +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. + ## 17.5.0 ### Minor Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 354de067751..a1075986947 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "17.5.0", + "version": "17.6.0", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index f9a3252ba60..de89a962eea 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,400 @@ # @objectstack/driver-memory +## 17.6.0 + +### Minor Changes + +- f1e921a: feat(spec)!: `$empty` joins `FILTER_OPERATORS`, and the view operators `is_empty` / `is_not_empty` lower to it (#20446) + + A stored 「is empty」 / 「is not empty」 — `['field', 'is_empty', …]`, `isempty`, `is_not_empty`, `isnotempty`, in a view rule, a sharing rule or any filter array — now lowers to `{ field: { $empty: true | false } }` instead of `$null`. `$empty` is answered by the field's DECLARED type: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. So an 「is empty」 rule on a text field now also finds `''`, and on a multi-value field also finds `[]`, which the `$null` lowering missed. `is_not_empty` is its exact complement. `$empty` is in `FILTER_OPERATORS` (and `ALL_OPERATORS`) now, and `canonicalAstOperator` folds the empty pair onto `is_empty` / `is_not_empty` rather than onto `is_null` / `is_not_null`. On `@objectstack/driver-memory`, a QueryAST comparison node (`{ type: 'comparison', operator: 'is_empty' }`) is answered by the same declared-type arm. + + **BREAKING**: two things accepted before are refused now, each loudly and with its fix. + + - **A `{ $empty: … }` object written as a field value** (a `where` pasted into an insert or update payload) is refused with `VALIDATION_FAILED` (`invalid_type`, "$empty is a filter operator, not a value"). Before, a text-like field stored it as data. + FROM `update('task', { title: { $empty: true } })` → TO write the value itself (`{ title: '' }`, `{ title: null }`); a filter belongs in `where`. + - **`is_empty` / `is_not_empty` where no face holds the column's declared type** is refused with `INVALID_FILTER` / 400 (`READ_SCOPE_COMPILE_FAILED` / 500 on an analytics read scope). The `$null` lowering answered these. The compositions: + - the built-in `id`, which no object declares. FROM `['id', 'is_empty', true]` → TO `['id', 'is_null', true]` / `is_not_null`; + - a federated (external) object on a driver that does not implement `registerExternalObject` (driver-memory, driver-mongodb). The boot already reports such an object as NOT bound to its remote table, naming it, and its reads answered from a table named after the object. FROM `is_empty` on such an object → TO bind it on a driver that implements federation (driver-sql and its heirs, driver-turso); + - an `AnalyticsService` constructed without `sourceFieldMeta`. FROM such a host → TO pass `sourceFieldMeta` (the package README shows it), or filter with `is_null` / `is_not_null`; + - a multi-value column on a SQL dialect `driver-sql` does not model (a knex client other than SQLite, PostgreSQL or MySQL). FROM `['tags', 'is_empty', true]` there → TO `['tags', 'is_null', true]` / `is_not_null`. + + Stored sharing rules and views that use 「is empty」 are not rewritten; they are re-read under the new meaning. Production rules that use 「is empty」 on a text or multi-value field were not measured; each finds more rows (the `''` / `[]` ones) from this release. + + Clause-②: yes (narrowing) + + +- 793fb83: `MemoryAnalyticsService` (the in-memory analytics cube face) now runs the two shared filter comparand doors every other analytics face runs, then the shared filter lowering of ADR-0053 D-D1 (as amended), before it compiles a query's `where`. It also compiles `$or` and `$null`, the two parts of the lowering's output it could not. + + Clause-②: yes (narrowing) + + + + **BREAKING**: `query()` and `generateSql()` now refuse, with `INVALID_FILTER` / 400, filter comparands they used to answer. Each is refused the same way by every other analytics face and by the query engine, so a filter that worked here worked nowhere else. Measured on a fixture where `d` is `'v1'`, `'v2'`, `null` and absent: + + - `undefined` in any comparand position — `{ d: undefined }` and `{ d: { $eq: undefined } }` answered the no-value rows, `{ d: { $ne: undefined } }` the valued ones, `{ d: { $in: ['v1', undefined] } }` row `v1`; + - a `null` member of `$in` / `$nin` (`{ d: { $in: ['v1', null] } }` answered `v1` and both no-value rows), and a `null` under an ordering operator (`{ d: { $gt: null } }` answered no row); + - a scalar where `$in` / `$nin` takes a list (`{ d: { $in: 'v1' } }`); + - a plain object, a `Map` or a binary value as a comparand — `{ d: { $ne: { a: 1 } } }` and `{ d: new Map() }` answered EVERY row. + + The fix is to write the comparand you mean: `null` or `{ $null: true }` for "has no value", a list for `$in` / `$nin` (and `{ $null: true }` in a `$or` for "one of these, or no value"), a scalar for an ordering operator. + + Corrected answers, each now what the live query path (`find()`) returns: + + - a bigint comparand within 2^53 (`{ n: { $gt: 2n } }`) is read as its number and answered; beyond 2^53 it is refused `INVALID_FILTER` / 400. Both used to fail with an uncoded error. + - `$between` is answered as its two bounds, with a bare-day maximum widened to the whole day before it is converted to the field's storage form; it was refused. + - `$null` (true: no value; false: has a value) and `$or` (a `{}` branch is TRUE, `$or: []` is FALSE) are compiled on both exits; they were refused. `$not`, `$startsWith`, `$endsWith` and `$empty` stay refused. `ANALYTICS_FILTER_CAPABILITIES` names `$null` and the `$or` combinator accordingly. + + The `generateSql()` echo and the `query()` pipeline dump for `$ne`, `$nin` and `$notContains` now show the lowering's NULL escape around this face's own guard — `(d IS NULL OR (d IS NULL OR d != 'v1'))` — the same rows as before. +- 95fed33: `MemoryAnalyticsService` lowers the `FilterArray` spelling of `where` instead of dropping it + + Clause-②: no (narrowing) + + + + An array `where` such as `[['stage', '=', 'won']]` used to skip every filter step of the + analytics (cube) face: `query()` aggregated every row and `generateSql()` echoed no `WHERE`, + while the object spelling `{ stage: 'won' }` answered its rows. The array is now lowered by + `@objectstack/spec`'s `isFilterAST` / `parseFilterAST` — the lowering the analytics `where` door + and the engine already apply — so both spellings answer the same rows and echo the same `WHERE` + on both exits. `[]` still means no filter. + + **BREAKING**: `query()` and `generateSql()` now refuse, with `INVALID_FILTER` / 400, a `where` + array that is not a filter — one `isFilterAST` rejects. Each such array used to answer EVERY row + and echo no `WHERE`; the analytics `where` door and the query engine refuse the same shapes. + Measured on a fixture where `d` is `'v1'`, `'v2'`, `null` and absent, each of these answered all + four rows: + + - an infix join, `[['d', '=', 'v1'], 'or', ['d', '=', 'v2']]`; + - an operator outside the filter-array vocabulary, `[['d', 'sounds_like', 'v1']]`; + - a list of scalars, `[1, 2, 3]`; + - a cube-style entry list, `[{ member: 'd', operator: 'equals', values: ['v1'] }]`. + + An array that does lower but carries a comparand or operator the face refuses in its object + spelling (`[['d', 'in', 'v1']]`, `[['d', 'starts_with', 'v']]`) is now refused as that object + spelling is; it too used to answer every row. + + The fix is to write the filter you mean: the prefix form `['or', condA, condB]` for an infix + join, an operator from the filter-array vocabulary, or the `FilterCondition` object. +- f8178ff: fix(driver-memory): `$contains` / `$notContains` on a multi-valued or JSON-stored field answer by membership, as the SQL drivers do + + Clause-②: yes (widening) — one new public method on the exported `InMemoryDriver` class, `filterContainsTest`; its return type `MemoryContainsTest` is not re-exported from the package entry. No accepted filter key or operator is added: `$contains` and `$notContains` keep their declared shape. + + On a field whose declaration makes it JSON-stored (`multiple: true` on a `lookup`, `user`, `select`, `radio`, `file` or `image` field, a `multiselect`, `checkboxes` or `tags` field, or a structured type such as `json`), the in-memory driver now answers `{ field: { $contains: v } }` by whole-element membership: some element of the stored array equals `v`. It used to match each element by substring, so `u1` matched a row storing `['u10']` and `'red'` matched a row storing `['redwood']`. A number member answered nothing: `{ nums: { $contains: '1' } }` missed `[1, 2]`. `$notContains` is the exact complement, and a row with no value still satisfies it. A scalar text column keeps the case-exact substring test. + + `driver-sql` gives the same answer on SQLite, PostgreSQL and MySQL; the two drivers were measured over the same fixture. The answer holds on every face of this driver: + + - `find()` and `count()`, in both filter spellings; + - the nested-relation filter on a multi-valued relation, which the engine lowers to one `$contains` per related id; + - `MemoryAnalyticsService`'s query, and its SQL echo, which now renders SQLite's `json_each` membership construct for such a column. + + The comparand is still a string. A number or boolean member is named by its text: `'1'` matches the stored number `1` (and `'1.50'` the number `1.5`), `'true'` matches the boolean `true`, and `'null'` matches a `null` member. A field the driver holds no declaration for, such as a field on an object never passed through `syncSchema`, keeps the substring reading. + + New: `InMemoryDriver.filterContainsTest(object, field, value)` returns the one test every face above lowers `$contains` to. It is a narrow seam for the analytics face, beside `filterSubstringPattern` and `filterComparandStorageForm`. The added public method is why this entry is `minor`. + + **If your tests relied on the old answer:** on the in-memory driver, a filter that matched an id by prefix or a tag by substring now returns only the member rows. That is what SQL already returned in production. Write `$contains` with the whole member value. +- a3dc817: fix(driver-memory, driver-mongodb)!: a non-boolean `$exists` comparand is refused with `INVALID_FILTER` / 400, as `$null`'s is, instead of selecting the rows with no value (#20897) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the in-memory driver and the MongoDB driver accept in a filter. A `$exists` comparand that is not a boolean (a string, a number, `null`, `undefined`, an object) is now refused with `INVALID_FILTER` / 400, where these two drivers used to answer it. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + `FieldOperatorsSchema` declares `$exists` as a boolean, and `driver-sql`, `driver-sqlite-wasm` and both Turso transports already refused any other comparand. The in-memory driver and the MongoDB driver did not: they read `$exists` as `value === true`, so every other value asked for the rows with NO value. `{ stage: { $exists: "yes" } }` and `{ stage: { $exists: 1 } }` returned the rows without a stage, the opposite of what was written. `0`, `null` and the string `"false"` landed on that same side by the same default, not because anything read them. The in-memory driver's analytics face read the same flag by truthiness and answered the valued rows for the same filter, so that driver gave two different answers. + + **What an author sees now.** `400 INVALID_FILTER` with `driver-sql`'s message, beginning `Operator "$exists" on field "FIELD" requires a boolean comparand (true or false).` and naming the position (`filter.stage.$exists`). On the in-memory driver the refusal covers `find`, `findOne`, `count`, `aggregate`, `updateMany`, `deleteMany` and the analytics face (`query()` and `generateSql()`). There, an `undefined` or object comparand is refused first by that face's comparand-type check, also `INVALID_FILTER` / 400, in its own words. A refused write changes nothing. + + **What to write instead.** Write the boolean itself. `"$exists": true` matches rows whose field has a value, and `"$exists": false` matches rows whose field has none. + + **Who is affected.** A caller that sent a non-boolean `$exists` to `InMemoryDriver` or `MongoDBDriver` (a test suite, a local or embedded deployment, a flow or hook calling the engine in-process) and read the answer as a real one. On `SqlDriver` the same filter was already a 400. + + **Unchanged.** `$exists: true` and `$exists: false` answer exactly as before. The aggregation `filter` and `having` positions, which the engine evaluates itself after the driver, are not changed by this entry. +- 45ce12a: fix(driver-memory)!: on a declared JSON-stored field, the query path and the analytics face refuse `$eq` / `$ne` / an ordering / `$between` / `$in` / `$nin` / implicit equality with `INVALID_FILTER` / 400, in the words the SQL family refuses them in, instead of answering each per element + + Clause-②: yes (narrowing) + + + + **BREAKING** (`@objectstack/driver-memory`): this narrows what the driver's filter doors accept, for every caller that reaches them: `find`, `findOne`, `count`, `updateMany`, `deleteMany` and `aggregate` with a `where`, through the engine or called directly, and `MemoryAnalyticsService`'s `query` and `generateSql`. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a `where` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, before any row is read. On the analytics face a `where` key is a cube member, judged by the field it resolves to. That is the set `driver-sql` refuses on such a column, for the same reason. + + **What an author sees now.** The body `driver-sql` answers for the same filter: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, and the full diagnostic, naming both and the position in the filter, is written to the driver's (or the analytics service's) logger at `warn`. + + **Why a refusal.** This driver answered each of those operators per element, through mingo's array semantics. Measured through `engine.find` over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $eq: 'u1' } }` and `{ owners: { $in: ['u1', 'u9'] } }` returned those two rows, `$nin` the other four, and `{ owners: { $gt: 'u1' } }` four rows by comparing each member as text, where every SQL dialect answers the same filters 400. An application whose tests run on this driver passed on a filter its production backend refuses. + + **Who is affected.** A test suite, demo or dev setup on this driver that filters a JSON-stored field with one of those operators and read the per-element rows as the answer. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. + + **Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not declared JSON-stored; and an object this driver holds no declaration for (one never passed through `syncSchema`), where nothing is judged and every operator answers as before. `InMemoryDriver` gains one method, `filterFieldDeclarations`, tagged `@internal`: it exists so the analytics face judges its `where` by the same declarations, and it is not a consumer contract. + +### Patch Changes + +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- b785c3b: fix: `sum` / `avg` answer the same double on every face the platform owns, added with one compensated fold that `@objectstack/core` now exports as `compensatedSum` (#20544) + + Clause-②: yes + + **New export.** `@objectstack/core` exports `compensatedSum(nums)`: the sum of + `nums`, added in order with Kahan-Babuska-Neumaier compensation, which is the + summation SQLite (3.43 and later) uses for its own `sum` and `avg`. It moved + here from `@objectstack/objectql`'s rows path (`in-memory-aggregation.ts`), + which now imports it instead of keeping a private copy. + + **What changed.** Three folds still added a group's values naively, and now call + the same function: + + - `@objectstack/driver-memory`'s `aggregate()` and `find()` with aggregations, + the path `engine.aggregate` takes on an in-memory datasource; + - `@objectstack/driver-memory`'s analytics face (`MemoryAnalyticsService`), + whose `sum` / `avg` measures are now a `$group` `$accumulator` in place of + mingo's `$sum` / `$avg`; + - `@objectstack/service-analytics`' draft preview. + + Over a `number` column holding `0.1`, `0.2` and `0.3`, each of them answered + `0.6000000000000001` / `0.20000000000000004`. They now answer `0.6` / + `0.19999999999999998`, as SQLite and the engine's rows path do. Over + `1e16, 1, -1e16` they answered `0` and now answer `1`. On driver-memory, + `engine.aggregate` gave two answers depending on its path: `having { s: { $eq: + 0.6 } }` kept the group on the rows path and dropped it on the native path. It + now keeps it on both. + + **What did not move.** Two addends, integers whose running total stays within + 2^53, and a non-finite total give the same answer as before. Which values count + as addends did not change either: booleans as 1 / 0, and nulls and non-numeric + strings left out, as each face already had it. `count`, `min` and `max` are + untouched. The analytics face's pipeline dump (`result.sql`) now renders the + accumulator's functions by name, so a `sum` measure and an `avg` measure still + dump differently. + + **Residual.** PostgreSQL and MySQL add their doubles natively without + compensation, and the platform does not wrap that arithmetic. So over three or + more fractions their native path can still differ from these faces in the last + place. An exact `$eq` on a fractional sum compares doubles; compare with a range. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- d3f88fa: fix(driver-memory): a cube `where` `$lte` on a bare day keeps the whole day on a declared `datetime` field (#20661) + + Clause-②: no + + `MemoryAnalyticsService` put a `$lte` comparand into the field's storage form before it applied the whole-day rule for a bare-day upper bound. On a field declared `datetime` (through `syncSchema`) the storage form of `'2026-07-28'` is the instant `'2026-07-28T00:00:00.000Z'`, and the whole-day rule does not widen an instant. So `where: { created_at: { $lte: '2026-07-28' } }` compiled an inclusive bound at that midnight and dropped every row later in the named day, while `find()` with the same filter kept them. `generateSql()` echoed the same narrowed bound. + + Both exits now follow ADR-0053's order: the bare day is widened first, and only the resulting bound is converted to the storage form. On a declared `datetime` field the example compiles `created_at < '2026-07-29T00:00:00.000Z'` and answers the same rows as `find()`. On `9999-12-31`, the last supported day, a declared `datetime` field now asks only for a value (`IS NOT NULL` in the echo), as an undeclared field already did. + + Unchanged: an undeclared field, a declared `date` field, a full timestamp or `Date` comparand (inclusive, as written), and a `timeDimensions[].dateRange` end, which already widened the day before building its bounds. `$between` stays refused on this face (`INVALID_FILTER`, 400). Nothing is removed or renamed, and there is nothing to migrate. +- 8fec76a: refactor(driver-memory): the cube face's own whole-day bound and the in-memory reference matcher are deleted; no answer a caller gets moves (#5930 step 4, #20822) + + Clause-②: no + + - **`MemoryAnalyticsService` (the cube face).** Its `where` door has run the shared `lowerFilterCondition` (`@objectstack/spec/data`) since #5930 step 3, on every column. A bare-day `$lte` therefore reaches the `lte` row already lowered: as `$lt` the next day, or as `$null: false` on `9999-12-31`. The row's own copy of that rule is deleted, and the `lte` row now compiles the comparison it is handed on both exits. The rows `query()` returns and the SQL `generateSql()` echoes are unchanged. An explicit `dateRange` end still widens a bare day through its own window arm (ADR-0053 D-D1 item 8). + - **The reference matcher (`memory-matcher.ts`, `match()`) is retired** (ruling D6 on #5930). No production code called it and the package never exported it: the published `dist` exports are the same 33 names before and after. `InMemoryDriver` keeps `getValueByPath`, the one helper it imported from that module. The matcher's tests now assert the live query path (`InMemoryDriver.find`), the shared filter shape gate, or the spec predicate the matcher evaluated. +- 8460592: fix: the whole-day bound on a bare `YYYY-MM-DD` upper bound is applied at the seams only — `DatabaseLoader.queryHistory` in driver mode becomes one, the engine seam lowers type-blind for an object with no field map, and `InMemoryDriver` drops its own copy (ADR-0053 D-D1 items 5 and 7, #20822) + + Clause-②: no + + - **`@objectstack/metadata` — `DatabaseLoader.queryHistory` in driver mode lowers its own filter.** With a raw `IDataDriver` (`MetadataManager.setDatabaseDriver`) the history filter reaches the driver without passing any seam. The loader now runs the shared `lowerFilterCondition` (`@objectstack/spec/data`) on it, typed by the history object it syncs: `until: 'YYYY-MM-DD'` reads `recorded_at < next day`, so every version recorded on that day is kept on every driver, and an instant `until` is kept as written. Engine mode is unchanged (the engine's own `where` seam lowers it). Before this, the whole day was kept only by each driver's own copy of the rule; with `@objectstack/driver-memory`'s copy deleted below, `until` = today would have gone from every version of the day to none. + - **`@objectstack/objectql` — an object with no field map is lowered type-blind.** The engine's `where` seam (on `find`, `findOne`, `count`, `update`, `delete` and `aggregate`'s `where` / `aggregations[i].filter`) reads the object's declared field map and rewrites a declared `datetime` column only. For an object the registry does not hold there is no declaration to read, and the seam now applies the whole-day rules to every column (a bare-day `$lte` becomes `$lt` the next day, a `$between` splits), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the declared type. It used to leave such an object to each driver's own copy. Visible on `SqlDriver`: a bare-day `$lte` on a non-`datetime` column of an unregistered object that holds ISO instant text now keeps the whole day; a `datetime` or `date` column answers as before. An object with a field map is unchanged. + - **`@objectstack/driver-memory` — `InMemoryDriver` compiles the comparison it is handed.** Its four copies of the whole-day rule are deleted (the `$lte` and `$between` arms of the filter translator, the `<=` and `between` arms of the AST-node translator). A read through the engine hands it a `where` the engine's seam has already lowered, so on that path a declared `datetime` column keeps the whole named day, and a declared `date` column answers as before. A row-level security `using` filter is not lowered by the engine's seam: the security middleware ANDs it into the query's `where` after that seam has run, and only the RLS compile seam lowers it, rewriting just the columns its field guard declares `datetime`. Two answers converge on what `SqlDriver` already returns (ADR-0053 D-D1 item 7's scope): on a registered object, a bare-day `$lte` / `$between` on a declared `text` column holding ISO instant text, or on a column the object does not declare, is now compared as written, where this driver used to widen it to the whole day. One path narrows outside those two: an RLS `using` policy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, is compiled with no field guard, so the RLS compile seam reads no column as `datetime` and the bound reaches this driver as written, where this driver used to widen it to the whole day; that holds until #20822 group 2 makes the RLS compile seam type-blind when it has no guard. A direct `find()` that passed no seam gets the comparison it wrote (item 5); lower the filter with `lowerFilterCondition` first to keep the whole-day reading. +- 682873d: fix(core): the refusal a filter gets for a scalar comparison or text operator on a multi-value or JSON field reads true on every backend that prints it, and reaches a REST caller whole + + Clause-②: no + + The `INVALID_FILTER` / 400 refusal `driver-sql`'s `where`, the engine's per-aggregation `filter` and `driver-memory` all print (`jsonColumnOperatorRefusalText`) explained itself with `driver-sql`'s storage ("a field this driver stores as a JSON TEXT column") and the two wrong answers SQL used to give. That is untrue on the engine and on `driver-memory`. The message was also 748 characters, and the REST envelope cuts a 4xx message at 499 plus an ellipsis, so callers on SQLite and PostgreSQL read `…Refused rather than compiled because the answ…` and never reached the sentence saying the field and the operator were withheld. + + The message now reads, on every backend, in 486 characters: `A constraint in this filter WAS NOT APPLIED: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member.`, then the same `$contains` / `$or` of `$contains` remedy, then `For no value, use "$null" or "$empty".` (a `null` comparand such as `{ f: null }`, `$eq: null` or `$ne: null` is refused too, and `$contains` could not express it), then `The field and the operator are withheld from the message; the full diagnostic is in the server log.` The diagnostic (the server-log text, and what a filter's own author is shown) gives the same reason with the operator named, names the field, and spells the remedy with the field's name. It drops the storage and the SQL history too, and is now whole on the wire for field names up to 26 characters (it was 643 characters or more and always cut). + + Code, status, the refused operator set and the `$contains` remedy are unchanged. A client that matched on the old words `JSON TEXT column` or `Refused rather than compiled` should match on `code: "INVALID_FILTER"` instead. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index 3026acc98a5..615bd2ce7f4 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index e7c37cccad4..6f9e6cc0628 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,258 @@ # @objectstack/driver-mongodb +## 17.6.0 + +### Minor Changes + +- a3dc817: fix(driver-memory, driver-mongodb)!: a non-boolean `$exists` comparand is refused with `INVALID_FILTER` / 400, as `$null`'s is, instead of selecting the rows with no value (#20897) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the in-memory driver and the MongoDB driver accept in a filter. A `$exists` comparand that is not a boolean (a string, a number, `null`, `undefined`, an object) is now refused with `INVALID_FILTER` / 400, where these two drivers used to answer it. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + `FieldOperatorsSchema` declares `$exists` as a boolean, and `driver-sql`, `driver-sqlite-wasm` and both Turso transports already refused any other comparand. The in-memory driver and the MongoDB driver did not: they read `$exists` as `value === true`, so every other value asked for the rows with NO value. `{ stage: { $exists: "yes" } }` and `{ stage: { $exists: 1 } }` returned the rows without a stage, the opposite of what was written. `0`, `null` and the string `"false"` landed on that same side by the same default, not because anything read them. The in-memory driver's analytics face read the same flag by truthiness and answered the valued rows for the same filter, so that driver gave two different answers. + + **What an author sees now.** `400 INVALID_FILTER` with `driver-sql`'s message, beginning `Operator "$exists" on field "FIELD" requires a boolean comparand (true or false).` and naming the position (`filter.stage.$exists`). On the in-memory driver the refusal covers `find`, `findOne`, `count`, `aggregate`, `updateMany`, `deleteMany` and the analytics face (`query()` and `generateSql()`). There, an `undefined` or object comparand is refused first by that face's comparand-type check, also `INVALID_FILTER` / 400, in its own words. A refused write changes nothing. + + **What to write instead.** Write the boolean itself. `"$exists": true` matches rows whose field has a value, and `"$exists": false` matches rows whose field has none. + + **Who is affected.** A caller that sent a non-boolean `$exists` to `InMemoryDriver` or `MongoDBDriver` (a test suite, a local or embedded deployment, a flow or hook calling the engine in-process) and read the answer as a real one. On `SqlDriver` the same filter was already a 400. + + **Unchanged.** `$exists: true` and `$exists: false` answer exactly as before. The aggregation `filter` and `having` positions, which the engine evaluates itself after the driver, are not changed by this entry. + +### Patch Changes + +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- 53ed3d1: fix(driver-mongodb): `MongoDBDriver` compiles the whole-day comparison it is handed — its own copy of the bare-day upper bound is deleted (ADR-0053 D-D1 items 5, 7 and 9, #20822) + + Clause-②: no + + - **What is deleted.** `translateFilter` no longer widens a bare `YYYY-MM-DD` `$lte` to `$lt` the next day, no longer widens a `$between` maximum the same way, and no longer turns `$lte '9999-12-31'` into `$ne: null` or drops a `$between` maximum on that day. Every verb that translates a `where` inherits it: `find`, `findOne`, `count`, `updateMany`, `deleteMany`, `explain`, and the `$match` stage of `aggregate` / `buildAggregationPipeline`. + - **A read through the engine or the RLS compile seam is unchanged.** The seam hands the driver a filter the shared `lowerFilterCondition` (`@objectstack/spec/data`) has already rewritten on the declared `datetime` columns: `$lt` the next day, a split `$between`, `$null: false` on the last supported day. The deleted copy gave the same answer on that input. A `date` column answers as before, because its stored calendar-day text orders the same either way. + - **Two answers converge on what `SqlDriver` returns** (ADR-0053 D-D1 item 7's scope). On a registered object, a bare-day `$lte` or `$between` maximum is now compared as written on a column that is not declared `datetime`: a `text` column holding ISO instant text, or a column the object does not declare. This driver used to widen it to the whole day. + - **A caller that passes no seam gets the comparison it wrote** (item 5): a `MongoDBDriver` verb or `translateFilter` called directly. A bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, and `$lte '9999-12-31'` compares against that midnight. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. + - No exported name changes. +- e18fea6: fix(objectql,driver-mongodb,formula): the `having` and per-aggregation evaluator compiles the whole-day comparison it is handed, and `$contains` asks membership on a JSON-stored field in `MongoDBDriver` and in `matchesFilterCondition` (ADR-0053 D-D1 items 5 and 9; the `FILTER_OPERATORS` `$contains` contract, #20822) + + Clause-②: no + + - **`@objectstack/objectql`: the aggregate evaluator's own whole-day copy is deleted.** The walker behind `having` and `aggregations[i].filter` no longer widens a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, on a `datetime` column to the whole day, and no longer drops the bound on `9999-12-31`. Through `engine.aggregate` nothing changes: the engine's seam lowers both positions with the shared `lowerFilterCondition` (`@objectstack/spec/data`) before the walker runs, by the object's declared `datetime` fields for the per-aggregation `filter` and by the aggregated column's type for `having`. A caller that passes no seam gets the comparison it wrote: `applyInMemoryAggregation(rows, ast, tz, fields)` called directly now counts `{ at: { $lte: '2026-02-01' } }` against that day's midnight. To keep the seam's reading on a direct call, lower each `filter` first with `lowerFilterCondition(filter, { isDatetimeColumn })`. + - **`@objectstack/driver-mongodb`: `$contains` / `$notContains` ask membership on a declared JSON-stored field.** On a field `syncSchema` recorded as `multiple: true`, a multi-option type (`tags`, `multiselect`, `checkboxes`) or a JSON type, `translateFilter` (every verb, and the aggregation `$match`) now emits an array-only `$elemMatch` over the members the comparand names, with the candidate rule the SQL dialects bind (`jsonMembershipCandidates`, `@objectstack/core`): `'1'` names the string `'1'` or the number `1`, `'true'` the string or `true`. It used to emit a `$regex`, which MongoDB applies to each element, so `{ owners: { $contains: 'u1' } }` matched a stored `['u10']` and `{ tags: { $contains: 'red' } }` a stored `['redwood']`. `$notContains` is the exact complement, and still admits a row with no value. A scalar column, and a field whose declaration the driver does not hold (an object never synced, a standalone `translateFilter` call), keep the substring `$regex`. + - **`@objectstack/formula`: `matchesFilterCondition` asks membership of a JSON-stored column.** When the caller supplies `options.fields` and it names the column, the declaration decides: membership on a JSON-stored column, substring on any other. Otherwise the stored value decides: an array asks membership, anything else substring. A stored array used to fail `$contains` and pass `$notContains` whatever it held. + - **The RLS write check, which evaluates a policy with this function, moves with it.** Under a `check` such as `record.tags.contains('x')` on a multi-valued field, a write whose post-image holds `['x']` (a row the same policy's read shows) is now admitted; it was refused `PERMISSION_DENIED` / 403. `['xy']` stays refused, and the read hides it. + - A scalar written to a declared multi-valued field is judged as written, before the write door wraps it in a list. So `tags: 'xy'`, which the check used to admit while the read hides the stored `['xy']`, is now refused 403. And `tags: 'x'` is now refused 403 too, although the read shows the stored `['x']`. Send the list, `tags: ['x']`. + - **`@objectstack/spec`: docblock only, in the shipped `src/data/filter.zod.ts`.** The three pointers to the deleted `SqlDriver.calendarDayUpperBoundRewrite` / `calendarDayBetweenRewrite` now name the shared `lowerFilterCondition` at the seams, and the `FILTER_OPERATORS` `$contains` implementation-status list gains `driver-mongodb` and `formula`. No schema, type or export changes. + - No exported name changes. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index 2135c0b484f..4fa7753a75a 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "main": "dist/index.js", diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index f07d032e08e..cc3472cd8d6 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,541 @@ # @objectstack/driver-sql +## 17.6.0 + +### Minor Changes + +- ceee88f: fix(driver-sql, driver-turso, plugin-security, spec)!: `SqlDriver` and `TursoDriver` compile the filter they are handed — their copies of the whole-day bound and of the NULL-safe `$not` rewrite are deleted, and the RLS compile seam lowers type-blind when it cannot read the declared types (ADR-0053 D-D1 items 5, 7 and 9, #20822) + + Clause-②: no (narrowing) + + + + **BREAKING**: `SqlDriver` in `@objectstack/driver-sql` loses three `protected` methods: `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite`. They were the driver's copy of the whole-day bound, which the shared lowering now applies once, at the seams, before a driver sees the filter. A subclass of `SqlDriver` that calls one of them, or overrides one with the `override` modifier, no longer compiles (TS2339, TS4113). That includes a subclass of `SqliteWasmDriver` or `TursoDriver`, which extend `SqlDriver`. A subclass that re-declares one without `override` still compiles, but the driver never calls it, so the rule it carried stops applying. It ships as `minor` under the launch-window convention. The class's public methods are unchanged. + + FROM → TO: a `SqlDriver` subclass that called `this.calendarDayUpperBoundRewrite(table, field, op, value)`, `this.calendarDayBetweenRewrite(table, field, value)` or `this.calendarDayExclusiveUpperBound(table, field, value)`, or overrode one of them, lowers the filter with `lowerFilterCondition` from `@objectstack/spec/data` instead, before the driver compiles it: `lowerFilterCondition(where, { isDatetimeColumn })`, where `isDatetimeColumn` answers which columns get the whole-day bound. + + **Supersedes two sentences of this release's shared-lowering entry** (`lowerFilterCondition`, #5930), which this change makes false: + + - "A guard without that set treats no column as `datetime`." Now: when the RLS compile seam has no field guard, or one without a `datetime` set, it cannot read which columns are `datetime`, so it applies the whole-day rule to every column (the `@objectstack/plugin-security` entry below). + - "Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input." Now: `SqlDriver`, `SqliteWasmDriver` and `TursoDriver` keep no copy of the whole-day bound or of the NULL-safe `$not` rewrite. A read through the engine or the RLS compile seam gets the lowered answer, and a call on the driver itself gets the comparison it wrote (the `@objectstack/driver-sql` and `@objectstack/driver-turso` entries below). + + - **`@objectstack/plugin-security` — an RLS policy compiled with no field guard is lowered type-blind.** The RLS compile seam runs the shared `lowerFilterCondition` on every compiled `using` and `check` filter. When the security plugin could not resolve the object's declared fields (no field guard), or a caller of `RLSCompiler.compileFilter` passes a guard without a `datetime` set, the seam cannot read which columns are `datetime`, and it now applies the whole-day rule to every column (a bare-day upper bound becomes `$lt` the next day), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the type. It used to read no column as `datetime`, which left the bound to each driver's own copy of the rule. Visible on a `using` policy such as `record.signed_on <= '2026-01-05'` on such an object: every row of that day is kept on every driver, including `InMemoryDriver`, which had compared it as written since its own copy was deleted. A guard with a `datetime` set is unchanged, and so are the NULL-polarity guards. + - **`@objectstack/driver-sql` — `SqlDriver` keeps no copy of the rules the seams apply.** Deleted: the whole-day rewrite of a bare-day `$lte` and of a `$between` maximum on a `datetime` column, on the plain and the legacy-normalised column paths, including the last supported day (the protected methods `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite` are removed from the class); and the NULL-safe rewrite of a `$not` operand (`nullSafeNegationOperand` and its polarity tables, module-private). A read through the engine or the RLS compile seam is unchanged: the seam hands the driver a filter the shared lowering has already rewritten, and the deleted copies gave the same answer on that input. A caller that passes no seam — `find`, `findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` or `findWithWindowFunctions` called on the driver itself — now gets the comparison it wrote: a bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, `$lte '9999-12-31'` compares against that midnight, and a `$not` is SQL's three-valued negation, so a row whose compared column is NULL is not returned by it. `$ne`, `$nin` and `$notContains` keep their NULL-safe form, which this emitter spells for the operator itself. The refusal of an `undefined` comparand (`INVALID_FILTER` / 400) is kept: without it some positions would answer instead of refusing. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. A subclass that called or overrode one of the three removed methods: see **BREAKING** above. + - **`@objectstack/driver-sqlite-wasm` — `SqliteWasmDriver` inherits the `SqlDriver` change above**, with the same answers on a seamed read and on a direct call. + - **`@objectstack/driver-turso` — both faces of `TursoDriver` compile the filter they are handed.** Local and replica mode inherit the `SqlDriver` change. Remote mode: `toRemoteFilter` no longer widens a bare-day `$lte` or a `$between` maximum (it still splits a two-bound `$between` into the `$gte` / `$lte` pair the remote transport compiles, both ends inclusive, and still converts each comparand to storage form), and `RemoteTransport` no longer rewrites a `$not` operand (its copy of the polarity tables is deleted). The two faces still answer every filter alike, on a seamed read and on a direct call. The remote transport keeps its refusal of an `undefined` comparand, worded as `driver-sql`'s, so both faces refuse it in one sentence. The same one line keeps the seam's reading on a direct call. + - **`@objectstack/spec` — the ADR-0087 ledger records the removal.** The protocol-18 step of `MIGRATIONS_BY_MAJOR` gains the semantic entry `driver-sql-calendar-day-methods-removed`, which names the three removed methods with their replacement and acceptance criteria. Every upgrade channel that projects protocol 18 carries it. `spec-changes.json` and the generated upgrade guide stop at the current protocol, 17, so neither changes in this release. A subclass that re-declares one of the methods without `override` still compiles and is never called, so the ledger, not the compiler, is the notice that reaches it. +- 95e24b0: fix(driver-sql,driver-turso)!: an upsert whose conflict lands on another organization's row is refused with `UNIQUE_VIOLATION` and writes nothing, and an upsert never changes a row's organization (#21185) + + Clause-②: no (narrowing) + + + + **BREAKING for `upsert` callers on the SQL drivers and on `TursoDriver`.** + + **What changed.** `upsert` resolves its conflict against the whole table, and the + primary key and a `unique: 'global'` column are installation-wide, so the row a + tenant-scoped call (`options.tenantId` on an object with a tenant column) collided + with could belong to another organization. The merge wrote the payload onto that + row, tenant column included. Now: + + - **A tenant-scoped upsert merges only into a row of the organization the row is + written under**, for any conflict target, the primary key included. A conflict + that lands on a row of another organization, or on a row with no organization, + is refused with `code: 'UNIQUE_VIOLATION'`, `status: 409`, and nothing is + written. That is the answer `create()` gets for the same collision: from the + caller's organization the call is an insert, and that insert collides. The + refusal names no organization and no value of the row it collided with. + - **The tenant column is insert-only** (`insertOnlyUpsertColumns`), like `id`, + `created_at` and `auto_number` columns: an upsert with no tenant context merges + into the row it lands on and keeps that row's organization. + + Mechanism, per face: on SQLite, PostgreSQL and the remote (libSQL) face, the merge + statement carries the organization predicate (`DO UPDATE … WHERE`), so another + organization's row is never written. On MySQL, whose `ON DUPLICATE KEY UPDATE` + takes no `WHERE`, the statement and a read of the landed row run in one + transaction (a savepoint inside a caller's transaction), and the read's failure + rolls the write back. The remote face now also stamps the caller's organization on + the row it inserts, as the local faces do. + + ## FROM → TO + + | you relied on | now | + |:--|:--| + | a tenant-scoped `upsert` merging into a row of another organization | refused with `UNIQUE_VIOLATION` / 409, nothing written | + | an `upsert` payload's tenant value moving the row it merges into | the row keeps its organization; to move a row between organizations, use `update()` | + + **What is not affected.** A tenant-scoped upsert whose conflict lands on a row of + its own organization merges as before, on every target. An upsert that inserts + lands under the caller's organization, or under the organization the payload + names explicitly, as before. + +### Patch Changes + +- addbbf0: feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518) + + Clause-②: yes (widening) + + - **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused. + - **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source. + - **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. The runtime resolves the reference onto that served field; see the picklist runtime entry of this release. + - **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package. + - **Translation.** `TranslationData` gains `picklists..{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them. + - **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES). + - **Extraction.** `os i18n extract` walks `picklists.NAME.{label, options.VALUE}`, including an extension's options under the list it extends, and `os lint` reports an untranslated option under its own rule, `i18n/missing-picklist`. + - **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column. +- df67985: driver-sql refusals, drift reports and log lines no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages the SQL driver shows to authors and operators ended with an issue-tracker number where + the reason belonged. The number goes, and where the sentence did not already say what was decided, it + now does: + + - Filter refusals (`INVALID_FILTER`): the withheld-detail wording ("withheld from the message; the full + diagnostic is in the server log"), the JSON-column, zero-operator, `$null` / `$exists`, undefined + comparand and unknown-combinator refusals, and the filter-array refusal. + - Schema and index messages: the `reference_to` DDL refusal (the FOREIGN KEY DDL that key used to gate + is retired, because it could never fire for a spec-conformant lookup), the MySQL TEXT-key and + row-size explanations, the hash-shadow UNIQUE messages, and the `os migrate plan` drift entries. + - The NULL-safe UNIQUE messages now say why rows without an organization were never constrained: SQL + UNIQUE is NULL-distinct. + - Boot log lines for the SQLite datetime, time and json canonicalisation and the MySQL `TIMESTAMP` / + `TIME` widening now say what the conversion is for. + + Text only: no error code, field name, status or behaviour changes. Three aggregate refusals keep their + citation for now, because a test in `@objectstack/driver-turso` compares them byte for byte with the + Turso remote transport's copies; they change together with those copies. +- 42d78b9: driver-turso refusals and log lines, and driver-sql's last three aggregate refusals, no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages the Turso driver shows to authors and operators ended with an issue-tracker number where + the reason belonged. Most of the Turso remote transport's numbers were bare ids from the repository that + file used to live in, so here they pointed at unrelated cards. The number goes, and where the sentence + did not already say what was decided, it now does: + + - Aggregate refusals, on both drivers: the undeclared-function, `count_distinct`-without-`field` and + per-aggregation `filter` refusals lose their citation on the SQL driver and the Turso remote + transport together, so the two faces still read one sentence. The remote transport's + declared-but-uncompiled and date-bucket refusals lose theirs too, and read exactly like the SQL + driver's again. + - Turso remote filter refusals (`INVALID_FILTER`): the withheld cross-field and unbindable-comparand + wording, and the full diagnostics behind every filter refusal (unsupported operator, unlowered + `$between`, undeclared or non-list combinator, non-node operand, non-object `where`, empty operator + map, undefined comparand, non-boolean `$exists`) lose only the citation, because their sentences + already said it. The non-boolean `$null` diagnostic now says every driver refuses it, so one filter + no longer gets a different answer per backend. + - The Turso remote `auto_number` refusal (`NOT_IMPLEMENTED`) now says why it refuses rather than + resolves: resolving would write NULL into the slot and persist the row without its record number. + - Log lines: the unnumbered-upsert warning loses its citation; the remote canonical backfill's info line + says what a conversion buys (the column drops the unindexable read-side repair only once a pass finds + nothing left to convert); the unresolvable-remainder warning says a value that cannot be read as an + instant is counted and reported, never guessed at. + + Text only: no error code, field name, status or behaviour changes. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- 810d42b: fix(driver-sql): the first boot of a new database no longer prints a `DATABASE_ERROR` for `sys_migration` (#20768) + + On the first boot of a new database, the SQL driver printed this line once, on its warn channel (stderr by default): + + ```text + [sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... no such table: sys_migration + ``` + + Nothing was wrong. At the start of its first schema sync, before it creates any table, the driver asks whether this deployment's file columns have moved (the ADR-0104 media-arm resolver). The resolver the engine supplies answers by reading `sys_migration`. On a new database that table does not exist yet, so the read is refused and the answer is "not moved", which is correct for an empty store. + + The driver now asks that question inside an async scope. Inside it, a read refused because its own target table does not exist goes to the logger's `debug` channel instead of `warn`. The default logger has no `debug`, so the line is not printed. The logger shape gains an optional `debug`. The refusal is still thrown to the resolver, and the resolver's answer is the same as before. + + What still warns: + + - every other refusal inside that scope, such as a malformed statement on a table that exists, or a missing table named by another relation (a view over a dropped table); + - a missing table read anywhere else, as before. + + The missing-table check is the shared `isMissingTableError` from `@objectstack/types`, which `@objectstack/metadata/errors` re-exports. There is nothing to migrate. +- cf0346e: fix(driver-sql): `os migrate plan` on a database that does not exist yet no longer prints `DATABASE_ERROR` for the tables whose DDL it deferred (#20821) + + `os migrate plan` (and the boot of `os migrate apply`) runs with the SQL driver's DDL deferred: the driver records every table as pending `create_table` and creates none of them. The same boot then reads `sys_metadata`, `sys_metadata_activation` and `sys_migration`. On a new database those tables do not exist yet, so each read was refused, and each refusal printed a line like this on the driver's warn channel (stderr by default): + + ```text + [sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_metadata' (SQLITE_ERROR) ... no such table: sys_metadata + ``` + + Nothing was wrong: every reader already answers from the refusal, and the plan lists the same tables as pending creates. A dry run on a new database printed six of these lines. + + The driver now sends such a refusal to the logger's `debug` channel instead of `warn`, when all three hold: + + - this driver has DDL deferred; + - the refused statement targets a table whose DDL this driver deferred; + - the shared `isMissingTableError` predicate from `@objectstack/types` recognises the refusal as that table being missing. + + The default logger has no `debug`, so the line is not printed. The refusal is still thrown to the caller with the same envelope (`DATABASE_ERROR`, status 500), and the plan's output is unchanged. + + What still warns: + + - every other refusal on a deferred driver, such as a malformed statement on a table that exists; + - a missing table that the driver did not defer, such as a table nothing in the boot declares; + - every refusal once the deferred DDL has been applied, or on a driver that never deferred any. + + There is nothing to migrate. +- d1633f3: fix: the analytics native-SQL path answers a measure its response declares `number` as a number on every dialect, presented by the one rule `driver-sql`'s `aggregate()` applies, which `@objectstack/core` now exports as `AGGREGATE_ANSWER_KIND` and `presentAsNumber` (#20889) + + Clause-②: yes (widening) + + **New exports.** `@objectstack/core` exports two names, moved here unchanged + from `@objectstack/driver-sql`, which now imports them instead of keeping them + private: + + - `AGGREGATE_ANSWER_KIND`: what each declared aggregate function answers. + `count`, `count_distinct`, `sum` and `avg` answer `'number'`; `min` and `max` + answer `'column'`, a value of the aggregated column. + - `presentAsNumber(value)`: the `'number'` presentation. A string `Number()` + reads as a number becomes that number. Any other value is returned as given: + a number, `null`, a boolean, empty or blank text, or text that reads as NaN. + + **What changed.** On PostgreSQL, `POST /api/v1/analytics/query` and + `POST /api/v1/analytics/dataset/query` answered through `NativeSQLStrategy` + returned count, count_distinct, sum, avg, and min / max over a numeric column + as strings, such as `count: "2"` and + `sum: "500.000000000000000000000000000000"`, while `fields[]` declared + `number`. A dataset's `row_count` did the same, and a measure-scoped count + mixed `"1"` with the number `0` in one column. SQLite answered numbers. The + strategy now presents each measure column by its declared aggregate function, + through the same table and presenter as `SqlDriver.aggregate()`. `min` / `max` + are presented only when their column is declared numeric, so `max` over a text + column, every dimension, and expression measures keep the value the database + returned. + + **Precision.** The answer is one JS number, the policy `driver-sql`'s + `aggregate()` already applies. A total that needs more digits than a double + holds, such as `9007199254740993`, answers the nearest double + (`9007199254740992`), which is also what SQLite and the engine path answer. + + **What did not move.** `@objectstack/driver-sql`'s behaviour is unchanged: its + `aggregate()` reads the same table, and its read presenter calls the same + function. The answers on SQLite are byte-identical. The arithmetic of the + analytics native statement did not change either. On PostgreSQL its `sum` and + `avg` still add exact decimals, so `0.1 + 0.2` answers `0.3` where the engine + path answers `0.30000000000000004`. +- 58a77db: fix(service-analytics)!: the analytics read scope and the native `where` answer `$contains` / `$notContains` on a multi-valued or JSON-stored field by membership, with the one construct `driver-sql` emits, now exported from `@objectstack/core` (#20987) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics doors answer for one class of read. A row policy (the read scope the analytics plugin compiles from the security service, or a host's own `getReadScope`) whose `$contains` or `$notContains` names a field declared multi-valued (`multiple: true` on a multi-capable type, or a multi-option type) or JSON-stored now selects the rows holding the comparand as an ELEMENT of the stored list. It used to select every row whose stored JSON text contained the comparand as a substring, so on SQLite a policy could admit rows outside it, and on PostgreSQL every query under such a policy answered `500` (MySQL was not measured). An analytics count under such a policy now equals what the same caller reads through the data door. On a datasource whose SQL dialect the analytics host cannot name, such a policy now refuses the query (`READ_SCOPE_COMPILE_FAILED` / `500`) instead of falling back to the substring reading. It ships as `minor` under the launch-window convention. + + **The `where`.** `POST /api/v1/analytics/query`, the dataset door and `/analytics/sql` on the native strategy render the same membership test for a `$contains` / `$notContains` in a query's `where` (or a dataset's `runtimeFilter`) on such a field: on PostgreSQL the query answers rows where it answered `500`, and on SQLite the count stops over-counting (`$contains`) and under-counting (`$notContains`). On a datasource whose dialect the host cannot name, the operator on such a field is refused `INVALID_FILTER` / `400`. The ObjectQL strategy already answered membership and is unchanged. + + **Unchanged.** On a scalar text field `$contains` stays the substring test, on every face. `$notContains` keeps its NULL rule: a row with no value satisfies it. A host that wires no field metadata keeps the substring reading, because it cannot tell a JSON column from a text one; the analytics plugin wires it from the data engine. + + **New export.** `@objectstack/core` exports `jsonMembershipPredicate(dialect, emitters, value)` and `jsonMembershipCandidates(value)`, with the `JsonMembershipDialect` and `JsonMembershipEmitters` types: the per-dialect membership construct (#17590) moved from `@objectstack/driver-sql`, where it was module-private, and made placeholder-agnostic. `@objectstack/driver-sql` imports it and emits byte-identical statements and bindings. + + **What to do after upgrading.** Nothing, unless a policy or a dashboard filter relied on the substring reading of a multi-valued or JSON-stored field: such a filter now selects members only, as the data door always did. A host whose analytics `sqlDialect` hook answers nothing for a SQL datasource should answer `'sqlite'`, `'postgres'` or `'mysql'`, or the operator on such a field is refused. +- a11faee: fix(objectql)!: a per-aggregation `filter` refuses `$in` / `$nin` / `$eq` / `$ne` / an ordering / `$between` / implicit equality on a declared JSON-stored field with `INVALID_FILTER` / 400, in the words `where` refuses them in, instead of counting rows the stored arrays cannot support + + Clause-②: yes (widening) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts in one position, `aggregations[i].filter`, on every driver and for every caller that reaches the engine: the REST query door (`POST /api/v1/data/:object/query`), a flow or hook, and the analytics strategy that lowers a dataset measure's filter onto `engine.aggregate`. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a per-aggregation `filter` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, and before any driver is asked for a row, so an empty table refuses it too. That is the set `driver-sql`'s `where` refuses on such a column, for the same reason. + + **What an author sees now.** The same 400 body the same filter gets as a `where`: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, as they are for `where`, and the full diagnostic, naming both and the aggregation position, goes to the server log. + + **Why a refusal.** The engine evaluates a per-aggregation filter itself, and it compared the whole stored array against a scalar. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $in: ['u1', 'u9'] } }` counted 0, `{ owners: { $nin: ['u1', 'u9'] } }` counted all 6, the two rows it was asked to exclude among them, `$gt` / `$lte` / `$between` counted 4 / 1 / 5, and `{ tags: { $eq: 'red' } }` counted the row holding `['red']` by JS loose equality. The same filters in `where` were 400 on both dialects. + + **Who is affected.** A dashboard, report, dataset measure or caller whose per-aggregation filter compares a JSON-stored field with one of those operators and read the count as a real answer. Also a host calling `applyInMemoryAggregation` directly with a `fields` map: it now judges each `aggregations[i].filter` against that map before any row (an empty `rows` array included) and throws the same `INVALID_FILTER` / 400. It takes an optional fifth argument, `reportWithheld(diagnostic)`, which receives the withheld field, operator and position; without it the diagnostic is dropped. A call without `fields` judges nothing, as before. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. + + **Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored; `having`; `where`; and a host whose engine has no declaration for the object, where nothing is judged. + + **`@objectstack/core`** (three new root exports): `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, `jsonColumnOperatorRefusalText(field, op, bare)` and its return type `JsonColumnOperatorRefusalText` (`{ message, diagnostic }`). They are the operator set and the two texts (the withheld message and the full diagnostic) of the JSON-column refusal, so `driver-sql`'s `where` and the engine's per-aggregation filter refuse with one set and one sentence. + + **`@objectstack/driver-sql`**: no behaviour change. Its JSON-column gate reads the set and the text from `@objectstack/core`; every refusal it prints is byte for byte what it printed before. +- 097ef80: fix: the analytics native-SQL path aggregates with the engine's own aggregate policies, so one query answers one number whichever strategy serves it: `sum` / `avg` accumulate in double, a PostgreSQL boolean aggregand is cast, and an all-NULL `sum` answers `0`. The operand policies move from `@objectstack/driver-sql` to `@objectstack/core` (#21042) + + Clause-②: yes (widening) + + **New exports.** `@objectstack/core` exports the aggregate operand policies, moved here from `@objectstack/driver-sql`, where they were module-private. The driver now imports them and emits byte-identical statements. + + - `AGGREGATE_ACCUMULATION`: what each declared aggregate function accumulates in on PostgreSQL and MySQL. `avg` accumulates in double; `sum` accumulates in double over a fractional column; the counts, `min` and `max` take the column as stored. + - `aggregandColumnClass(shape)`: the one column-class predicate those policies read, over a column's declared `{ type, multiple }`. It answers `'fractional'`, `'integral'`, `'boolean'`, or `undefined` for every other column, a multi-valued one included. The type `AggregandColumnClass` names the three classes. + - `POSTGRES_BOOLEAN_AGGREGAND_CAST`: the functions whose boolean aggregand is cast to `int` on PostgreSQL. These are `sum`, `avg`, `min` and `max`; the two counts are never cast. + - `doubleAccumulationOperand(operand, dialect)`: the column's text, parsed as a double, spelled for `'postgres'` or `'mysql'`. + - `aggregandOperandSql(func, columnClass, dialect, operand)`: the operand an aggregate wraps, with the cast inside the double operand. The type `AggregandSqlDialect` names its dialects (`'sqlite'`, `'postgres'`, `'mysql'`, `'unknown'`). + + **What changed.** `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query` served by `NativeSQLStrategy` (the default on a SQL driver) skipped three policies `SqlDriver.aggregate()` applies. So the ObjectQL strategy and `engine.aggregate` answered differently for the same query. Measured on SQLite and PostgreSQL 16.13: + + - On PostgreSQL, `sum` / `avg` over an exact-decimal column, and `avg` over an integer one, added exact decimals. For example, `0.1 + 0.2` answered `0.3` and `11 / 9` answered `1.222222222222222`, where the engine answers `0.30000000000000004` and `1.2222222222222223`. The native statement now accumulates in double, as the driver does. + - On PostgreSQL, `sum` / `avg` / `min` / `max` over a boolean field answered `500` (`function sum(boolean) does not exist`). The native statement now casts the boolean aggregand to `int`, as the driver does, and answers the numbers the engine answers. + - On every dialect, a group whose aggregand is NULL in every row, and a measure-scoped `sum` that admits no row, answered `sum` `null` at the cube door. The strategy now folds a `null` answer to `emptyGroupValueFor` (`@objectstack/spec`) for every measure, so that `sum` answers `0`. `avg`, `min` and `max` over nothing stay `null`. The dataset door already answered `0`. + + This is no narrowing: each answer moves to the value the platform already declared for the same query. + + **What did not move.** `@objectstack/driver-sql`'s statements and answers are unchanged: a move-proof test compiles each aggregate function over each column class on SQLite, PostgreSQL and MySQL, and the statements equal the ones captured before the move. SQLite's native statement is unchanged, because neither operand policy applies there. A host that relays no field declarations to the analytics service, or names no SQL dialect, gets today's native arithmetic. +- 682873d: fix(core): the refusal a filter gets for a scalar comparison or text operator on a multi-value or JSON field reads true on every backend that prints it, and reaches a REST caller whole + + Clause-②: no + + The `INVALID_FILTER` / 400 refusal `driver-sql`'s `where`, the engine's per-aggregation `filter` and `driver-memory` all print (`jsonColumnOperatorRefusalText`) explained itself with `driver-sql`'s storage ("a field this driver stores as a JSON TEXT column") and the two wrong answers SQL used to give. That is untrue on the engine and on `driver-memory`. The message was also 748 characters, and the REST envelope cuts a 4xx message at 499 plus an ellipsis, so callers on SQLite and PostgreSQL read `…Refused rather than compiled because the answ…` and never reached the sentence saying the field and the operator were withheld. + + The message now reads, on every backend, in 486 characters: `A constraint in this filter WAS NOT APPLIED: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member.`, then the same `$contains` / `$or` of `$contains` remedy, then `For no value, use "$null" or "$empty".` (a `null` comparand such as `{ f: null }`, `$eq: null` or `$ne: null` is refused too, and `$contains` could not express it), then `The field and the operator are withheld from the message; the full diagnostic is in the server log.` The diagnostic (the server-log text, and what a filter's own author is shown) gives the same reason with the operator named, names the field, and spells the remedy with the field's name. It drops the storage and the SQL history too, and is now whole on the wire for field names up to 26 characters (it was 643 characters or more and always cut). + + Code, status, the refused operator set and the `$contains` remedy are unchanged. A client that matched on the old words `JSON TEXT column` or `Refused rather than compiled` should match on `code: "INVALID_FILTER"` instead. +- e35c40a: feat(driver-turso): the remote transport issues `auto_number` values (#21113) + + Clause-②: yes (widening) + + A `create()`, `bulkCreate()` or `upsert()` on the Turso REMOTE transport that + leaves an `autonumber` field empty (`undefined`, `null` or `''`) now gets a + generated value. It used to be refused with `NOT_IMPLEMENTED` / 501, so on a + hosted tenant database — which is on this transport — no object declaring an + `auto_number` field could get a new record at all. Nothing is declared anew in + the spec or in the package exports; `supports.autonumber` stays `true` and is + now honoured. + + - The value comes from the same persistent `_objectstack_sequences` counter the + local and embedded-replica transports use, rendered by the same format rules + (`autonumberFormat` / `format`, organization scope, date and `{field}` + tokens), bootstrapped from the table's highest existing value by the same + reading, and re-seeded the same way after rows land above the counter by a + seed replay or import. A remote driver and an embedded replica of one + database draw from one counter row. + - The counter moves in one statement over the connection (`UPDATE … RETURNING`, + or on a cold counter `INSERT … ON CONFLICT (key_hash) DO UPDATE … RETURNING`), + so writers in different processes never draw the same number. + - An `upsert()` that merges into an existing row keeps the number already in + the row; a row that carries its own number is written unchanged. + - A `_objectstack_sequences` table in the pre-`key_hash` shape is refused in + remote mode with `DATABASE_ERROR` / 500 and the remedy in the message (open + the database once through the local or embedded-replica transport, which + migrates it); remote mode does not migrate it and does not key by the legacy + rule. + - `RemoteTransport.upsert()` takes an optional fifth argument naming columns + that are written on insert and left alone on merge. + + `@objectstack/driver-sql`: the sequence rules a second transport shares are + now `protected` members of `SqlDriver` (`resolveSequenceTenantId`, + `defineSequencesTable`, `maxAutonumberCounter`, `escapeLikePrefix`, + `sequencesTableName`, `autoNumberCollisionRetries`). No export is added and no + behaviour changes on any dialect. +- c6b6889: fix(driver-sql): an autonumber format whose rendered prefix carries `_`, `%` or `\` seeds its counter from the stored MAX on SQLite + + Clause-②: no + + The SQL driver reads the highest counter already stored under an autonumber prefix in two places: the first issue of a counter (the cold bootstrap) and the re-seed after a create collides with a number that a seed replay, an import or direct SQL already wrote. Both escape the prefix's `\`, `%` and `_` with a backslash for a `LIKE` scan, but the scan declared no `ESCAPE` character, and SQLite's `LIKE` has none unless one is declared. On SQLite (better-sqlite3, and the Turso local and embedded-replica faces; the WebAssembly SQLite driver inherits the same scan) such a prefix therefore matched no stored row: + + - **Cold**, the counter started at 1 under numbers already stored. Measured: a format `SO_{0000}` over a stored `SO_0007` issued `SO_0001`. + - **On the re-seed**, the counter could not move, so every retry collided again and the create was refused once the retries ran out. + + The prefix is rendered, so the character can come from data as well as from the format: `{region}-{0000}` with a region value of `north_east` was affected in the same way. + + The scan now binds the driver's one `LIKE` escape character on every dialect, as the driver's filter `LIKE` already does. PostgreSQL and MySQL already used a backslash as their default `LIKE` escape, so the answer there does not change; a prefix with none of the three characters is not affected anywhere. Counters already seeded too low are not rewritten: the next collision on one now re-seeds it from the stored MAX, as on any other prefix. +- ebdb6f2: An `upsert` keyed on a business column keeps the stored row's primary key on the Turso remote face, and both drivers answer the stored row. + + Clause-②: no + + **Remote face (`@objectstack/driver-turso`).** `upsert(object, data, ['email'])` on a remote (hosted) database used to replace the matched row's `id`: with the payload's `id` when it carried one, else with a freshly generated one. Every reference to the old id was left pointing at nothing, and no error was raised. The merge now leaves `id` and `created_at` alone, as the local and embedded-replica faces already do. It reads the columns to leave alone from the same list the local faces use, so `id`, `created_at` and the `auto_number` columns are kept on a merge on every face. An upsert on the primary key (no `conflictKeys`, or `['id']`) is unchanged, and an upsert that inserts still writes the payload's `id`, or a generated one. + + **The answer (`@objectstack/driver-sql`, and the remote face).** On such a merge, `upsert` returned the payload instead of the stored row, so the answer carried the payload's `id` (or the generated one), an id no stored row has. It now returns the stored row: its own `id`, with the merged values. The row is read back by the conflict-key values. When a conflict key is empty in the payload, nothing can have matched it, so the row was inserted and it is read back by its `id`, as before. + + To change a row's `id` on purpose, use `update()`. An `upsert` never changes it. +- be5a83c: On MySQL, `SqlDriver.create` and `SqlDriver.bulkCreate` now answer the rows they stored (#21227). + + Clause-②: no + + MySQL has no `INSERT … RETURNING`. knex drops the clause on the MySQL family and answers the insert id instead, so `create` answered `0` and `bulkCreate` answered a one-element array whatever the row count, although every row was stored. Callers that use the answer failed one layer up: on a MySQL datasource, sign-up answered `400 FAILED_TO_CREATE_USER` with the user stored and no account, the dev admin seed failed, and a multi-row `bulkCreate` through the engine was refused after its rows had landed. + + On the MySQL family both doors now read the rows back by the ids they wrote, under the tenant the rows were written with, inside the caller's transaction when there is one: one extra `SELECT` per `create` and per `bulkCreate` batch. SQLite and PostgreSQL still answer from `RETURNING`, with no extra statement and no change in what they answer. If a written row is gone before it can be read back (deleted in between by another statement or a trigger), the call throws `DATABASE_ERROR` (500) and does not retry the insert. + + Nothing to change in a project. Code that read the record from the result now gets it on MySQL as on the other dialects. +- 7923c8e: On MySQL, a table that declares a `Field.datetime` with `defaultValue: 'NOW()'` is now created (#21241). + + Clause-②: no + + On MySQL the driver builds a declared `Field.datetime` column as `DATETIME(3)`, but it gave the column's `NOW()` default a bare `CURRENT_TIMESTAMP`, which has precision 0. MySQL refuses a `CURRENT_TIMESTAMP` default whose precision differs from its column's (`Invalid default value for '…'`). The whole `CREATE TABLE` failed, and so did `ALTER TABLE … ADD` for a new field. The object's data endpoints then answered `500`. Two platform tables were affected: `sys_activity` and `sys_presence`. Record writes still succeeded, but none of them got an activity-timeline row. + + The default now carries the column's precision. It is `CURRENT_TIMESTAMP(3)`, the expression the builtin `created_at` / `updated_at` columns already used, and both now read one precision setting. PostgreSQL and SQLite emit the same DDL as before. + + One older case is fixed in the same place. A database created before datetime columns became `DATETIME(3)` holds them as `TIMESTAMP`. Schema sync widens those columns with `ALTER TABLE … MODIFY`, and that statement restated the default of `created_at` / `updated_at` but dropped the default of a declared `NOW()` field. After the widening, an insert that left the field out stored `NULL`. The widening now restates that default too, with the same expression. + + Nothing to change in a project. On the next boot, schema sync creates any table that failed before. No other migration is needed: on MySQL, no table could have been created with the refused default. A column that an earlier widening already left without a default does not get one back. +- 95b91cc: On MySQL, a write to an object that does not declare `created_at` or `updated_at` no longer fails with `Incorrect datetime value … for column 'updated_at'`. The driver creates both columns on every table it builds, and the engine stamps both on every insert as ISO-8601 text (`2026-10-01T21:49:27.479Z`). Only a column the object declared as `Field.datetime` was rewritten into the `2026-10-01 21:49:27.479` form MySQL accepts. The engine declares both columns on most objects, but not on an object with `managedBy: 'better-auth'` or `systemFields: false`, so those writes were refused. + + Clause-②: no + + **What this fixes.** `sys_jwks` declares `created_at` only, so on MySQL the JWT signing key was never stored. `GET /api/v1/auth/jwks` and `GET /api/v1/auth/token` answered 500, `get-session` carried no `set-auth-jwt` header, and no OIDC or MCP token could be issued. `sys_member` failed the same way, so the seeded admin had no organization membership. Now both answer 200, the key is stored, and the membership is stored. In the CRM example's boot, 9 objects declare `created_at` without `updated_at` and 2 declare neither. Every write door formats the column: `create`, `bulkCreate`, `upsert`, `update` and `updateMany`. + + **SQLite and PostgreSQL.** The value the engine stamps is bound unchanged on both, so their behaviour is the same. One input shape changes on SQLite: a JS `Date` written to an undeclared audit column is now stored as the canonical ISO text, as it already is for a declared `Field.datetime`. Before, it was stored as epoch milliseconds and read back as a number. A column the object declares keeps its declared type. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/observability@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index e4400589530..7e5f409bc33 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index f744cd4f67b..f894b14953e 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,194 @@ # @objectstack/driver-sqlite-wasm +## 17.6.0 + +### Patch Changes + +- ceee88f: fix(driver-sql, driver-turso, plugin-security, spec)!: `SqlDriver` and `TursoDriver` compile the filter they are handed — their copies of the whole-day bound and of the NULL-safe `$not` rewrite are deleted, and the RLS compile seam lowers type-blind when it cannot read the declared types (ADR-0053 D-D1 items 5, 7 and 9, #20822) + + Clause-②: no (narrowing) + + + + **BREAKING**: `SqlDriver` in `@objectstack/driver-sql` loses three `protected` methods: `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite`. They were the driver's copy of the whole-day bound, which the shared lowering now applies once, at the seams, before a driver sees the filter. A subclass of `SqlDriver` that calls one of them, or overrides one with the `override` modifier, no longer compiles (TS2339, TS4113). That includes a subclass of `SqliteWasmDriver` or `TursoDriver`, which extend `SqlDriver`. A subclass that re-declares one without `override` still compiles, but the driver never calls it, so the rule it carried stops applying. It ships as `minor` under the launch-window convention. The class's public methods are unchanged. + + FROM → TO: a `SqlDriver` subclass that called `this.calendarDayUpperBoundRewrite(table, field, op, value)`, `this.calendarDayBetweenRewrite(table, field, value)` or `this.calendarDayExclusiveUpperBound(table, field, value)`, or overrode one of them, lowers the filter with `lowerFilterCondition` from `@objectstack/spec/data` instead, before the driver compiles it: `lowerFilterCondition(where, { isDatetimeColumn })`, where `isDatetimeColumn` answers which columns get the whole-day bound. + + **Supersedes two sentences of this release's shared-lowering entry** (`lowerFilterCondition`, #5930), which this change makes false: + + - "A guard without that set treats no column as `datetime`." Now: when the RLS compile seam has no field guard, or one without a `datetime` set, it cannot read which columns are `datetime`, so it applies the whole-day rule to every column (the `@objectstack/plugin-security` entry below). + - "Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input." Now: `SqlDriver`, `SqliteWasmDriver` and `TursoDriver` keep no copy of the whole-day bound or of the NULL-safe `$not` rewrite. A read through the engine or the RLS compile seam gets the lowered answer, and a call on the driver itself gets the comparison it wrote (the `@objectstack/driver-sql` and `@objectstack/driver-turso` entries below). + + - **`@objectstack/plugin-security` — an RLS policy compiled with no field guard is lowered type-blind.** The RLS compile seam runs the shared `lowerFilterCondition` on every compiled `using` and `check` filter. When the security plugin could not resolve the object's declared fields (no field guard), or a caller of `RLSCompiler.compileFilter` passes a guard without a `datetime` set, the seam cannot read which columns are `datetime`, and it now applies the whole-day rule to every column (a bare-day upper bound becomes `$lt` the next day), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the type. It used to read no column as `datetime`, which left the bound to each driver's own copy of the rule. Visible on a `using` policy such as `record.signed_on <= '2026-01-05'` on such an object: every row of that day is kept on every driver, including `InMemoryDriver`, which had compared it as written since its own copy was deleted. A guard with a `datetime` set is unchanged, and so are the NULL-polarity guards. + - **`@objectstack/driver-sql` — `SqlDriver` keeps no copy of the rules the seams apply.** Deleted: the whole-day rewrite of a bare-day `$lte` and of a `$between` maximum on a `datetime` column, on the plain and the legacy-normalised column paths, including the last supported day (the protected methods `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite` are removed from the class); and the NULL-safe rewrite of a `$not` operand (`nullSafeNegationOperand` and its polarity tables, module-private). A read through the engine or the RLS compile seam is unchanged: the seam hands the driver a filter the shared lowering has already rewritten, and the deleted copies gave the same answer on that input. A caller that passes no seam — `find`, `findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` or `findWithWindowFunctions` called on the driver itself — now gets the comparison it wrote: a bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, `$lte '9999-12-31'` compares against that midnight, and a `$not` is SQL's three-valued negation, so a row whose compared column is NULL is not returned by it. `$ne`, `$nin` and `$notContains` keep their NULL-safe form, which this emitter spells for the operator itself. The refusal of an `undefined` comparand (`INVALID_FILTER` / 400) is kept: without it some positions would answer instead of refusing. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. A subclass that called or overrode one of the three removed methods: see **BREAKING** above. + - **`@objectstack/driver-sqlite-wasm` — `SqliteWasmDriver` inherits the `SqlDriver` change above**, with the same answers on a seamed read and on a direct call. + - **`@objectstack/driver-turso` — both faces of `TursoDriver` compile the filter they are handed.** Local and replica mode inherit the `SqlDriver` change. Remote mode: `toRemoteFilter` no longer widens a bare-day `$lte` or a `$between` maximum (it still splits a two-bound `$between` into the `$gte` / `$lte` pair the remote transport compiles, both ends inclusive, and still converts each comparand to storage form), and `RemoteTransport` no longer rewrites a `$not` operand (its copy of the polarity tables is deleted). The two faces still answer every filter alike, on a seamed read and on a direct call. The remote transport keeps its refusal of an `undefined` comparand, worded as `driver-sql`'s, so both faces refuse it in one sentence. The same one line keeps the seam's reading on a direct call. + - **`@objectstack/spec` — the ADR-0087 ledger records the removal.** The protocol-18 step of `MIGRATIONS_BY_MAJOR` gains the semantic entry `driver-sql-calendar-day-methods-removed`, which names the three removed methods with their replacement and acceptance criteria. Every upgrade channel that projects protocol 18 carries it. `spec-changes.json` and the generated upgrade guide stop at the current protocol, 17, so neither changes in this release. A subclass that re-declares one of the methods without `override` still compiles and is never called, so the ledger, not the compiler, is the notice that reaches it. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [df67985] +- Updated dependencies [42d78b9] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [810d42b] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [cf0346e] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [e35c40a] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [c6b6889] +- Updated dependencies [ebdb6f2] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [be5a83c] +- Updated dependencies [d2bc644] +- Updated dependencies [7923c8e] +- Updated dependencies [95b91cc] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/driver-sql@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index 47cd675cd63..55613dd633a 100644 --- a/packages/drivers/driver-sqlite-wasm/package.json +++ b/packages/drivers/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings", "keywords": [ diff --git a/packages/drivers/driver-turso/CHANGELOG.md b/packages/drivers/driver-turso/CHANGELOG.md index 4570b858f6c..3671850471b 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,477 @@ # @objectstack/driver-turso +## 17.6.0 + +### Minor Changes + +- c876a74: fix(spec,driver-turso)!: a turso config that forces `mode: 'replica'` with no `syncUrl` is refused where it is written and when the driver is built, instead of running as a plain local database that never syncs + + Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves. + + An embedded replica is a local file kept in sync with the remote named in `syncUrl`. A config that forced `mode: 'replica'` on a `file:` url with no `syncUrl` (or an empty one) was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the built driver before this change, with and without `sync`: it constructed with `transportMode` `'replica'`, `isSyncEnabled()` answered `false`, no sync interval started, the sync call did nothing, and every read and write went to the local file. A datasource declared as a replica ran as a plain local database that never replicated, with no error and no warning. + + **BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out: + + - **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service; + - **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened. + + The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. The sibling refusals keep their order. A forced replica on a remote url, an in-memory url or a bare path still meets its `url` refusal first. One with `sync` and no `syncUrl` still meets the `sync` refusal first; the schema now reports the `mode` issue beside it. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it. + + ### Migration: FROM → TO + + | You wrote | Write instead | + | --- | --- | + | `url: 'file:./data/replica.db', mode: 'replica'` (no `syncUrl`, or `syncUrl: ''`) | an embedded replica: keep the `file:` url and name the remote, `syncUrl: 'libsql://my-db.turso.io'` | + | the same | a plain local database: drop `mode` (`url: 'file:./data/app.db'` alone) | + + A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and ran as a local database. The way out is the table above. + + Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero. + + +- 05cb2bc: fix(spec,driver-turso)!: a turso config that forces `mode: 'local'` beside a `syncUrl` is refused where it is written and when the driver is built, instead of running as an embedded replica under a `local` label + + Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves. + + A `syncUrl` names the remote an embedded replica syncs with. A config that forced `mode: 'local'` on a `file:` url (or `:memory:`) beside a non-empty `syncUrl` was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the driver source before this change, with a client that counts syncs: it constructed with `transportMode` `'local'`, then synced on connect, started the sync interval, and `isSyncEnabled()` answered `true` — exactly what the same config with no `mode` (a replica) did. A datasource declared local was kept in sync with a remote, and only a label said otherwise. + + **BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out: + + - **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service; + - **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened. + + The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. It is the twin of the forced `mode: 'replica'`-without-`syncUrl` refusal, the other way round: honouring `mode: 'local'` by skipping the sync would ignore a declared `syncUrl` instead, which is the same defect with the keys swapped. The sibling refusals keep their order: a forced local mode on a remote url or a bare path still meets its `url` refusal first. An empty `syncUrl` is unset and is still accepted. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it. + + ### Migration: FROM → TO + + | You wrote | Write instead | + | --- | --- | + | `url: 'file:./data/replica.db', mode: 'local', syncUrl: 'libsql://my-db.turso.io'` | an embedded replica: drop `mode` (`url` and `syncUrl` select the replica) | + | the same | a plain local database: drop `syncUrl` (and `sync`), keeping `url: 'file:./data/app.db'` with or without `mode: 'local'` | + + A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and synced with the remote under a `local` label. The way out is the table above. + + Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` or `syncUrl` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero. + + +- e35c40a: feat(driver-turso): the remote transport issues `auto_number` values (#21113) + + Clause-②: yes (widening) + + A `create()`, `bulkCreate()` or `upsert()` on the Turso REMOTE transport that + leaves an `autonumber` field empty (`undefined`, `null` or `''`) now gets a + generated value. It used to be refused with `NOT_IMPLEMENTED` / 501, so on a + hosted tenant database — which is on this transport — no object declaring an + `auto_number` field could get a new record at all. Nothing is declared anew in + the spec or in the package exports; `supports.autonumber` stays `true` and is + now honoured. + + - The value comes from the same persistent `_objectstack_sequences` counter the + local and embedded-replica transports use, rendered by the same format rules + (`autonumberFormat` / `format`, organization scope, date and `{field}` + tokens), bootstrapped from the table's highest existing value by the same + reading, and re-seeded the same way after rows land above the counter by a + seed replay or import. A remote driver and an embedded replica of one + database draw from one counter row. + - The counter moves in one statement over the connection (`UPDATE … RETURNING`, + or on a cold counter `INSERT … ON CONFLICT (key_hash) DO UPDATE … RETURNING`), + so writers in different processes never draw the same number. + - An `upsert()` that merges into an existing row keeps the number already in + the row; a row that carries its own number is written unchanged. + - A `_objectstack_sequences` table in the pre-`key_hash` shape is refused in + remote mode with `DATABASE_ERROR` / 500 and the remedy in the message (open + the database once through the local or embedded-replica transport, which + migrates it); remote mode does not migrate it and does not key by the legacy + rule. + - `RemoteTransport.upsert()` takes an optional fifth argument naming columns + that are written on insert and left alone on merge. + + `@objectstack/driver-sql`: the sequence rules a second transport shares are + now `protected` members of `SqlDriver` (`resolveSequenceTenantId`, + `defineSequencesTable`, `maxAutonumberCounter`, `escapeLikePrefix`, + `sequencesTableName`, `autoNumberCollisionRetries`). No export is added and no + behaviour changes on any dialect. +- 862f12c: fix(driver-turso)!: in remote mode, a filter on a declared JSON-stored field is refused with `INVALID_FILTER` / 400 for every operator the local face refuses there, and `$contains` / `$notContains` answer membership instead of a substring of the stored text (#21178) + + Clause-②: yes (narrowing) + + + + **BREAKING** (`@objectstack/driver-turso`, remote mode): this narrows what `TursoDriver` answers when its `url` is a remote libSQL endpoint (such as `libsql://` or `https://`), the transport every hosted tenant database runs on, for every door that compiles a `where`: `find`, `findOne`, `count`, `updateMany`, `deleteMany`, `aggregate` and distinct values. It ships as `minor` under the launch-window convention for accept-set narrowings. Local and embedded-replica mode inherit `driver-sql`'s compiler and already answered this way; nothing moves there. + + **What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a `where` that aims any operator in `@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS` at the field is refused with `INVALID_FILTER` / 400, at any depth under `$and` / `$or` / `$not`, before any statement runs: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin`, `$startsWith`, `$endsWith`, `$icontains`, `$like`, `$ilike`, and implicit equality (`{ "owners": "u1" }`), whatever the comparand, `null` included. + + **What `$contains` / `$notContains` answer now.** Membership: `{ "owners": { "$contains": "u1" } }` matches the rows whose stored list holds `u1` as an element, so it no longer matches a row holding only `u10`; `$notContains` is its exact complement, a row with no value included; and a structured-JSON object answers no member at all, instead of matching text inside its serialization. On a scalar text field both remain the substring test they were. + + **What an author sees now.** The body the local transport answers for the same filter, byte for byte: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, and the full diagnostic, naming both, is written to the driver's logger at `warn`. + + **Why.** The remote transport compiles its own SQL and read neither the shared refused set nor the membership construct, so over a `multiple: true` lookup holding `["u1","u2"]`, `["u2"]`, `["u3","u1"]` and `["u10"]` it answered: `$nin: ["u1"]` and `$ne: "u1"` every row, the rows holding `u1` included; `$eq`, `$in` and implicit equality no row; `$lt` / `$lte` a lexicographic verdict over the serialization; `$startsWith: "["` and `$endsWith: "]"` every row with a value; `$contains: "u1"` the row holding only `u10` too. One driver gave two answers to one filter depending only on the connection string, and the exclusion operators failed open. + + **Who is affected.** A caller, saved filter, list view, report or read scope that reaches a remote-mode `TursoDriver` with one of those operators on a JSON-stored field and read the rows it got as the answer. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", `$not` around either for the exclusion, and `$null` / `$exists` / `$empty` for presence. + + **New optional API.** `RemoteTransport.setJsonColumnResolver(resolver)` in `@objectstack/driver-turso`, which `TursoDriver` wires to its own `isJsonColumn`, beside `setDeclaredValueShapeResolver`. A `RemoteTransport` driven standalone without it treats no column as JSON-stored and compiles as before. + + **Unchanged.** `$contains` and `$notContains` on a scalar field, `$exists`, `$null` and `$empty`; every operator on a field that is not declared JSON-stored; and a table this driver holds no declaration for, where nothing is judged. +- 95e24b0: fix(driver-sql,driver-turso)!: an upsert whose conflict lands on another organization's row is refused with `UNIQUE_VIOLATION` and writes nothing, and an upsert never changes a row's organization (#21185) + + Clause-②: no (narrowing) + + + + **BREAKING for `upsert` callers on the SQL drivers and on `TursoDriver`.** + + **What changed.** `upsert` resolves its conflict against the whole table, and the + primary key and a `unique: 'global'` column are installation-wide, so the row a + tenant-scoped call (`options.tenantId` on an object with a tenant column) collided + with could belong to another organization. The merge wrote the payload onto that + row, tenant column included. Now: + + - **A tenant-scoped upsert merges only into a row of the organization the row is + written under**, for any conflict target, the primary key included. A conflict + that lands on a row of another organization, or on a row with no organization, + is refused with `code: 'UNIQUE_VIOLATION'`, `status: 409`, and nothing is + written. That is the answer `create()` gets for the same collision: from the + caller's organization the call is an insert, and that insert collides. The + refusal names no organization and no value of the row it collided with. + - **The tenant column is insert-only** (`insertOnlyUpsertColumns`), like `id`, + `created_at` and `auto_number` columns: an upsert with no tenant context merges + into the row it lands on and keeps that row's organization. + + Mechanism, per face: on SQLite, PostgreSQL and the remote (libSQL) face, the merge + statement carries the organization predicate (`DO UPDATE … WHERE`), so another + organization's row is never written. On MySQL, whose `ON DUPLICATE KEY UPDATE` + takes no `WHERE`, the statement and a read of the landed row run in one + transaction (a savepoint inside a caller's transaction), and the read's failure + rolls the write back. The remote face now also stamps the caller's organization on + the row it inserts, as the local faces do. + + ## FROM → TO + + | you relied on | now | + |:--|:--| + | a tenant-scoped `upsert` merging into a row of another organization | refused with `UNIQUE_VIOLATION` / 409, nothing written | + | an `upsert` payload's tenant value moving the row it merges into | the row keeps its organization; to move a row between organizations, use `update()` | + + **What is not affected.** A tenant-scoped upsert whose conflict lands on a row of + its own organization merges as before, on every target. An upsert that inserts + lands under the caller's organization, or under the organization the payload + names explicitly, as before. +- 4b59a38: fix(driver-turso)!: a tenant-scoped call on the remote (libSQL) face reaches the rows the local face reaches, and a remote `create` stamps the caller's organization (#21226) + + Clause-②: no (narrowing) + + + + **BREAKING for callers of a remote-mode `TursoDriver` that pass `tenantId`.** + + **What changed.** The engine hands every driver the caller's organization as + `DriverOptions.tenantId`, and the group posture's membership set as `tenantIds` + (ADR-0131 D8). The local face applies them through `SqlDriver.applyTenantScope` + on every read and on every update and delete predicate, and stamps the + organization on a new row. The remote face's doors received no driver options, + so their statements carried the caller's filter and nothing else. Now: + + - **`find`, `findOne`, `count`, `aggregate`, `update`, `delete`, `bulkUpdate`, + `bulkDelete`, `updateMany` and `deleteMany` carry the caller's tenant scope on + the remote face.** The predicate is not a second copy: the remote face asks the + local face's own chokepoint for it and ANDs what that compiles to onto each + statement. So the rows a scoped call reaches are the same on both faces: the + caller's organization, rows with no organization, and, under the group posture, + the caller's membership set. + - **A remote `create` (and `bulkCreate`) stamps the caller's organization** on a + row that names none, as the local `create` does. An explicit value on the row + is kept. + - **`distinct` still refuses a tenant-scoped call on the remote face**, as before. + - A scope the remote face cannot read is refused (`INTERNAL_ERROR` / 500), never + sent without the scope. + + Where the engine's tenant wall composes a predicate above the driver, it already + kept other organizations' rows out of these answers. Where it composes none (the + posture in which that wall is inert, or an elevated caller that carries its + organization), the driver scope is the only fence, and the remote face had none. + + ## FROM → TO + + | you relied on | now | + |:--|:--| + | a tenant-scoped remote `find` / `findOne` / `count` / `aggregate` reading another organization's rows | those rows are excluded (`findOne` answers `null`); call without `tenantId` to read every organization, as on the local face | + | a tenant-scoped remote `update` / `delete` by id reaching another organization's row | `update` answers `null` and `delete` answers `false`, and the row is untouched | + | a tenant-scoped remote `updateMany` / `deleteMany` / `bulkUpdate` / `bulkDelete` reaching another organization's rows | only rows in scope are written; the count reports them | + | a tenant-scoped remote `create` landing a row with no organization | the row carries the caller's organization; to write a row with none, call without `tenantId` | + + **What is not affected.** A call without `tenantId`, or on an object with no + tenant column, sends the same statement as before. The local and embedded-replica + faces are unchanged. A scoped call's answer for the caller's own rows, and for + rows with no organization, is unchanged. + +### Patch Changes + +- 42d78b9: driver-turso refusals and log lines, and driver-sql's last three aggregate refusals, no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages the Turso driver shows to authors and operators ended with an issue-tracker number where + the reason belonged. Most of the Turso remote transport's numbers were bare ids from the repository that + file used to live in, so here they pointed at unrelated cards. The number goes, and where the sentence + did not already say what was decided, it now does: + + - Aggregate refusals, on both drivers: the undeclared-function, `count_distinct`-without-`field` and + per-aggregation `filter` refusals lose their citation on the SQL driver and the Turso remote + transport together, so the two faces still read one sentence. The remote transport's + declared-but-uncompiled and date-bucket refusals lose theirs too, and read exactly like the SQL + driver's again. + - Turso remote filter refusals (`INVALID_FILTER`): the withheld cross-field and unbindable-comparand + wording, and the full diagnostics behind every filter refusal (unsupported operator, unlowered + `$between`, undeclared or non-list combinator, non-node operand, non-object `where`, empty operator + map, undefined comparand, non-boolean `$exists`) lose only the citation, because their sentences + already said it. The non-boolean `$null` diagnostic now says every driver refuses it, so one filter + no longer gets a different answer per backend. + - The Turso remote `auto_number` refusal (`NOT_IMPLEMENTED`) now says why it refuses rather than + resolves: resolving would write NULL into the slot and persist the row without its record number. + - Log lines: the unnumbered-upsert warning loses its citation; the remote canonical backfill's info line + says what a conversion buys (the column drops the unindexable read-side repair only once a pass finds + nothing left to convert); the unresolvable-remainder warning says a value that cannot be read as an + instant is counted and reported, never guessed at. + + Text only: no error code, field name, status or behaviour changes. +- 19fc8d6: fix(driver-turso): a declared index the remote face skips because a key column never materializes is logged at `error`, not `warn` + + Clause-②: no + + In remote mode (a `libsql://` URL), schema sync skips a declared index whose key column is not + a stored column: a name that is not a field of the object (a misspelling), or a virtual + `formula` field, which is computed on read and has no column. The skip itself is unchanged, since + DDL naming a missing column would fail the whole sync. It used to be reported through the + driver's `warn` diagnostics, so a skipped UNIQUE index left duplicates accepted while the log + said `warn`. + + The skip is now logged at `error`, on the same channel the remote face already uses for a + declared index it could not create, and the local face uses for the same skip. There is one + line per skipped index per sync. It names the object, the index and the missing column, says + whether the index was UNIQUE, states what is not enforced (duplicates for a UNIQUE index, a full + table scan for a plain one), and says how to fix it: make every key column a stored field of + the object, or remove the index. + + No DDL, accept set or refusal changes: the same indexes are created and the same ones are + skipped. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- ceee88f: fix(driver-sql, driver-turso, plugin-security, spec)!: `SqlDriver` and `TursoDriver` compile the filter they are handed — their copies of the whole-day bound and of the NULL-safe `$not` rewrite are deleted, and the RLS compile seam lowers type-blind when it cannot read the declared types (ADR-0053 D-D1 items 5, 7 and 9, #20822) + + Clause-②: no (narrowing) + + + + **BREAKING**: `SqlDriver` in `@objectstack/driver-sql` loses three `protected` methods: `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite`. They were the driver's copy of the whole-day bound, which the shared lowering now applies once, at the seams, before a driver sees the filter. A subclass of `SqlDriver` that calls one of them, or overrides one with the `override` modifier, no longer compiles (TS2339, TS4113). That includes a subclass of `SqliteWasmDriver` or `TursoDriver`, which extend `SqlDriver`. A subclass that re-declares one without `override` still compiles, but the driver never calls it, so the rule it carried stops applying. It ships as `minor` under the launch-window convention. The class's public methods are unchanged. + + FROM → TO: a `SqlDriver` subclass that called `this.calendarDayUpperBoundRewrite(table, field, op, value)`, `this.calendarDayBetweenRewrite(table, field, value)` or `this.calendarDayExclusiveUpperBound(table, field, value)`, or overrode one of them, lowers the filter with `lowerFilterCondition` from `@objectstack/spec/data` instead, before the driver compiles it: `lowerFilterCondition(where, { isDatetimeColumn })`, where `isDatetimeColumn` answers which columns get the whole-day bound. + + **Supersedes two sentences of this release's shared-lowering entry** (`lowerFilterCondition`, #5930), which this change makes false: + + - "A guard without that set treats no column as `datetime`." Now: when the RLS compile seam has no field guard, or one without a `datetime` set, it cannot read which columns are `datetime`, so it applies the whole-day rule to every column (the `@objectstack/plugin-security` entry below). + - "Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input." Now: `SqlDriver`, `SqliteWasmDriver` and `TursoDriver` keep no copy of the whole-day bound or of the NULL-safe `$not` rewrite. A read through the engine or the RLS compile seam gets the lowered answer, and a call on the driver itself gets the comparison it wrote (the `@objectstack/driver-sql` and `@objectstack/driver-turso` entries below). + + - **`@objectstack/plugin-security` — an RLS policy compiled with no field guard is lowered type-blind.** The RLS compile seam runs the shared `lowerFilterCondition` on every compiled `using` and `check` filter. When the security plugin could not resolve the object's declared fields (no field guard), or a caller of `RLSCompiler.compileFilter` passes a guard without a `datetime` set, the seam cannot read which columns are `datetime`, and it now applies the whole-day rule to every column (a bare-day upper bound becomes `$lt` the next day), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the type. It used to read no column as `datetime`, which left the bound to each driver's own copy of the rule. Visible on a `using` policy such as `record.signed_on <= '2026-01-05'` on such an object: every row of that day is kept on every driver, including `InMemoryDriver`, which had compared it as written since its own copy was deleted. A guard with a `datetime` set is unchanged, and so are the NULL-polarity guards. + - **`@objectstack/driver-sql` — `SqlDriver` keeps no copy of the rules the seams apply.** Deleted: the whole-day rewrite of a bare-day `$lte` and of a `$between` maximum on a `datetime` column, on the plain and the legacy-normalised column paths, including the last supported day (the protected methods `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite` are removed from the class); and the NULL-safe rewrite of a `$not` operand (`nullSafeNegationOperand` and its polarity tables, module-private). A read through the engine or the RLS compile seam is unchanged: the seam hands the driver a filter the shared lowering has already rewritten, and the deleted copies gave the same answer on that input. A caller that passes no seam — `find`, `findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` or `findWithWindowFunctions` called on the driver itself — now gets the comparison it wrote: a bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, `$lte '9999-12-31'` compares against that midnight, and a `$not` is SQL's three-valued negation, so a row whose compared column is NULL is not returned by it. `$ne`, `$nin` and `$notContains` keep their NULL-safe form, which this emitter spells for the operator itself. The refusal of an `undefined` comparand (`INVALID_FILTER` / 400) is kept: without it some positions would answer instead of refusing. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. A subclass that called or overrode one of the three removed methods: see **BREAKING** above. + - **`@objectstack/driver-sqlite-wasm` — `SqliteWasmDriver` inherits the `SqlDriver` change above**, with the same answers on a seamed read and on a direct call. + - **`@objectstack/driver-turso` — both faces of `TursoDriver` compile the filter they are handed.** Local and replica mode inherit the `SqlDriver` change. Remote mode: `toRemoteFilter` no longer widens a bare-day `$lte` or a `$between` maximum (it still splits a two-bound `$between` into the `$gte` / `$lte` pair the remote transport compiles, both ends inclusive, and still converts each comparand to storage form), and `RemoteTransport` no longer rewrites a `$not` operand (its copy of the polarity tables is deleted). The two faces still answer every filter alike, on a seamed read and on a direct call. The remote transport keeps its refusal of an `undefined` comparand, worded as `driver-sql`'s, so both faces refuse it in one sentence. The same one line keeps the seam's reading on a direct call. + - **`@objectstack/spec` — the ADR-0087 ledger records the removal.** The protocol-18 step of `MIGRATIONS_BY_MAJOR` gains the semantic entry `driver-sql-calendar-day-methods-removed`, which names the three removed methods with their replacement and acceptance criteria. Every upgrade channel that projects protocol 18 carries it. `spec-changes.json` and the generated upgrade guide stop at the current protocol, 17, so neither changes in this release. A subclass that re-declares one of the methods without `override` still compiles and is never called, so the ledger, not the compiler, is the notice that reaches it. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- ebdb6f2: An `upsert` keyed on a business column keeps the stored row's primary key on the Turso remote face, and both drivers answer the stored row. + + Clause-②: no + + **Remote face (`@objectstack/driver-turso`).** `upsert(object, data, ['email'])` on a remote (hosted) database used to replace the matched row's `id`: with the payload's `id` when it carried one, else with a freshly generated one. Every reference to the old id was left pointing at nothing, and no error was raised. The merge now leaves `id` and `created_at` alone, as the local and embedded-replica faces already do. It reads the columns to leave alone from the same list the local faces use, so `id`, `created_at` and the `auto_number` columns are kept on a merge on every face. An upsert on the primary key (no `conflictKeys`, or `['id']`) is unchanged, and an upsert that inserts still writes the payload's `id`, or a generated one. + + **The answer (`@objectstack/driver-sql`, and the remote face).** On such a merge, `upsert` returned the payload instead of the stored row, so the answer carried the payload's `id` (or the generated one), an id no stored row has. It now returns the stored row: its own `id`, with the merged values. The row is read back by the conflict-key values. When a conflict key is empty in the payload, nothing can have matched it, so the row was inserted and it is read back by its `id`, as before. + + To change a row's `id` on purpose, use `update()`. An `upsert` never changes it. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [df67985] +- Updated dependencies [42d78b9] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [810d42b] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [cf0346e] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [e35c40a] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [c6b6889] +- Updated dependencies [ebdb6f2] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [be5a83c] +- Updated dependencies [d2bc644] +- Updated dependencies [7923c8e] +- Updated dependencies [95b91cc] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/driver-sql@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index 6984b91a976..d9b04a2876f 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Turso/libSQL Driver for ObjectStack — Edge-first SQLite with embedded replicas", "keywords": [ diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index f348ed42171..ed21def0a17 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,205 @@ # @objectstack/formula +## 17.6.0 + +### Patch Changes + +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- e18fea6: fix(objectql,driver-mongodb,formula): the `having` and per-aggregation evaluator compiles the whole-day comparison it is handed, and `$contains` asks membership on a JSON-stored field in `MongoDBDriver` and in `matchesFilterCondition` (ADR-0053 D-D1 items 5 and 9; the `FILTER_OPERATORS` `$contains` contract, #20822) + + Clause-②: no + + - **`@objectstack/objectql`: the aggregate evaluator's own whole-day copy is deleted.** The walker behind `having` and `aggregations[i].filter` no longer widens a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, on a `datetime` column to the whole day, and no longer drops the bound on `9999-12-31`. Through `engine.aggregate` nothing changes: the engine's seam lowers both positions with the shared `lowerFilterCondition` (`@objectstack/spec/data`) before the walker runs, by the object's declared `datetime` fields for the per-aggregation `filter` and by the aggregated column's type for `having`. A caller that passes no seam gets the comparison it wrote: `applyInMemoryAggregation(rows, ast, tz, fields)` called directly now counts `{ at: { $lte: '2026-02-01' } }` against that day's midnight. To keep the seam's reading on a direct call, lower each `filter` first with `lowerFilterCondition(filter, { isDatetimeColumn })`. + - **`@objectstack/driver-mongodb`: `$contains` / `$notContains` ask membership on a declared JSON-stored field.** On a field `syncSchema` recorded as `multiple: true`, a multi-option type (`tags`, `multiselect`, `checkboxes`) or a JSON type, `translateFilter` (every verb, and the aggregation `$match`) now emits an array-only `$elemMatch` over the members the comparand names, with the candidate rule the SQL dialects bind (`jsonMembershipCandidates`, `@objectstack/core`): `'1'` names the string `'1'` or the number `1`, `'true'` the string or `true`. It used to emit a `$regex`, which MongoDB applies to each element, so `{ owners: { $contains: 'u1' } }` matched a stored `['u10']` and `{ tags: { $contains: 'red' } }` a stored `['redwood']`. `$notContains` is the exact complement, and still admits a row with no value. A scalar column, and a field whose declaration the driver does not hold (an object never synced, a standalone `translateFilter` call), keep the substring `$regex`. + - **`@objectstack/formula`: `matchesFilterCondition` asks membership of a JSON-stored column.** When the caller supplies `options.fields` and it names the column, the declaration decides: membership on a JSON-stored column, substring on any other. Otherwise the stored value decides: an array asks membership, anything else substring. A stored array used to fail `$contains` and pass `$notContains` whatever it held. + - **The RLS write check, which evaluates a policy with this function, moves with it.** Under a `check` such as `record.tags.contains('x')` on a multi-valued field, a write whose post-image holds `['x']` (a row the same policy's read shows) is now admitted; it was refused `PERMISSION_DENIED` / 403. `['xy']` stays refused, and the read hides it. + - A scalar written to a declared multi-valued field is judged as written, before the write door wraps it in a list. So `tags: 'xy'`, which the check used to admit while the read hides the stored `['xy']`, is now refused 403. And `tags: 'x'` is now refused 403 too, although the read shows the stored `['x']`. Send the list, `tags: ['x']`. + - **`@objectstack/spec`: docblock only, in the shipped `src/data/filter.zod.ts`.** The three pointers to the deleted `SqlDriver.calendarDayUpperBoundRewrite` / `calendarDayBetweenRewrite` now name the shared `lowerFilterCondition` at the seams, and the `FILTER_OPERATORS` `$contains` implementation-status list gains `driver-mongodb` and `formula`. No schema, type or export changes. + - No exported name changes. +- 05be352: fix(formula,plugin-security): the refusal of a field-to-field comparison across comparison classes now leads with its remedy, so the remedy reaches REST callers (#20869) + + Clause-②: no + + A row-level policy that compares two fields of no shared comparison class (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object) is refused with `INVALID_FILTER` / 400. The REST door keeps a 4xx message under 500 characters by cutting it to its first 499 characters plus an ellipsis. Both messages for this refusal put the remedy last, so the remedy was always cut off, and a caller read the diagnosis but never the fix: + + - The record matcher's message (`@objectstack/formula`, raised by the RLS write check on an insert or update through `/data`) was 972 characters, with the remedy starting at character 825. + - The explain engine's message (`@objectstack/plugin-security`, answered by `GET` / `POST /api/v1/security/explain`) put the remedy after the policy names and the diagnostic. Those have no length limit, so the message was 601 characters with a short policy name and longer with longer names. + + Both messages now start with the remedy. It is the same sentence as before and has only moved: + + - The record matcher's message is 494 characters and reaches the wire whole. In order it says: the remedy; that the two columns share no class, and which classes exist; why the comparison is refused; and why the columns are not named. It still names no column, operator or policy; the server log names them. + - The explain engine's message starts with the remedy, then names the policy and both columns, then gives the reason. Whatever the names' length, the remedy sits in the first 125 characters. With long names the REST door may cut the reason at the end. + + Unchanged: the error code (`INVALID_FILTER`), the status (400), which comparisons are refused, the refusal a find answers with (driver-sql's read refusal, 383 characters, which already reached the wire whole), and every other refusal. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index 4349979e225..d897568b05a 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "main": "dist/index.js", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index fe12dec2475..dc1bb1f23a8 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,460 @@ # @objectstack/lint +## 17.6.0 + +### Minor Changes + +- b616c0a: Authoring a key whose liveness-ledger verdict is `dead` now draws a `liveness-dead-property` warning, and a `live-elsewhere` key a `liveness-live-elsewhere-property` warning, with no per-row `authorWarn` opt-in: the verdict itself is the warning. Before this, both rule ids were exported and never produced, because no shipped `dead` or `live-elsewhere` row opted in. + + Clause-②: no + + **Which keys warn.** A ledger row warns when its status is `dead`, `live-elsewhere` or `experimental`, or when it sets `authorWarn: true` (still the only way a `planned` row warns). Among authorable keys in the metadata types the rule walks, four are `dead` today: a view container's own `name` and `label` (the `defineView` container, not `list.label`), and a permission set's `rowLevelSecurity[].label` and `rowLevelSecurity[].description`. No walk visits `manifest`, `connectors` or realtime subscriptions, so their rows still warn nobody. That includes `manifest.runtime`, the one `live-elsewhere` row. + + **The hint.** A row that warns only because of its `dead` or `live-elsewhere` verdict shows its `authorHint`, else the verdict's default hint: "Remove it — it is declared in the spec but not consumed at runtime." for `dead`. It never shows the ledger's internal `note`. Rows that opt in with `authorWarn`, and `experimental` rows, show exactly the hint they showed before. + + **Retired keys.** A `retiredKey` tombstone keeps its `dead` row. Every command that parses (`os validate`, `os build`, the runtime publish gate) still refuses the key first, so it gets no second report. `os lint` does not parse: a config it accepts without `defineStack` that carries a retired key now gets a `liveness-dead-property` warning where it got nothing. + + **What changes for a project.** Nothing is refused, and nothing changes without `--strict`. `os lint --strict` and `os validate --strict` now exit 1 instead of 0 on a stack that was otherwise warning-clean and authors a view container `label` or `name`, or a row-level-security policy `label` or `description`. A view container that carries its own `name` and `label` beside its `object` binding is one such shape: it draws two warnings per container, and under `--strict` that flips the exit. Across this repository's example apps, only `app-showcase` gains warnings: two, on one permission set's policy `label` and `description`, and no example's exit code changes. To clear the warning, delete the key: nothing reads it. +- e651556: `os validate`, `os build` and `os lint` refuse an `api` flow with no per-flow secret, the flow the automation engine already refuses to register (#20553). + + Clause-②: yes (narrowing — `os validate` / `os build` / `os lint` newly refuse a secretless `api`-bound flow; the new exported rule id `FLOW_API_TRIGGER_SECRET_MISSING` widens `@objectstack/lint`) + + + + **BREAKING** — an accept-set narrowing on three authoring commands, shipped as + `minor` under the launch-window convention (`check-changeset-no-major` refuses + `major` until GA; breaking-ness is carried by this banner and the ADR-0087 + disposition above, not by the level). A stack that declares an `api`-bound flow + whose start node carries no usable `config.secret` used to pass `os validate`, + `os build` and `os lint`; they now exit non-zero and name the flow. + **One-line fix:** set a non-blank `config.secret` on the flow's start node — or, + for a flow that is only ever started explicitly, declare `type: 'autolaunched'` + with no `triggerType: 'api'`. + + `@objectstack/lint` gains one rule id, `flow-api-trigger-secret-missing`, at `error`, emitted by a new exported rule, `validateFlowApiTriggerSecret`, in the `validate-flow-trigger-readiness` family. It names a flow whose binding resolves to the inbound `api` trigger when that flow's start node carries no usable `config.secret`. A usable secret is a string that is non-empty after trimming. The rule fires for a missing, blank or non-string secret, and for an `api` flow with no start node. + + **Why.** ADR-0041's `trigger-api` acceptance criteria require a per-flow secret with HMAC verification. Since 17.5.0 the automation engine refuses such a flow in `registerFlow`, whatever its `status`: the `/automation` write doors answer `400`, and a boot skips the flow with a warning. `ApiTrigger.start()` also refuses to arm it. `os validate` builds neither, so it answered `✓ Validation passed` for a flow no runtime would register. It now exits non-zero and names the flow. + + **Which flows count as `api`-bound.** The rule uses the engine's own binding, `deriveTriggerBinding`. An array-form record `triggerType` goes to the record-change trigger first. Otherwise the flow gets the kind `resolveFlowTriggerKind` answers, which is a flow declaring `type: 'api'` or a start-node `triggerType: 'api'`. The engine gives the record-change, time-relative and schedule triggers precedence over `api`. So a `type: 'api'` flow whose start node also carries a `record-*` token, a `timeRelative` descriptor or a `config.schedule` binds that other trigger. The engine never asks that flow for a secret, and the rule stays silent on it. + + **Where the refusal surfaces.** `os validate`, `os build` and `os lint`. The runtime metadata publish gate is deliberately not covered yet (#20611): it judges a `/meta` save before the stored secret the flow read path withholds is restored, so for now a secretless flow saved there is still stored, and the engine then refuses it at registration. + + **Fix.** Set a non-blank `config.secret` on the flow's start node, and sign each post with it in the `x-objectstack-signature` header. A flow that is only ever started explicitly and never receives inbound posts is `type: 'autolaunched'`, with no `triggerType: 'api'` on its start node, and it needs no secret. + + `validateFlowApiTriggerSecret` and `FLOW_API_TRIGGER_SECRET_MISSING` are exported from `@objectstack/lint`. +- 31ed067: The runtime metadata publish gate refuses an `api` flow with no per-flow secret, and reads a secret the flow read path withheld as present (#20611). + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on the runtime metadata write door, + shipped as `minor` under the launch-window convention (`check-changeset-no-major` + refuses `major` until GA; breaking-ness is carried by this banner and the ADR-0087 + disposition above, not by the level). An `active` save through `/meta` of an + `api`-bound flow whose start node carries no usable `config.secret` (a + `PUT /api/v1/meta/flow/:name`, or the publish of such a draft) used to be stored; + the automation engine then refused to register it (`400` on the `/automation` + doors, a skip with a warning at boot). It is now refused at the save with + `422 INVALID_METADATA`, the issue naming `flow-api-trigger-secret-missing` at the + start node's `config.secret`, and nothing is stored. A draft save is still + accepted; its publish is refused the same way. + **One-line fix:** set a non-blank `config.secret` on the flow's start node — or, + for a flow that is only ever started explicitly, declare `type: 'autolaunched'` + with no `triggerType: 'api'`. + + **What does not change: a signed flow's round trip.** Every served flow definition withholds the start node's `config.secret`, so a body saved back after a read arrives without it, and the save restores the stored secret only after every gate has run, so that no gate handles a restored credential. The gate is now told WHERE the save will restore a credential from the stored row: those positions only, never the values. `flow-api-trigger-secret-missing` reads a secret that was withheld and is stored as present, and one that is absent and not stored as missing. So a GET → edit → PUT of a signed flow, and the first save of a code-authored flow whose secret is in the app's source, keep passing and keep their secret. An explicit empty `config.secret` is the author's own value and is refused as blank. + + `@objectstack/lint`: + + - `validateFlowApiTriggerSecret` now runs on the runtime publish gate too (`surfaces` `['cli', 'runtime-publish']`, `runtimeTypes: ['flow']`). Its `surfaceReason` is gone. + - `AuthoringRuleContext` gains an optional `restoredCredentialPaths`: a `ReadonlySet` of stack-relative positions in the rules' own finding-path spelling (`flows[0].nodes[1].config.secret`). Only the runtime publish gate sets it; `runAuthoringRules` never forwards it, so `os validate`, `os build` and `os lint` judge the author's own values as before. + - `runRuntimeAuthoringRules` accepts an optional `restoredCredentialPaths`: item-relative dotted positions in the `@objectstack/spec/kernel` redactor registry's `redactedKeys` spelling (`nodes.1.config.secret`). The gate re-spells them against the written item's place in its snapshot. + - `validateFlowApiTriggerSecret(stack, options?)` accepts an optional `{ restoredCredentialPaths }`, and treats a listed start-node secret position as present. + + `@objectstack/metadata-protocol`: `saveMetaItem` hands the runtime authoring gate the positions its own credential carry-forward will fill, computed from the same stored body. This costs one indexed `sys_metadata` read on an `active` save of a type with a registered redactor (`datasource`, and `flow` where the automation plugin registers one), and nothing for any other type or for a draft save. The carry-forward itself is unchanged and still runs after every gate. The draft→active promotion judges the stored draft row, which already holds what that draft's save carried forward, so it needs no such positions. +- ed54768: A credential typed as a literal into a flow position that every flow reader is served now draws one `flow-credential-literal` warning at `os validate`, `os build`, `os lint` and the runtime publish gate, and the spec describes of those positions route an outbound credential to a declarative connector's `credentialRef` (#20654). + + Clause-②: no + + **Why.** A flow definition is served, as authored, to every member who can read flows. The flow read path withholds the credential slots the spec declares, but it cannot withhold a value inside an open map or a url, because it cannot tell a credential there from an ordinary value. The supported home for an outbound credential is a declarative connector: its `auth: { type, credentialRef }` names a secrets-layer reference that is resolved at boot and never stored in metadata. + + **What the warning covers.** An `http` node's `config.headers` entry, a query parameter of an `http` node's `config.url`, and a node's `connectorConfig.input` at any depth, including nodes inside `try_catch`, `loop` and `parallel` regions. A value draws when it is a non-blank string with no `{…}` template, and either its name reads as a credential (`Authorization`, `Cookie`, `x-api-key`, a name carrying `token`, `secret`, `password` and similar) or it opens with an auth scheme (`Bearer`, `Basic`, `Token`, `Digest`, `ApiKey`) followed by a value. A `{variable}` template is resolved per run and draws nothing. + + **What it does not do.** It never refuses: every finding is a `warning`, and a save, validate, build or lint that passed before still passes (`--strict` promotes it, as it promotes every warning). It never echoes the value it names. Nothing is withheld on any read. + + **Fix, by where the credential sits.** Declare a `connectors:` entry with a `provider` and call it from a `connector_action` node. A header credential goes to `auth: { type: 'bearer', credentialRef }`, or to `auth: { type: 'api-key', headerName, credentialRef }` for a key in a named header. A key in the url's query string goes to `auth: { type: 'api-key', paramName, credentialRef }`. On a connector node, drop the credential from `input`: the connector authenticates through its own `auth.credentialRef`. + + `@objectstack/lint` exports the rule `lintFlowCredentialLiterals`, its id `FLOW_CREDENTIAL_LITERAL`, and the one predicate it asks, `isCredentialShapedLiteral(name, value)`. In `@objectstack/spec`, only the descriptions of `HttpConfigSchema.headers` and a flow node's `connectorConfig.input` change; no shape changes. +- 5e470f8: fix(lint)!: `os validate`, `os build` and `os lint` refuse a dataset dimension over a JSON-stored field, the group key the analytics door already refuses at query time + + Clause-②: yes (narrowing) + + + + **BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail, and so can a runtime dataset save (Studio, REST `/meta`, MCP), which runs the same rule. A dataset dimension is a group key, and the analytics door refuses a query that groups by a JSON-stored column with `400 INVALID_FIELD` before any SQL is built, so such a dimension could be declared but never served. The new rule `dimension-json-stored-field-refused` (gating, `error`) refuses it where the author writes it. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is removed. The package entry exports the new id as `DIMENSION_JSON_STORED_FIELD_REFUSED`, beside `MEASURE_AGGREGATE_FIELD_TYPE_REFUSED`, and the `rule` member of `DatasetMeasureAggregateFinding` gains it. + + **What is refused.** A dimension whose `field` resolves, on the dataset's object or across its join chain, to a field declared with a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) or a multi-value declaration (`multiselect`, `checkboxes`, `tags`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`). The two classes are `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES` and `isMultiValueField`, the predicates the analytics door reads. + + **What an author sees now.** The finding names the dataset, the dimension, the field, the object that declares it and its declaration, and says the analytics door refuses every query that groups by it. It names the route: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own; for a multi-value field, filter by one member with `$contains` in a record query, one query per member. It is located at `datasets[N].dimensions[M].field`, name-keyed on the runtime wire. + + **Unchanged.** A dimension over any other field, a single-value `select` or `lookup` included; a dimension whose field does not resolve (`dataset-field-unknown` reports that) or declares no type; a dataset over an object this stack does not define; measures, filters and every other position. A cube dimension (`analyticsCubes`) is not judged: no authoring rule reads cubes. +- 5e470f8: fix(lint)!: a dataset `count_distinct` measure over a field declared `multiple: true` is refused by `measure-aggregate-field-type-refused`, as the compile leg and the engine already refuse it + + Clause-②: yes (narrowing) + + + + **BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail, and so can a runtime dataset save, which runs the same rule. `measure-aggregate-field-type-refused` reads the field's declaration, not its type alone: `count_distinct` over a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true` is refused, because that field is a list stored as JSON and no two backends compare such values for equality alike. The dataset compile leg already answers the pair `400 DATASET_INVALID`, and the engine's `count_distinct` door answers it `400 INVALID_FIELD`. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What an author sees now.** The finding names the measure, the field, the object and the declaration with its flag (`select` with `multiple: true`), and says the aggregate accepts its row's types, none of them with `multiple: true`. The hint names the aggregates the declaration does accept, read from the same predicate: `count`. + + **What to write instead.** `count` over the field, or `count_distinct` over a field that stores one scalar value. To count the records holding one member, filter by it with `$contains` in a record query. + + **Unchanged.** `count_distinct` over the same types without the flag; `count` over any field; every other aggregate, whose rows accept no multi-capable type and whose verdicts therefore do not move; and the skips the rule already had. +- ce8a6d2: `os validate`, `os build` and `os lint` now warn on a dashboard widget `options` key that no renderer reads, by name, as `unconsumed-widget-option` — the check the SDUI page save gate already ran on a `dashboard` node, now run over dashboard metadata (`*.dashboard.ts`, `defineStack({ dashboards })`). + + Clause-②: yes + + **What is flagged.** Every key in a dataset-bound widget's `options` outside the read set `CONSUMED_WIDGET_OPTION_KEYS` from `@objectstack/sdui-parser`: `dateGranularity`, `description`, `limit`, `sortBy`, `sortOrder`, `stageOrder`. `DashboardWidgetOptionsSchema` stays open (`.passthrough()`), so such a key still parses; it just stops being silent. Typical cases are `icon`, `columns`, `format`, `currency`, `color`, `suffix`, `showLegend` and `horizontal`, none of which styles anything on a widget bound to a dataset, and a misspelled declared key such as `sortDirection` or `granularity`. The finding is reported at `dashboards[N].widgets[M].options`, and its message names the key. + + **Where presentation goes instead.** A number's format and currency are the dataset measure's `format` and `currency`; a tile's accent is the widget's `colorVariant`; a chart's look is the widget's `chartConfig`. + + **Same check, same level, same exemptions.** The rule is `validateDashboardWidgetOptions`, exported from `@objectstack/lint` and registered for all three commands. It calls `checkDashboardWidgetOptions` from `@objectstack/sdui-parser` and keeps its `warning` level and `code`. Widgets with no `dataset`, legacy `component` widgets and widgets carrying `suppressWarnings: ['unconsumed-widget-option']` are not flagged. It does not run on the Studio/REST/MCP publish path. + + **What changes for a project.** Nothing is refused, and nothing changes without `--strict`. `os validate --strict` and `os lint --strict` now exit 1 instead of 0 on a stack that was otherwise warning-clean and writes such a key. Across this repository's example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`), no dashboard writes one: zero findings, and no example's exit code changes. To clear the warning, delete the key, or move the intent to the home named above. +- ee42f00: `os validate`, `os build` and `os lint` now check every keyed child of an action's translation entry against what the action declares, under both `objects.OBJECT._actions.ACTION` and `globalActions.ACTION`. Before this, only `params.NAME` was checked. + + Clause-②: yes + + **What is refused.** Two keys are new `translation-target-unknown` errors, the same code and level an undeclared `params` key already gets: + + - `outcomeMessages.OUTCOME` when the action's own `outcomeMessages` does not declare that outcome, or declares no `outcomeMessages` at all. `translateAction` overlays only the outcomes the action declares, so such copy is never shown. + - `resultDialog.fields.PATH` when no entry of the action's `resultDialog.fields[]` has that literal `path`, or the action declares no `resultDialog` or a dialog with no `fields`. The label lookup is keyed by each declared field's `path`, dots included, so such a label is never shown. The finding's config path quotes a dotted key as one member (`resultDialog.fields["client.ghost"]`). + + **What is warned.** `params.NAME.options.VALUE` under a declared param is judged against that param's inline `options[].value`. It is a `translation-option-key-unknown` warning, the code and level a field's `options` key already gets, and it names the stored value when the key is a display label. An options map under a param with no inline options and no `field` is warned once, because nothing reads it. A field-backed param with no inline `options` inherits its list from the field when the dialog renders, so its option keys are not judged. + + **What changes for a project.** A bundle that carries one of the refused keys now fails `os validate` with exit 1 instead of passing, and `os build` refuses it. The fix is to rename the key to a declared outcome or result-field `path`, declare the outcome or field on the action first, or delete the key. The option-key warning changes an exit code only under `--strict`. The four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and the bundle shipped with `@objectstack/platform-objects` produce no finding on any of these keys, so none of their exit codes change. + +### Patch Changes + +- f29c83d: Provenance comments in `@objectstack/lint`'s authoring-rule registry were re-anchored + + Five comment and docblock lines in `src/authoring-rules.ts` that cited tracker + numbers which no longer resolve on GitHub now cite the commit in this + repository's history that decided the matter, and keep saying what was + decided. Comments only: no rule id, finding message, hint, severity, type or + runtime behaviour changes. +- c6b37cd: The `react-prop-deprecated` warning states its reason in words instead of citing a tracker number + + The finding `validateReactPageProps` reports for a react-page prop written in a + deprecated react-tier spelling used to end by pointing the author at an issue + number that no longer resolves. It now says what that decision was, in the + sentence being read: the react tier converges on the metadata-tier vocabulary, + so the deprecated spelling keeps working through the deprecation window and is + removed after it. The block tag, the prop and the canonical metadata-tier + spelling it names are unchanged, and so are the rule id, the `warning` + severity, the hint and every other finding. +- 975b248: fix(objectql,spec)!: a `groupBy` on a multi-value field and a `count_distinct` on a JSON-stored field are refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver, and the aggregate × field-type table stops accepting `count_distinct` over the JSON-stored types + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, in two positions, on every driver and for every caller that reaches the engine (the REST query door, a flow or hook, and the analytics strategy that lowers a cube query onto `engine.aggregate`). Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + - A `groupBy` entry that names a **multi-value** field: an inherently-multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. Both entry spellings are judged, the field name and the `{ field }` object. + - A `count_distinct` aggregation over a **JSON-stored** field: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently-multi option type, or a multi-capable field declared `multiple: true`. + + **BREAKING** (`@objectstack/spec`): `AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct` no longer lists the ten JSON-stored types (the structured-JSON seven and `multiselect`, `checkboxes`, `tags`), so `isAggregateCompatibleWithFieldType('count_distinct', type)` answers `false` for them. Every reader of the table refuses those pairs now: the dataset-measure lint rule (`measure-aggregate-field-type-refused`, run by `os validate` and at a runtime dataset save), the analytics dataset compile leg (`400 DATASET_INVALID`), and the engine door above. The `count` row is unchanged. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, `groupBy[0].field`, or `aggregations[0].field`), the field and its declaration, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps. For a multi-value field the route is to filter by one member: `where` with `$contains` on the field, one query per member. For a structured-JSON field it is to store the part you count in a field of its own, or to count rows with `count`. The thrown error carries `field`, `fields`, `object` and `param` (`groupBy` or `aggregations`). + + **Why a refusal.** Every SQL driver stores these values in a JSON column, and the drivers share no meaning for one as a group key or a distinct key. Measured through `POST /api/v1/data/:object/query` over three rows: grouping by any of the eight multi-value declarations answered one group per array on the in-memory driver, one group per serialized array on SQLite, and 500 `DATABASE_ERROR` on PostgreSQL 16. `count_distinct` over any structured-JSON or multi-value field answered 3 on the in-memory driver (equal values counted apart), 2 on SQLite (serialized text compared), and 500 on PostgreSQL (no equality operator for `json`). No example app and no published stack groups by a multi-value field or counts one distinct, so no meaning is defined for either here. + + **What to write instead.** A dataset measure or a query that counted a JSON-stored field distinct: use `count` over it, or store the scalar part you meant to count in a field of its own and `count_distinct` that field. A grouping by a multi-value field: filter by each member with `$contains` and count. + + **Who is affected.** A caller that grouped by a multi-value field, or counted a JSON-stored field distinct, on the in-memory driver or on SQLite and read the answer as a real one; on PostgreSQL both were already a 500. A dataset whose measure pairs `count_distinct` with a JSON-stored field is refused by the lint rule and the compile leg. + + **Unchanged.** (Two shapes the structured-JSON `groupBy` entry of this same release lists as unchanged are narrowed here: a `multiple: true` select as a group key, and `count_distinct` over a structured-JSON field. This entry is the later word on both.) A `groupBy` or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; the `having`, filter and sort positions; and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. + + `@objectstack/lint`: the dataset-measure refusal's hint no longer says `count_distinct` accepts every type. + + `@objectstack/service-analytics`: the dataset compile leg's refusal of a `count_distinct` measure over a JSON-stored field says why it diverges (the drivers compare the values for equality three ways) and prescribes `count`, or a scalar field for the part being counted; its other refusals no longer say `count_distinct` accepts every type. +- b84b240: feat(cli)!: `objectstack validate` and `objectstack build` refuse a field whose `picklist` names no picklist the stack declares, and lint R8 counts `picklist` as an options source (#20825) + + Clause-②: no (narrowing — `objectstack validate` / `objectstack build` newly refuse a field `picklist` that names no picklist the stack declares; the R8 change removes a false-positive warning and widens no accept set of its own) + + + + **BREAKING** — an accept-set narrowing on two authoring commands, shipped as + `minor` under the launch-window convention. A stack with a select field whose + `picklist` names no picklist the stack declares — `picklist: 'industy'` beside a + `picklists: [{ name: 'industry', … }]` — used to pass `objectstack validate` and + `objectstack build` (which wrote the artifact). Both now exit 1 and name the field + and the list it names (`picklist-reference-unknown`). + **One-line fix:** correct `picklist` to a list the stack declares, or declare the + list it names (`picklists: [{ name, label, options }]`, or a `*.picklist.ts` file the + stack imports). + + **Which references are judged.** The ones the load path registers: the top-level + `objects` and `objectExtensions` of a one-package stack, or each `packages[]` + entry's own. A reference resolves against every picklist the stack declares, + including one a sibling package in the same artifact owns. + + **A list from a package outside the stack is reported, not refused.** When the + package declaring the field lists a `manifest.dependencies` entry the stack does not + carry, the list may live there, and these commands cannot read it. That reference is + an `info` notice (`picklist-reference-unverified`) in `warnings` and on the console, + naming the field, the list and the dependencies — never a failure, not even under + `--strict`. + + **Lint R8 (`field/select-missing-options`)** no longer reports a select, multiselect + or radio field that names a `picklist`: the picklist is its options source. The + warning it used to give pointed at `options`, which a field naming a `picklist` + cannot add — the field schema refuses the two together. A select with neither still + warns, and its fix now names both sources as alternatives. +- a29a0ea: feat(spec)!: an `object-master-detail-form` block's detail entries are a strict shape, and their columns are the inline grid column contract (#20928) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. The console's master-detail grid reads one column shape from three carriers: a relationship field's `inlineColumns`, a form view's `subforms[].columns`, and an `object-master-detail-form` page block's `details[].columns`. The first two were judged; the third was `z.array(z.unknown())`. + + **`@objectstack/spec`** + + - **`ComponentPropsMap['object-master-detail-form'].details`** is now an array of strict detail entries: `childObject` (required), `relationshipField`, `columns`, `formFields`, `inlineMode` (`grid` | `form`), `amountField`, `sortField`, `totalField`, `title`, `minRows`, `maxRows` and `addLabel` — the keys the console's `MasterDetailForm` reads off an entry. An unknown key is named, with a rename for the near-misses a form view's `subforms[]` entry also answers (`foreignKey` → `relationshipField`, `object` → `childObject`, …). + - **`details[].columns`** references `InlineGridColumnSchema`, the strict, name-keyed column the other two carriers take. Every rule the column schema holds applies here too, with its own message: an unknown key is named; the retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`; a column without `name` is refused; `scale` on a column declaring `type: 'currency'` is refused with the currency ruling's remedy. Page-component `properties` is read by the component-props gate, so `objectstack validate`, `build` and `lint` report these as advisory `component-props-unknown-key` / `component-props-invalid` findings; a stored page still saves and loads, because `properties` is not parsed on the metadata save or load path. + - **`defineStack`'s cross-reference check** now reaches the block wherever a page carries it (a region, a container's children, a slot) and judges a detail column that declares no `type` as the type it renders, as it already does on the other two carriers: an identity-only column over a `currency` field of the entry's `childObject` that carries `scale` is refused with the column schema's own message (`STACK_CROSS_REFERENCE_INVALID`, 422). A child object the stack does not declare, a column naming no field of it, and a column the column schema refuses on its own (left to the component-props gate) are not judged there. + - **New type `ObjectMasterDetailFormPropsParsed`** — the post-parse shape of `ObjectMasterDetailFormProps`. The two now differ, because a column's `readonlyWhen` / `requiredWhen` bare-string predicate normalizes to an Expression envelope at parse. + + **`@objectstack/lint`** + + - **`field-no-consumers`** reads an `object-master-detail-form` detail entry as the child collection it is: a column `name`, `amountField` and `relationshipField` credit the field of the entry's `childObject`, `totalField` the parent's, and an entry with no `columns` credits the columns the child derives. A child field drawn only by a master-detail block's grid was reported inert. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `details: [{ title: 'Lines' }]` | `details: [{ title: 'Lines', childObject: 'invoice_line' }]` | + | `details: [{ childObject: 'invoice_line', columns: ['product', 'quantity'] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'product' }, { name: 'quantity' }] }]` | + | `details: [{ childObject: 'invoice_line', columns: [{ field: 'quantity' }] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'quantity' }] }]` | + | `details: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency', scale: 2 }] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency' }] }]` | + | `columns: [{ name: 'amount', scale: 2 }]` where `amount` is a `currency` field of the entry's `childObject` | `columns: [{ name: 'amount' }]` | + | a detail entry or column carrying a key its shape does not declare | the entry or column without that key | + + The one-line fix: give every detail entry its `childObject`, write each column as `{ name, … }` using only the keys a relationship field's `inlineColumns` accepts, and delete `scale` from any column that renders as a currency column, whether it declares `type: 'currency'` or takes it from a `currency` child field. Nothing replaces `scale` there: the currency's ISO 4217 minor unit decides the displayed decimals. + + ## Who is affected, measured + + On `origin/main` `ebdb6f2aca`: one authored `object-master-detail-form` block in the examples (the showcase project workspace, one entry `{ title, childObject, addLabel }`, no columns), which parses unchanged, and one documentation example whose three bare-string columns are rewritten as `{ name }` columns in this change. Zero `field`-keyed detail columns. Deployed metadata was not measured. +- 3693a1b: `field-no-consumers` no longer calls a field inert when an inline grid column names it + + `os validate`, `os build` and `os lint` warned that a child object's field was inert ("no site of any kind names it") when the only thing naming it was an inline master-detail grid column, such as `{ name: 'quantity' }`. The warning told an author to delete a field the grid draws. A column's `name` is now read as a reference to the child object's field, on both carriers of the column: + + - a relationship field's `inlineColumns`. The field sits on the child object and its `reference` names the parent, so the column names a field of the object that declares the relationship field. The grid is drawn only when that field sets `inlineEdit`. Without it, the columns draw nothing, and the field is reported `carrier-only` with the column listed as a site a removal must clean. + - a form view's `subforms[].columns`, on the view's `form` and on every `formViews` entry. The column names a field of the entry's `childObject`, not of the object the view is bound to. + + `name` anywhere else is still a literal and never a field reference. The rule id, the `warning` severity and the finding's shape are unchanged. The message now also lists an inline grid column among the consumers, and an `inlineColumns` entry on a field without `inlineEdit` among the carriers. +- e07566b: `deriveInlineGridColumns` (`@objectstack/spec/data`) derives the default columns of an inline master-detail grid, and `field-no-consumers` stops calling two kinds of in-use child field "inert" (#20951). + + Clause-②: yes (widening) + + - **`@objectstack/spec`.** New exports from `@objectstack/spec/data`: `deriveInlineGridColumns(def, { relationshipField?, exclude?, maxColumns? })`, its element type `DerivedInlineGridColumn`, and `DEFAULT_MAX_INLINE_GRID_COLUMNS` (`6`). The function answers which child fields an inline grid draws when its author listed no columns: a relationship field with `inlineEdit` and no `inlineColumns`, or a `subforms` entry with no `columns`. It returns identity-only entries (`{ name }`, plus `defaultHidden: true` on columns past the visible budget, which collapse into the column chooser and are never dropped), in the child's field order, skipping system, audit, tenancy, ownership and sort-position names, the relationship field, `system` / `readonly` / `hidden` fields and the types a grid cell cannot edit. It is the renderer's current rule, reproduced exactly; the renderer hydrates each column from the child field. No schema accepts anything new or refuses anything new. + - **`@objectstack/lint`.** `field-no-consumers` now reads a `subforms` entry's `amountField` and `relationshipField` against the entry's `childObject`, and keeps `totalField` on the parent. It also credits the columns of a derived inline grid through `deriveInlineGridColumns`. Before, `os validate` warned that the child's summed amount column, the subform's relationship field and every derived grid column were inert, and credited a same-named parent field in the amount column's place. A field the derivation leaves out (for example a `hidden` one) is still reported. +- 327391c: fix(lint): a liveness finding's fix text is the row's `authorHint`, else the verdict's default hint, and never the row's internal ledger `note`, for every row class. Before this, a row that opted in with `authorWarn`, and an `experimental` row, with no `authorHint` printed its `note` as the fix text: on `app-showcase`, the two `liveness-planned-property` findings for `externalSharingModel` printed a 728-character maintainer note that cites a tracker id. They now print "Keep it — a consumer is being built against this property; it has no runtime effect yet." No rule id, message, severity or exit code changes, and a row that has an `authorHint` prints it exactly as before (#21096) + + Clause-②: no +- 3dc33b2: **BREAKING** — an anonymous public form no longer offers record search. The form field's `publicPicker` block (`view.form.sections[].fields[].publicPicker`: `displayFields`, `maxResults`, `filter`, `object`) is removed, and the anonymous lookup route `GET /api/v1/forms/:slug/lookup/:field` is deleted. A public form's `lookup`, `master_detail` and `user` fields are now always left off its anonymous rendering, whatever the form declares. + + Clause-②: yes (narrowing) + + Retired immediately (ADR-0087 D2), with no alias window: the maintainer's ruling reverses the earlier one that had declared the key. Mainstream web-to-lead forms do not let an anonymous visitor search records either, and no example, template, plugin or first-party UI declared or called the picker. + + ## FROM → TO + + | you wrote (17.5 and earlier) | write instead | + | --- | --- | + | `{ field: 'account', publicPicker: { displayFields: ['name'], maxResults: 10 } }` on a public form | delete the `publicPicker` block — the field is left off the anonymous rendering anyway | + | a public form whose visitors chose from a short, fixed list of records | a `select` field with static `options` listing the choices | + | a public form whose visitors had to pick an existing record | the same form behind sign-in (an internal form), where the lookup field searches with the signed-in user's own access | + | a client calling `GET /api/v1/forms/:slug/lookup/:field` | nothing to call: the path is no longer registered and answers what any unregistered path answers (`404 ENDPOINT_NOT_FOUND`) | + + **The one-line fix:** delete the `publicPicker` block; an anonymous public form no longer offers record search. Use a `select` field with static `options`, or put the form behind sign-in. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`FormFieldInput` types the key `never`), and the parse — `defineView()`, `defineStack({ views })`, `os validate`, `PUT /api/v1/meta/view/:name` — refuses it at `…sections[N].fields[N].publicPicker` with the prescription: + + > `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + **What a REST client sees.** The two error codes only that route produced, `LOOKUP_NOT_PUBLIC` and `LOOKUP_TARGET_MISSING`, leave the error-code ledger with it. `GET /api/v1/forms/:slug` and `POST /api/v1/forms/:slug/submit` are unchanged apart from the unconditional strip above. + + ## The retirement kit + + - **A `retiredKey()` tombstone on the form field**, so the parse carries the prescription instead of a bare unknown-key verdict. The block's own schema and its two types go with it: `FormFieldPublicPickerSchema`, `FormFieldPublicPicker` and `FormFieldPublicPickerParsed` are no longer exported, and `ui/FormFieldPublicPicker` is no longer published as a JSON Schema. + - **The D2 conversion `form-field-public-picker-removed`** (protocol 18, retired from the load path) deletes the key from every form field of every form payload — `sections[]`, `groups[]`, top-level `fields[]` and nested rows. Its D3 record is the semantic entry `form-field-public-picker-retired`, which asks the author how a visitor should now choose. + - **`@objectstack/rest`:** the lookup route and its filter-lowering helper are deleted, and the resolve route's strip of lookup / `master_detail` / `user` fields no longer has an opt-in. + - **`@objectstack/lint`:** the preset-comparand rule no longer reads a picker's `filter` (its claiming reader for that position went with the key). + + ## What an operator with a STORED form sees + + A `sys_metadata` view row saved before this release may still carry the key. Nothing breaks at read: the conversion replays on rehydration and strips it, so the view is served canonical and parses, and the field stays off the anonymous rendering either way. `os migrate meta --stored` lists those rows, and `--apply` rewrites them. + + +- aa23e2c: Provenance comments in `@objectstack/lint` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no rule id, finding message, hint, severity, type or runtime behaviour + changes. +- 315888d: feat(spec): one list of page-component slot positions, derived from the component rows and read by every page walk — `page:card`'s `footer` is now walked by all three (#20940) + + The platform has three walks that descend into a page component's `properties` bag, and each kept its own list of where child components hang: the ADR-0087 conversion walker (`children`, `body`, `footer`, `items[].children`), `@objectstack/lint`'s `walkPageComponents` (the same four) and the exported `walkAddressedPageComponents` (`children`, `items[].children`). So a node in a card's `footer` — a declared, rendered slot ("Card footer components (slot)") — was judged by `os lint` and skipped by every consumer of the exported walk: `translatePage` left its copy untranslated, `os i18n extract` offered no key for it, and objectui's validator passed it unjudged. + + **`@objectstack/spec` — new exports `pageComponentSlotPositions()` and `PageComponentSlotPosition` (`@objectstack/spec/ui`).** The component rows now mark each composition slot at its declaration, and `pageComponentSlotPositions()` derives the one list from `ComponentPropsMap`: `children`, `footer` and the panel position `items[].children`, plus the tombstoned `body` flagged `retired: true`. The marker changes nothing about the schema it marks — the parse, the JSON Schema and the authorable surface are unchanged. The list is derived on first call and memoized, never at import. `minor` because the package's public surface grows by these two exports. + + **`walkAddressedPageComponents` descends `properties.footer`.** It reads the list's authorable entries, in the list's order (`children`, `footer`, then `items[].children`); signature and return shape are unchanged. What follows from it: + + - `translatePage` translates the copy of a component in a card footer through `pages..components.`, like any other nested component. + - `os i18n extract` offers those keys, and `os i18n check` counts them, for a stack whose card footers hold components with an `id` and copy. + - objectui's validator, which judges the nodes this walk visits, now judges a card footer's nodes. + + `page:card.body` stays undescended, as #5775 ruled: it is not an authorable spelling. + + **The conversion walker reads every entry, the retired one included.** Its reach does not change: it descends `children`, `body`, `footer` and `items[].children`, as before. Stored documents still carry `body`, the renderers still draw it, and a conversion that runs before `page-card-body-to-children` meets the sub-tree there. Within one component the visit order is now `children`, `body`, `footer`, then the panels. That order is observable only as the order of the notices for a component that carries both a direct slot and panels. + + **`@objectstack/lint` — `walkPageComponents` reads the list's authorable entries.** It walks `footer` as before, and it stops walking the retired `body` spelling. The walk matches by shape, so this drops a `body` array on any component, not only on `page:card`. #5775 (maintainer ruling 2026-08-06, direction A) made `children` the one composition key. The renderers keep reading `body` only as a back-compat fallback for stored documents. On `page:card` the tombstone's rename prescription still refuses `body`, and so does the thin containers' guidance; the sub-tree is judged once it sits under `children`. So the rules built on this walk no longer report findings about nodes under any component's `body` array. The conversion walker keeps reaching them for stored documents. + + **`@objectstack/cli`:** no code change. `os i18n extract` and `os i18n check` pick up the `footer` component keys through the shared walk. The extractor's object-section pass stops reading `record:details` sections under a retired `body`, through lint's walk. + + **Why no ADR-0087 ledger entry.** Nothing an author writes moves: no spec key is retired or renamed, no stored `sys_metadata` shape changes, and no conversion or migration id is touched. `objectstack migrate meta` has nothing to act on. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [a093ce3] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [5bed1f6] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [b8191f7] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/sdui-parser@17.6.0 + - @objectstack/formula@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 291eb7cda30..9fbe47d4657 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Static, build-time validation for an ObjectStack metadata graph — dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.", "type": "module", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index e3a1274bc5f..fc442096970 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,186 @@ # @objectstack/plugin-mcp-server +## 17.6.0 + +### Minor Changes + +- 3ddd3d0: fix(mcp)!: the MCP stdio transport serves a stored metadata body only as its type's read projection, and refuses to evaluate it + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what one door serves and accepts for the two stored-metadata tables (the stored row and its version history). On the MCP stdio transport, a read now carries the stored body as its type's read projection instead of the stored bytes, and a call that would evaluate the stored body is refused. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** + + - **Reads.** The stdio bridge's query and get verbs, and the record resource, serve the stored body through the same projection the generic data door and every metadata read serve: stored credential material is withheld, a body that cannot be judged is omitted, and a credential-free body is served unchanged. + - **Evaluate shapes.** A group, filter, sort or aggregate member on the stored body column is refused with `400 INVALID_FIELD` before the engine runs — the data door's code and envelope. + + **What stays answerable.** Every scalar column of the two tables is still served, filtered, sorted, grouped and counted; only the stored body column is affected, and every other object is unchanged. A member's read of these tables is refused as before. + +### Patch Changes + +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 9c7e2749dff..49e5d5c6b8a 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack as an MCP server — exposes your app's objects (and AI tools) over the Model Context Protocol (stdio + Streamable HTTP)", "type": "module", diff --git a/packages/metadata-core/CHANGELOG.md b/packages/metadata-core/CHANGELOG.md index 8cab94d404a..6fc78ee20b1 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,189 @@ # @objectstack/metadata-core +## 17.6.0 + +### Patch Changes + +- 3572916: The shared engine case tables and the published contract suites in metadata-core no longer cite tracker numbers in their case labels; each label states its case in words + + Clause-②: no + + Several labels these tables and suites ship ended with an issue-tracker number where the case belonged. A + test driven from them printed that number as part of its name, and a failing assertion quoted it as the + reason. The number goes; where the label did not already say what the case is, it now does. + + - `ENGINE_DELETE_DISPATCH_CASES`, `ENGINE_UPDATE_DISPATCH_CASES` and `ENGINE_FINDONE_PREDICATE_CASES`: + the `what` labels of 22 rows. Among them, the compare-and-set rows now say the by-id path would drop the + CAS guard; the payload-id rows say which declared `where.id` would be silently dropped; and the falsy + `where.id` boundary says it is a scalar, so neither the different-row refusal nor the non-scalar refusal + applies. + - `@objectstack/metadata-core/testing`: the repository contract suite's `serialized-form identity` group + title, and two `why` texts of `OBJECT_SCHEMA_MASK_CASES` (the empty-readable-set refusal, and the + write-capable exemption, which now names the schema write gate, `manage_metadata`). + + Text only: no case is added, removed or re-ordered, and no `options`, `data`, `expect`, `expectId`, `id`, + `readable` or `context` value moves. A suite that selects or skips these cases by their label text (a + `-t` filter, a skip list) needs the new spelling. +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index a16b1ab8cbb..efeedad8f0e 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Metadata Repository contracts: types, canonicalization, errors, interface (ADR-0008).", "type": "module", diff --git a/packages/metadata-fs/CHANGELOG.md b/packages/metadata-fs/CHANGELOG.md index b512eef0e12..73542b08d80 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,14 @@ # @objectstack/metadata-fs +## 17.6.0 + +### Patch Changes + +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [f3b16fc] + - @objectstack/metadata-core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index 6ee2abf96c9..af2d10b157b 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "FileSystemRepository: Node-only Repository implementation backed by JSON files and a JSONL change log (ADR-0008).", "type": "module", diff --git a/packages/metadata-protocol/CHANGELOG.md b/packages/metadata-protocol/CHANGELOG.md index f2df9aadfb9..73014dc520f 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,668 @@ # @objectstack/metadata-protocol +## 17.6.0 + +### Minor Changes + +- 9905e61: fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level `options` bag is refused by name (#20051) + + **BREAKING** — two narrowings on the `view` write door (`PUT /api/v1/meta/view/:name`, the Studio and MCP save). They ship as `minor` under the repo's launch-window convention for breaking changes. This is stage (iv), the last stage, of ruling 甲 on #20051. The storage half follows the maintainer's ruling on its Q2, letter B (「同意 批次 #256」). + + Clause-②: no (narrowing) + + ## What changes + + **1. What a saved view stores.** `saveMetaItem` used to validate a `view` body and then store the request body as sent, with two normalizations grafted back (filter operator spellings, and form `groups` → `sections`). It now stores the parsed value of every key the body carried, and nothing else: + + - **An undeclared key is dropped.** The members' top-level `.strip()` used to drop it from the parse only, so it lived in the store and nowhere in the contract. Every key the console reads back off a stored row is declared (`VIEW_CONSOLE_ROUND_TRIP_KEYS`), so nothing the console relies on is lost. The keys the console writes that are dropped are the ones mapped rather than declared in that record: a sort row's `id` (the builders mint a fresh one), a top-level `id` (read only when a row has no `name`), and `objectName` (every reader falls back to `object`, which both writers stamp). + - **A declared key keeps its normalized value.** Examples: `notEquals` → `not_equals`, `exportOptions: ['csv']` → `{ formats: ['csv'] }`, and a CEL string → its expression object. + - **A moved key is stored under its canonical spelling.** Examples: `groups` → `sections`, and `visibleOn` → `visibleWhen`. The second was never grafted before, so a form stored with `visibleOn` kept the legacy spelling. + - **A schema default the author did not write is NOT stored.** This is ADR-0087's `storable` rule, the one flows already follow: a stored row never pins the day's default. A console toolbar save (sort, density, hidden fields, column widths, inline edit) stores no `type: 'grid'`. A form stores no `sharing.enabled` and no section `collapsible` / `collapsed` / `columns` it did not write. Storing the whole parse output instead would also have minted rows that fail their own re-save: a column-less toolbar patch carrying the `grid` default is refused as "sets `type` but lists no `columns`". + + The stored row re-parses to exactly what the save accepted, so a GET → PUT of it is judged the same. Every other metadata type keeps its request body, with the two grafts, as before. + + **2. A ViewItem record's top-level `options` bag is refused.** On a record (`{ name, object, viewKind, config }`), the member's top-level strip used to drop `options` from the parse unread while the save stored it. The interface page then rendered it and the object page did not. It is now refused by name with `422 INVALID_METADATA` at `options`, and the message prescribes `config.KIND`. No console write puts the bag on a record. The flattened list overlay's legacy `options` bag is unchanged: it is judged key by key, and objectui pins it. + + ## FROM → TO + + | you wrote | the stored row / the door now | + |:--|:--| + | a view body with a key its member does not declare (`objectName`, a form-only `layout` on a list view, `isPinned` on a form) | the key is not stored: write only declared keys (`object`, not `objectName`) | + | a view body relying on a schema default being written into the row | the row carries only what you wrote; the parse applies the default on every read | + | `sort: [{ id, field, order }]` | stored as `sort: [{ field, order }]` | + | `groups: [...]` / `visibleOn: '…'` on a form | stored as `sections: [...]` / `visibleWhen: { dialect: 'cel', source: '…' }` | + | a ViewItem record with `options: { kanban: {...} }` | `422` at `options`: move it to `config: { kanban: {...} }` (`config.KIND`), or remove it | + + **The one-line fix:** write the declared spelling. A stored view you read back is what the contract accepts, and a record's per-kind blocks live under `config`. + + ## Existing rows + + Stored rows are not migrated and not re-read differently. The maintainer's word on this card is 「20051 不考虑现有的数据」. A row keeps its bytes until its next save, and that save stores it as described above. A record carrying a top-level `options` is refused on its next save and served as stored until then. + + +- b531c7b: `os serve` hands the runtime metadata save door the deployment's SDUI component manifest, and the save door compiles an html page's `source` against it: an unknown component or a `requires` that disagrees with the source is refused with a `422`, and `requires` is stamped from the compiled source (ADR-0080 §5). + + Clause-②: yes (narrowing — on a host that registers a manifest, the runtime metadata save door newly refuses an html page whose source uses a component the manifest does not declare, or whose `requires` disagrees with its source; the new exported `SDUI_MANIFEST_SERVICE` widens `@objectstack/metadata-protocol`) + + + + **BREAKING** — an accept-set narrowing on the runtime metadata save door, shipped + as `minor` under the launch-window convention (`check-changeset-no-major` refuses + `major` until GA; breaking-ness is carried by this banner and the ADR-0087 + disposition above, not by the level). On a server that has a manifest, a + `PUT /api/v1/meta/page/NAME` (and the draft publish) of a `kind: 'html'` page used + to store the source unjudged; it now answers `422 INVALID_METADATA` when the source + uses a component the deployment's console does not provide, naming the component in + each issue's `where` and `message`, or when a hand-written `requires` lists a + namespace the source does not use, omits one it does, or names one no component in + the manifest carries. **One-line fix:** use a component the manifest declares (or + install the plugin that provides it in the console the deployment serves), and omit + `requires` — it is derived from the source. + + **The channel.** `@objectstack/metadata-protocol` exports `SDUI_MANIFEST_SERVICE` + (`'sdui-manifest'`), a plain service key. `os serve` resolves the manifest once at + boot through the same resolver `os validate` uses — the project's own + `sdui.manifest.json` beside the served config, then the copy `@objectstack/console` + ships — and registers it under that key. The save door reads the key on every + publish, so a host that registers or replaces it later is seen by the next save. + + **The compile.** The save door runs `@objectstack/sdui-parser`'s `compile()`, the + compiler behind `os validate`'s JSX page gate, against the registered manifest. Its + diagnostics carry the same rule ids the CLI reports (`jsx-forbidden-tag`, + `jsx-unknown-component`, …); errors refuse the publish, warnings ride the response's + `advisories`. A disagreeing `requires` is refused under + `page-requires-disagrees-with-source`. A page that compiles is stored with the + `requires` its source yields, on a draft save too; a draft that does not compile, or + whose `requires` disagrees, is stored as written (drafts are not gated) and its + publish refuses it. + + **Without a manifest nothing changes.** A host that resolves no manifest registers + nothing and prints one boot line — `Page source and \`requires\` not validated at + save`, naming every place looked — and the save door stores html pages exactly as + before. A registered value with no `components` map is warned about once and never + compiled against. + + Measured before the refusal shipped: the three html pages in this repository + (`examples/app-showcase`: `showcase_capability_map`, `showcase_command_center_jsx`, + `showcase_start_here`) all compile against the pinned console's manifest with no + diagnostic and yield `requires: ['ui']`; none authors `requires`. +- c96beb2: fix(security): a flow's inbound-hook secret is withheld from every served flow definition, and a read → edit → republish round trip keeps it (#20552) + + Clause-②: yes (widening) + + **The widening.** `@objectstack/metadata-protocol` gains one public method, + `ObjectStackProtocolImplementation.getMetaItemsForExecution`. It returns the stored + bodies without the serving decorations, for in-process binders that execute what they + read. No door that answers a caller may use it. + + An `api` flow's start node carries its inbound hook's HMAC secret (`config.secret`, + ADR-0041), the one credential that hook has. Every read that served the flow's + definition served the secret with it, to any authenticated caller. It is now + withheld from what is SERVED, and from nothing the engine executes. + + **What no longer carries the secret.** The automation domain's flow-definition read + and the flow its `POST` / `PUT` / clone doors answer with; and on the metadata plane, + every read of a flow — item, list, layered, draft preview, published snapshot, diff, + audit — plus a package export. The key is removed, not masked: a mask is a non-blank + string the registration gate would accept as the secret. + + **Consequence for a reader.** A client that read the secret back from a definition + no longer can. A package exported from one deployment and imported into another + arrives without it, and its `api` flows are refused at registration until a secret is + set on the start node again. + + **The round trip.** A save that carries the projected form — no `secret` where the + read served none — keeps the stored secret, on both authoring surfaces (the metadata + plane's save door and the automation domain's `PUT` / `POST`). Only an explicit value + replaces it, so a rotation is written as before. The start node is matched by its + `id`, not its position, so an edit that reorders `nodes` keeps it too. The first save of an item that has no stored row yet, such as a code-authored flow or datasource, takes the value from the code layer the read served, for every type with a registered redactor. + + - `@objectstack/service-automation` owns the projection (`redactFlowCredentials`) and + registers it as the `flow` read-path redactor at plugin `init`. The engine keeps + binding with the stored secret: it now reads flows from the protocol's execution + face, because the served face no longer holds the credential its hooks verify + against. + - `@objectstack/metadata-protocol` gains `getMetaItemsForExecution` on + `ObjectStackProtocolImplementation` — the same flattened list `getMetaItems` + serves, without the serving decorations (no `_diagnostics`, no credential + redaction). It is for in-process engines that execute what they read; every door + that answers a caller keeps serving `getMetaItems`. `carryForwardRedactedValues` + now follows a redacted path through an array by the element's `id`. + - `@objectstack/metadata`'s `getPublished` applies the type's registered read-path + redactor to the body it returns. It was the one metadata read exit that served a + stored body without it. +- 31ed067: The runtime metadata publish gate refuses an `api` flow with no per-flow secret, and reads a secret the flow read path withheld as present (#20611). + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on the runtime metadata write door, + shipped as `minor` under the launch-window convention (`check-changeset-no-major` + refuses `major` until GA; breaking-ness is carried by this banner and the ADR-0087 + disposition above, not by the level). An `active` save through `/meta` of an + `api`-bound flow whose start node carries no usable `config.secret` (a + `PUT /api/v1/meta/flow/:name`, or the publish of such a draft) used to be stored; + the automation engine then refused to register it (`400` on the `/automation` + doors, a skip with a warning at boot). It is now refused at the save with + `422 INVALID_METADATA`, the issue naming `flow-api-trigger-secret-missing` at the + start node's `config.secret`, and nothing is stored. A draft save is still + accepted; its publish is refused the same way. + **One-line fix:** set a non-blank `config.secret` on the flow's start node — or, + for a flow that is only ever started explicitly, declare `type: 'autolaunched'` + with no `triggerType: 'api'`. + + **What does not change: a signed flow's round trip.** Every served flow definition withholds the start node's `config.secret`, so a body saved back after a read arrives without it, and the save restores the stored secret only after every gate has run, so that no gate handles a restored credential. The gate is now told WHERE the save will restore a credential from the stored row: those positions only, never the values. `flow-api-trigger-secret-missing` reads a secret that was withheld and is stored as present, and one that is absent and not stored as missing. So a GET → edit → PUT of a signed flow, and the first save of a code-authored flow whose secret is in the app's source, keep passing and keep their secret. An explicit empty `config.secret` is the author's own value and is refused as blank. + + `@objectstack/lint`: + + - `validateFlowApiTriggerSecret` now runs on the runtime publish gate too (`surfaces` `['cli', 'runtime-publish']`, `runtimeTypes: ['flow']`). Its `surfaceReason` is gone. + - `AuthoringRuleContext` gains an optional `restoredCredentialPaths`: a `ReadonlySet` of stack-relative positions in the rules' own finding-path spelling (`flows[0].nodes[1].config.secret`). Only the runtime publish gate sets it; `runAuthoringRules` never forwards it, so `os validate`, `os build` and `os lint` judge the author's own values as before. + - `runRuntimeAuthoringRules` accepts an optional `restoredCredentialPaths`: item-relative dotted positions in the `@objectstack/spec/kernel` redactor registry's `redactedKeys` spelling (`nodes.1.config.secret`). The gate re-spells them against the written item's place in its snapshot. + - `validateFlowApiTriggerSecret(stack, options?)` accepts an optional `{ restoredCredentialPaths }`, and treats a listed start-node secret position as present. + + `@objectstack/metadata-protocol`: `saveMetaItem` hands the runtime authoring gate the positions its own credential carry-forward will fill, computed from the same stored body. This costs one indexed `sys_metadata` read on an `active` save of a type with a registered redactor (`datasource`, and `flow` where the automation plugin registers one), and nothing for any other type or for a draft save. The carry-forward itself is unchanged and still runs after every gate. The draft→active promotion judges the stored draft row, which already holds what that draft's save carried forward, so it needs no such positions. +- cd6d8a5: fix(objectql,platform-objects,metadata-protocol)!: the platform's `sys_migration` primary-key lookups go through `findOne`, so an existing deployment no longer prints "Paged read of 'sys_migration' is NOT deterministic" on every boot and every `os migrate plan` (#20648) + + Clause-②: no (narrowing) + + + + The deployment ledger is read one row at a time, by primary key. Five readers + spelled that read as `find(sys_migration, { where: { id }, limit: 1 })`: the + engine's migration-gate read (`readMigrationFlagVerified`, behind + `haveFileColumnsMoved`, `isFileReferencesMigrationVerified` and + `isValueShapesMigrationVerified`), the engine's deviation marker and + creation-attestation revocation, `readDataMigrationFlag` in + `@objectstack/platform-objects/system`, and the seed-tenancy repair's receipt. + The SQL driver cannot tell that read from page one of a walk. The engine's gate + read runs at boot before the schema pass registers `sys_migration` with the + driver, and on a table the driver has not registered an unsorted paged read + warns that its pages may repeat or skip rows. Measured on a SQLite database + created by 17.4.0: every 17.5.0 boot and every `os migrate plan` printed that + warning once, for a lookup that cannot return two rows. All five readers now use + `findOne`, the single-row route the driver already exempts. The driver's check is + unchanged: an unsorted `limit` read on a table the driver did not create still + warns. + + **BREAKING**: this narrows what two published engine interfaces accept. The + first is `MigrationFlagEngine` in `@objectstack/platform-objects/system`. It is + the parameter type of `readDataMigrationFlag`, `isDataMigrationVerified`, + `mayActIrreversibly`, `recordDataMigrationRun`, `recordFileColumnMove` and + `attestFreshDatastore`, and part of `FilesToReferencesEngine` in + `@objectstack/service-storage`. The second is `SeedTenancyLedger` in + `@objectstack/metadata-protocol`, the type of a `SeedTenancySeam`'s `ledger`. + Each now requires `findOne` where it required `find`, so a hand-written stand-in + that provides only `find` no longer satisfies either type. It ships as `minor` + under the launch-window convention for accept-set narrowings. The ObjectQL engine + has both methods, so a host that passes the engine needs no change. + + **Your fix:** a stand-in that implemented `find` for these helpers implements + `findOne(object, options)` instead, answering the row whose `where.id` matches, + or `null`. + + At run time, a stand-in that still provides only `find` fails the read. + `readDataMigrationFlag` then answers `null`, the same answer as a missing row, so + the gates it feeds stay closed. `resolveSeedTenancySeam` now attaches a `ledger` + only for a host that has `getObject`, `findOne`, `insert` and `update`. For a + find-only host the seam's `ledger` is `undefined`, and when the seed-tenancy + repair applies, it says at `warn` that it could not record its receipt. +- 4b45afa: fix(runtime): the `/automation` write doors refuse a packaged flow, as the metadata door does (#20679) + + Clause-②: yes (widening) + + A flow that a code package ships has a locked base (ADR-0126 §2): changing or removing it in place is refused. `PUT /api/v1/meta/flow/:name` already refused it. The two `/automation` definition doors did not: an administrator holding `manage_metadata` could rewrite a packaged flow in the live engine with `PUT /api/v1/automation/:name` or with `POST /api/v1/automation` under its name (a create onto an existing name overwrites it), or remove it with `DELETE /api/v1/automation/:name`. + + All three now answer a packaged flow with the same code and status the metadata door gives (`403` `NOT_OVERRIDABLE`), and with the same sentence wherever the metadata protocol's own package door answers. The refusal comes before the engine is called, so nothing is registered or removed. On `DELETE`, it also comes before the engine's own `DELETE_RESTRICTED` / `409` for a packaged subflow that packaged callers still reach. + + What is not refused: + + - A flow that no code package ships, including a flow created with `POST /api/v1/automation` or authored through the metadata door. It is updated and removed as before. + - `POST /api/v1/automation/:name/clone`, which copies a packaged flow under a new name. This is the supported way to customize one (ADR-0126 §7.1). + - `POST /api/v1/automation/:name/toggle`, the switch that turns a packaged flow on or off (ADR-0126 §7.2). + - A deployment that sets `OS_METADATA_WRITABLE=flow`. It opens both doors, as the refusal message says. + + **The widening.** `@objectstack/metadata-protocol` gains one public method, `ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name, operation })`. It returns the refusal the metadata door would give for writing (`'save'`) or removing (`'delete'`) an existing item because a code package ships it, or `null` when that door would not refuse on this ground. `saveMetaItem` and `deleteMetaItem` call the same code, so the two doors cannot disagree. Their own refusals are unchanged. +- 1940afd: fix(metadata-protocol): `getPackagedDashboardBase(name)` answers the dashboard a code package ships, before any overlay (#20680) + + `ObjectStackProtocolImplementation` gains `getPackagedDashboardBase(name)`, the dashboard twin of `getPackagedObjectBase`. It returns the packaged (code-layer) declaration of a dashboard, which is the `packagedBase` that `translateDashboard` compares a served dashboard against, so that a tenant's published overlay is not overwritten by the packaged translation catalog. + + It reads the artifact registry's code-package entry only. An overlay that was hydrated under the plain registry key can therefore never be returned as the base it is compared against. It returns `undefined` for a dashboard that no code package ships, for an unknown or empty name, and for a registry that cannot answer. A caller treats `undefined` as "no base known", and the catalog applies as before. + + The method is additive. No existing read changes answer: the item and list reads already serve a published org overlay by the same identity the write stored (`type`, name, `package_id`, organization). Nothing is removed or renamed. +- 9ad6544: fix(metadata-protocol): `getPackagedViewBase(name)` answers the view a code package ships, before any overlay (#20731) + + `ObjectStackProtocolImplementation` gains `getPackagedViewBase(name)`, the view twin of `getPackagedDashboardBase`. It returns the packaged (code-layer) declaration of a view, which is the `packagedBase` that `translateView` compares a served view against, so that a tenant's published overlay is not overwritten by the packaged translation catalog. + + `name` is the view's registry identity, the qualified `.` that each view of a `defineView` container is registered under and that the overlay row and both reads carry. The bare view key that the catalog uses under its object is not an identity (another object may ship a view with the same key), and it answers `undefined`. + + It reads the artifact registry's code-package entry only, through the same lookup as `getPackagedDashboardBase`. An overlay that was hydrated under the plain registry key can therefore never be returned as the base it is compared against. It returns `undefined` for a view that no code package ships, for an unknown or empty name, and for a registry that cannot answer. A caller treats `undefined` as "no base known", and the catalog applies as before. + + The method is additive. No existing read changes answer, and `getPackagedDashboardBase` answers exactly as before. Nothing is removed or renamed. +- 76bd58f: fix(automation): which flows are packaged is the package loader's fact, never the flow definition's own, and every flow written through an authoring door is authored in the deployment (#20761) + + Clause-②: yes (widening) + + A flow counts as packaged only when a managed package's loader registered it (ADR-0126 §2, ADR-0131 D6). Before this change, a flow definition written through an authoring door could carry a code package's provenance, and the automation engine then treated that flow as the package's. + + - **The automation engine reads the loader's set.** The ADR-0126 §7.3 subflow guards, the arming gate, the activation switch and the package an activation row names now come from the packages the loader registered. The provenance a flow definition carries is kept for display only. `AutomationEngine` gains `setPackagedFlowSource(reader)` and `packagedFlowOwner(name)`, and the package exports the `PackagedFlowSource` type. `AutomationServicePlugin` attaches the reader for you: it asks the metadata protocol when the engine needs the answer. An engine with no reader attached treats no flow as packaged. + - **One authoring rule for flows.** `ObjectStackProtocolImplementation` gains two methods. `packagedArtifactOwner({ type, name })` names the package whose loader registered an item. `tenantAuthoredWriteRefusal({ type, name, item, packageId? })` is the rule every flow write door asks: the automation create, update and clone doors, and the metadata door's flow write. + - A write to a name a package ships is refused as a locked base. The answer is `packagedBaseRefusal`'s own (`403 NOT_OVERRIDABLE`), so sending a shipped flow's definition back is refused. + - A definition that claims a code package's provenance for a name no package ships is refused with `422 INVALID_METADATA`, and nothing is written. Before, the automation doors kept the claim and the metadata door removed it without saying so. + - A customer flow's definition sent back as it was read is accepted as before. That includes a stored flow bound to one of your own packages, whose read carries that binding. + - `packagedBaseRefusal` also takes an optional `packageId`, the base a save names. + - **The metadata door's other types are unchanged.** Only flows are judged by this rule. Migrating stored rows and duplicating a package are not affected either. + - **A clone is saved.** `POST /automation/:name/clone` now writes its copy as a stored flow of the deployment, through the metadata protocol's save, with no package provenance. The copy reads back on the metadata door and is still there after a restart. Before, it lived only in the running engine and was gone after a restart. If the save fails, the clone is withdrawn and the failure is returned. + + **If a write of yours is now refused with `422 INVALID_METADATA`:** remove the package provenance from the flow definition and send it again. To customize a packaged flow, clone it under a new name. +- b1aee33: fix(metadata-protocol)!: a flow saved through the metadata door naming, as its base, a package this deployment has not installed is refused, instead of being stored bound to a package that does not exist (#20863) + + Clause-②: no (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. `PUT /api/v1/meta/flow/:name` answered `200` and stored the flow live when the save named, as the package the flow belongs to, a package id this deployment has never installed. It now answers `422 WRITABLE_PACKAGE_REQUIRED`, and nothing is written, served or registered. + + **What changed.** The one authoring rule every flow write door asks (`tenantAuthoredWriteRefusal`) now also judges the base a save names. After the locked-base refusal and before the provenance check, a named base must be a package the registry holds as installed: a code package, an installed package, or a tenant's own writable base created through the package door. That is the same registry read the metadata write path already resolves a base against, so no second list of packages is kept. The refusal does not depend on what the definition carries: a save with no provenance of its own and a save whose provenance names that same missing package were both stored before, and both are refused now. It applies on a single-kernel host and on an environment kernel alike. + + - The code is `WRITABLE_PACKAGE_REQUIRED` / `422`, the one ADR-0070 D1 already uses for a runtime create whose base is missing or read-only. The refusal names the package id the save sent. + - **Unchanged:** a flow saved without naming a package; a flow saved into an installed package; the locked-base refusal of a shipped flow, which still answers first; every other metadata type, whose saves keep their old handling; the `/automation` create, update and clone doors, which name no package; and the server-stated rewrites of stored rows (the stored-metadata migration and package duplication), which the rule does not judge. + + **What to send instead.** Save the flow into a package this deployment has installed (the package list shows them, and a base that does not exist yet is created first through the package door), or save the flow without naming a package. +- 250dec8: A stored page whose `requires` names a plugin the deployment's console does not load is reported when the page loads, and a draft → active promotion re-stamps an html page's `requires` with the save door's own computation (ADR-0080 §5: `requires` is validated at save and load, and derived from the source). + + Clause-②: no + + **At load.** The boot hydration of stored metadata (`loadMetaFromDb`) prints one `warn` line for each stored page whose `requires` lists a namespace no component in the deployment's SDUI component manifest carries, naming the page and every such namespace under the marker `[page_requires_plugin_absent]`. It is a report, never a refusal: the page has already loaded when the line is printed, and it is served. The manifest is read through the same `SDUI_MANIFEST_SERVICE` key the save door reads; `os serve` registers it before any plugin initialises, so it is there when stored pages load. A host that registers no manifest judges no page at load, exactly as it judges none at save. + + **At promotion.** `publishMetaItem` and `publishPackageDrafts` now store the promoted body with the `requires` that an active save of the same body would store, computed against the manifest registered at the moment of the publish. Before, the draft's `requires` was carried into the active row as it was. A draft saved before the host had a manifest reached `active` with no `requires`, and an agreeing list kept the draft's own spelling (its order and any repeats). Nothing is newly refused: wherever the runtime authoring gate runs, it already refused a draft whose source does not compile, or whose `requires` disagrees with its source, and it still does. A host with no manifest promotes the draft as written. + + `SysMetadataRepository.promoteDraft` takes an optional `deriveActiveBody(draftBody)` that derives the active row's body from the draft row it promotes. When it is omitted, the draft body is promoted unchanged, as before. +- 657b6b7: The dry run and the partial-success batch insert now say which row lost which field. `ObjectQL.validate` (and `validateData`, which relays it) answers `droppedFields` on each accepted row of `results`, and `ObjectQL.insertMany` (and `insertManyData`, which passes it through) answers `droppedFields` on each `ok` outcome: the caller-supplied fields the engine legally strips from that row, one `DroppedFieldsEvent` per reason, in the engine's own reason vocabulary (`computed` for a `formula` value, `readonly` for a static `readonly` or runtime-owned field). The key is absent when nothing was taken from the row. + + - **Recorded at the strips, never inferred from the union.** Each strip records what it takes from each row as it runs. A `beforeInsert` hook that assigns a protected key on one row keeps it there, so that row is not named, while a sibling row that supplied the same key and lost it is. + - **A row the write does not complete carries none.** A preview row the verdict refuses, and an `ok: false` outcome, carry no `droppedFields`: a drop means the write completed without the field. + - **The dry run and the commit agree.** On `insert` mode the preview runs the same strips the write runs, so a row's preview drops and its outcome drops are the same list. One gap is unchanged: the preview runs no hooks, so a key a `beforeInsert` hook assigns is reported by the preview and kept by the write. An `update`-mode preview does not run the `readonlyWhen` or primary-key strips, which judge a prior record the preview does not read. + - **Unchanged:** the `onFieldsDropped` listener on `insert`, `insertMany` and `validate` still reports the batch-level union, one event per reason, naming no row. So does `insertManyData`'s top-level `droppedFields`. `insert(object, rows[])` still returns the records, with no per-row slot. `strictReadonlyWrites` still refuses the whole batch before any outcome is built. + + Graded `minor` in both packages: each widens a published method's declared answer with a new optional key (`InsertManyRowOutcome` gains `droppedFields`, and so does each outcome of `insertManyData`'s return type), which is an additive widening of the public surface. Nothing is removed, renamed or refused. The keys on the wire, `ValidateDataResponseSchema.results[].droppedFields` and `ImportRowResultSchema.droppedFields`, were already declared in `@objectstack/spec`. +- 514001a: fix(rest,runtime): the published-snapshot read of a flow name a managed package ships answers the package's flow, as the layered read does (#21002) + + Clause-②: yes (widening) + + `flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Since the previous half of #21002, the layered read, `GET /api/v1/meta/flow/:name/layers`, reports the package's flow as the effective layer for a name a managed package ships, and a stored flow of that name as a separate layer that does not take effect. The published-snapshot read, `GET /api/v1/meta/:type/:name/published`, and its runtime-dispatcher twin read that same layered answer, but served its stored layer whenever one was present. So for such a name they still answered `200` with the stored flow, not the package's. + + Both published-snapshot doors now serve the layered read's effective layer when that read put the package's flow over a stored flow, which is the package's flow. They ask the metadata protocol's own check for that decision rather than repeating it. In every other case they answer exactly as before: a flow name no managed package ships, and every other metadata type, `object` included, still answer the stored layer when one is present, and an item with no stored layer still falls through to the code/package snapshot. The stored flow is not deleted, rewritten or refused. + + **The widening.** `@objectstack/metadata-protocol` makes one existing method public: `ObjectStackProtocolImplementation.isShippedFlowName(type, name)`. It answers whether `name` is a flow name a managed package ships. It was private to the class, so a door in another package could not ask it any other way. Its answer is unchanged, and the layered read, the by-name read and the flow list keep calling it. +- cfad7de: fix(metadata-protocol)!: stored metadata bodies read through the generic data door are served as their type's read projection, so stored credentials are withheld there too; grouping those tables by the body column is refused (#21086) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the generic data door's query accepts as a grouping target on two system tables. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** A row of `sys_metadata` or `sys_metadata_history` read through `GET /api/v1/data/:object`, `POST /api/v1/data/:object/query`, `GET /api/v1/data/:object/:id` (and anything that reads through the same `findData` / `getData`, such as the export route) now carries its `metadata` column as the body's type's read projection: the same object every `/meta` read exit serves, chosen through the same `@objectstack/spec/kernel` redactor registry. For a `datasource` body that means the stored credential material the datasource doors already withhold is withheld here too, decided by the same redactor. A body with nothing to withhold, and every body of a type that registers no redactor, is served as the stored bytes. + + - A projection that names `metadata` without `type` (`?select=metadata`) still works: the door reads `type` to choose the redactor and does not serve it. + - A body the door cannot judge is omitted rather than served: one whose row carries no `type`, and one that does not parse while its type registers a redactor. + + **What an author sees now on a grouping.** `400 INVALID_FIELD` for a `groupBy` entry naming `metadata` on either table, located at the entry (`groupBy[0]`, or `groupBy[0].field` for the object form), saying the query was not run and naming the route: group by `type`, `name` or another scalar column, and read the bodies with a plain list. A group key stands for every row that shares it, and the redactor is chosen per row, so the key cannot be projected without changing which rows it counts. + + **Unchanged.** Every other object, including one with a column of its own named `metadata`; every other grouping on these tables; and every internal reader of `sys_metadata`, which reads through the engine rather than through this door and keeps reading the stored body. +- 336e191: fix(security)!: stored metadata bodies are projected or refused at the audit, analytics, realtime and data-door filter/sort exits too + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what three doors accept or serve for the two stored-metadata tables — the generic data door refuses a filter or sort on the body column, the analytics door refuses it as a dimension / measure / filter / sort member, and the realtime event and the audit/activity copy now carry the body as its type's read projection instead of the stored bytes. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** + + - **Audit / activity copy (`@objectstack/plugin-audit`).** The audit writer copies a `sys_metadata` / `sys_metadata_history` row into `sys_audit_log.new_value` / `old_value` and `sys_activity.metadata`. That copy now projects the body through the shared redactor, so stored credential material is withheld from the second store too. A new `os migrate audit-metadata-bodies` command rewrites the copies already at rest (dry run by default, `--apply` to write, idempotent). + - **Analytics (`@objectstack/service-analytics`).** A query naming the stored body column of these objects as a dimension, measure, filter or sort is refused with `400 INVALID_FIELD`, before any strategy runs — the posture analytics already takes for a member it will not evaluate. + - **Realtime (`@objectstack/objectql`).** A `data.record.*` event projects its `after` / `changes` body through the same redactor, so a subscriber to these objects' events receives no stored credential. + - **Data door filter / sort (`@objectstack/metadata-protocol`).** A filter or sort on the body column is refused with `400 INVALID_FIELD`, the same family and shape as the existing groupBy refusal. + + **What stays answerable.** Every scalar column of these objects — `type`, `name`, `scope`, `state`, timestamps — is still grouped, filtered, sorted, counted and served; only the body column is affected. Every other object is unchanged. + +### Patch Changes + +- fe463b4: metadata-protocol refusals, hints and log lines no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages the metadata protocol shows to authors, administrators and operators ended with an + issue-tracker number where the reason belonged. The number goes, and where the sentence did not + already say what was decided, it now does: + + - Refusals: `insertManyData` without an engine `insertMany` now names what that method is (the + partial-success batch insert, so a bad row neither fails the whole batch nor runs the good rows' + `beforeInsert` hooks twice); the unknown-metadata-type refusal says a plugin cannot declare a type + because `additionalTypes` was retired, having never been read; the stored non-canonical type + refusals on publish and revert say the `/meta` URL door now folds a type to its canonical spelling + before it writes, so such a row predates that. + - The schedule-flow `organization_id` hint says why the author's value is the only source: the engine + fills only an organization the run resolved, and a schedule resolves none. + - Log lines: the three `kernel:ready` "migration skipped" warnings now say what the migration that did + not run would have ensured; the history-counter abort says the old path took a failed read for an + empty table; the publish-closure degrade says the batch's own drafts are left out of the closure; + the cold-boot org-scoped audit calls the write refusal it points at declared-types-only. The + overlay, `sys_view_definition` and `sys_setting` index messages, the seed/API tenancy repair and its + receipt, the batch-row withhold and the object-existence gate's no-registry warning lose only the + citation, because their sentences already said it. + - The live-MySQL testkit's isolation error loses its citation. + + Text only: no error code, field name, status or behaviour changes. +- 3f45b6c: fix(security): every credential a flow definition holds is withheld from what is served, at every depth, and an edit round trip keeps each one where it belongs (#20590) + + Clause-②: no + + **What is now withheld.** Beside an `api` flow's inbound-hook secret (the start node's + `config.secret`), every served flow definition now also withholds an `http` node's + outbound signing secret (`config.signingSecret`), and both are withheld wherever the + node sits: at the top level, or inside a `loop` body, a `parallel` branch, or a + `try_catch` region. The engine still executes the stored values. + + **Removing a signing secret.** A definition saved back without the key keeps the + stored secret, because an absent key is what every read serves. To remove it, save + the key as the empty string (`signingSecret: ''`): the durable callout is then + delivered unsigned, and the empty value is served as written, so the next round trip + keeps it cleared. + + **Changing a node's kind.** An edit that keeps a node's `id` and changes its kind no + longer carries that node's stored credential onto it. The credential belonged to the + old kind; a start node that needs a secret asks for one again at registration. + + **Moving a node.** A node moved into or out of a `loop` body, a `parallel` branch or a + `try_catch` region keeps its stored credential across the round trip, as long as its + `id` and kind are unchanged and it is the only node, at the top level or in any region, + that carries that `id`. An edge or a config value with the same `id` does not count. + + **The `/meta` list read on a dispatcher host.** When the metadata protocol's list read + fails, the list answers that failure (`503 SERVICE_UNAVAILABLE` for a store outage, or + the protocol's own refusal) instead of serving the metadata service's stored list, + which applies no credential redaction. A host whose protocol has no list verb keeps + its metadata-service fallback. +- a7d9768: Provenance comments in `@objectstack/metadata-protocol` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes + (and ADR-0005's design-principle-3 correction where that record exists). Some of these docblocks sit + on exported members, so the reworded text appears in the published `index.d.ts` / `index.d.cts`, and + a few comments that esbuild keeps appear in the JavaScript output. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- ca5408c: fix(metadata-protocol): a dotted relation filter path names the nested-relation form as the route + + A filter key such as `account.industry` — a dotted path through a relation field — is still refused with `INVALID_FIELD` / 400 at the query parameter door. Its words no longer say a filter reaches only the object's own columns, which stopped being true when the engine began serving the nested-relation form in `where`: they now name that form, `{ "account": { "industry": VALUE } }`, beside the denormalise remedy, in the same words as the engine's own refusal. +- 4d0b9cd: fix(metadata-protocol): the refusal of an in-place edit or removal of a packaged flow names the clone and the on/off switch, not a redeploy or `OS_METADATA_WRITABLE` (#20819) + + Clause-②: no + + A write or removal that targets a flow shipped by a code package, and does not name that package, is refused with `403 NOT_OVERRIDABLE`. That covers `PUT /api/v1/meta/flow/:name` without `?package=`, `DELETE /api/v1/meta/flow/:name`, `PUT` and `DELETE /api/v1/automation/:name`, and `POST /api/v1/automation` onto a packaged flow's name. The refusal used to say "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch", and cited ADR-0005. The administrator of an installed package can do neither. + + The refusal now names the two paths ADR-0126 sanctions for a packaged flow, and cites ADR-0126: + + - clone it under a new name to customize it: `POST /api/v1/automation/:name/clone` with `{ name, label }`; + - or switch it off: `POST /api/v1/automation/:name/toggle` with `{ enabled: false }`. Where one install serves several organizations, only the platform operator can use the switch. + + The status, the code and which writes are refused are unchanged. `OS_METADATA_WRITABLE=flow` still opens the lock as before; the refusal just no longer suggests it. Every other metadata type's refusal reads exactly as before. A write that names the shipping package with `?package=` is refused with `403 ITEM_LOCKED` by a separate limb, which this change leaves as it was. +- 0c5a71b: fix(metadata-protocol): every refusal of an in-place edit of a packaged flow, action or permission set names that type's own sanctioned path, at every door, and a packaged action's removal names one too, not a redeploy or `OS_METADATA_WRITABLE` (#20910) + + Clause-②: no + + ADR-0126 puts `flow`, `action` and `permission` in Regime C. The packaged base is locked, and the refusal names the sanctioned path. Until now only a flow's package-less refusal did, and only at one of the three places that refuse such a write. The other places prescribed "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE …" or "Set OS_METADATA_WRITABLE to enable additional types at runtime". A packaged action's removal named no path at all. + + There is now one regime table, and each type's row names only the primitives that type has: + + - a packaged flow: clone it under a new name with `POST /api/v1/automation/:name/clone` and `{ name, label }`, or switch it off with `POST /api/v1/automation/:name/toggle` and `{ enabled: false }`; + - a packaged action: switch it off with `POST /api/v1/actions/_activation/:object/:action` and `{ enabled: false }` (`:object` is `global` for an object-less action). No clone is named, because cloning an action is not a sanctioned path; + - a packaged permission set: clone it under a new name, with the "Clone" action on the permission set or `POST /api/v1/data/sys_permission_set` with a new name. This is the same wording the permission-set lock in `@objectstack/plugin-security` already uses. + + The switches are operator-only where one install serves several organizations. Every refusal cites ADR-0126. All three places that refuse such a write read the same table: + + - **`403 NOT_OVERRIDABLE` on an environment-scoped kernel.** This covers `PUT /api/v1/meta/:type/:name` without `?package=`, and for a flow or an action `DELETE` too. + - **`403 NOT_OVERRIDABLE` where the `/meta` protocol is not environment-scoped**, for example the default local `pnpm dev` boot. The metadata repository refuses that write one layer down, and now with the same sentence. + - **`403 ITEM_LOCKED` for a write that names the read-only package with `?package=`, while `OS_METADATA_WRITABLE` is not set for the type.** The sentence now opens "Cannot overlay 'TYPE' in package 'ID': that package is read-only, and its packaged base is locked against in-place edits." and then names the path. + + A packaged flow's package-less sentence is byte-for-byte unchanged. Statuses, codes, `lockSource`, `packageId`, `docs` and which writes are refused are unchanged too. `OS_METADATA_WRITABLE` still opens the lock for a write that names no package. The `ITEM_LOCKED` refusal given while the variable IS set reads exactly as before. Removing a permission set's overlay row is still allowed, as repair. Every type with no declared regime reads exactly as before, at every door. +- 75519e1: fix(metadata-protocol): a stored flow under a name a managed package ships is no longer registered or listed as the package's flow (#20913) + + Clause-②: no + + `flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Two places still treated a stored flow of a shipped name as an overlay of the package's flow: + + - The startup hydration registered the stored flow carrying the package's provenance, so the automation engine could not tell it apart from the package's own flow. It is now registered as the tenant-authored row it is. + - The flattened flow list served the stored flow in the package's place, marked as the package's. That list is `GET /api/v1/meta/flow` and the execution view the automation engine binds flows from. For a name a managed package ships, the list now serves the package's flow. + + The stored flow is not deleted, rewritten or refused. It stays in the store, and the automation engine reports it as a shadowed definition at startup. Every other metadata type, and every flow name no managed package ships, is listed as before. The by-name read, `GET /api/v1/meta/flow/:name`, is not changed. +- 25f2e64: fix(metadata-protocol): the by-name read of a flow name a managed package ships serves the package's flow, as the flow list does (#20946) + + Clause-②: no + + `flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. The flow list, `GET /api/v1/meta/flow`, and the execution view the automation engine binds flows from already serve the package's flow for a name a managed package ships (#20913). The by-name read, `GET /api/v1/meta/flow/:name`, did not: for such a name it served a stored flow of that name, marked as the package's flow. So the two read doors answered two different flows for one name. + + The by-name read now applies the same rule the list applies, through the same checks. For a name a managed package ships, it serves the package's flow, with or without a package scope, whatever the stored flow's own package binding or markings say. + + The stored flow is not deleted, rewritten or refused. It stays in the store, and the automation engine still reports it as a shadowed definition at startup. Pending drafts, flow names no managed package ships, organization-scoped rows and every other metadata type are read as before. +- 94990a2: fix(metadata-protocol): the layered read of a flow name a managed package ships reports the package's flow as the effective layer, as the by-name read and the flow list do (#21002) + + Clause-②: no + + `flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. The flow list, `GET /api/v1/meta/flow`, and the by-name read, `GET /api/v1/meta/flow/:name`, already serve the package's flow for a name a managed package ships (#20913, #20946). The layered read, `GET /api/v1/meta/flow/:name/layers`, did not: for such a name it reported a stored flow of that name as the effective layer, while its lock and provenance flags named the package. So the layered read and the other two read doors answered two different flows for one name. + + The layered read now decides the effective layer with the same check the other two doors use. For a name a managed package ships, the effective layer is the package's flow, with or without a package scope, whatever the stored flow's own package binding or markings say. The stored flow is still reported, as a separate layer of its own scope that does not take effect. The deprecated layers flag on the by-name read answers the same. + + The stored flow is not deleted, rewritten or refused. Flow names no managed package ships, organization-scoped rows and every other metadata type are read as before. +- 70dae53: feat(spec): discovery reports which optional `/auth` route families are mounted, starting with the better-auth admin family (`authFamilies.admin`) (#21046) + + Clause-②: yes + + **New key.** `DiscoverySchema` declares an optional `authFamilies` block, `{ admin: boolean }`. `admin` says whether the better-auth admin family (`{routes.auth}/admin/*`: `list-users`, `set-role`, `update-user`, `ban-user`, …) is mounted on this deployment. On a deployment that does not enable the admin plugin those routes answer a plain `404`, the same as a mistyped path, so a caller checks `authFamilies.admin` before building a URL into the family. `@objectstack/spec/api` also exports the block's schema (`AuthFamiliesSchema`, type `AuthFamilies`) and its reader, `readAuthFamilies(authService)`. + + **Same answer as `/auth/config`.** The value is the auth service's own `getPublicConfig().features.admin`, the object `GET /api/v1/auth/config` serves. Both discovery producers read it through `readAuthFamilies`: `getDiscovery()` in `@objectstack/metadata-protocol` (served by `@objectstack/rest` at `GET /api/v1/discovery`) and `getDiscoveryInfo()` in `@objectstack/runtime` (served at `GET /.well-known/objectstack`). Neither re-derives whether the admin plugin is on, so on one boot the two documents and `/auth/config` agree. On a stock boot `authFamilies.admin` is `false`. With the admin plugin on (`plugins.admin: true`, or SCIM, which forces it on) it is `true`. + + **When the key is absent.** A producer that cannot read the answer emits no `authFamilies`, rather than a guessed `false`. That happens when no `auth` service is registered (then `routes.auth` is absent too), when the registered service has no `getPublicConfig()`, or when that call throws (`/auth/config` answers `500 AUTH_CONFIG_ERROR` in that state). Treat an absent block as "not known to be mounted". + + **What did not change.** No existing key, route or status moved. The unmounted admin routes still answer a plain `404`. +- d34aa58: fix(metadata-protocol): the layered read's code layer is empty for an item no package ships, whether or not a stored copy of it has been loaded into the registry (#21059) + + Clause-②: no + + The layered read, `GET /api/v1/meta/:type/:name/layers`, reports an item's code layer as the packaged definition, and as empty when no package ships the item and it exists only as a stored customization. For an item no package ships, it answered that correctly only until the stored copy had been loaded into the in-memory registry, for example by a restart's startup load or by a list read. After that, the code layer answered the stored copy, and the lock and provenance flags were derived from it. So the same read of the same item gave two answers, depending on what had been loaded. + + The code layer now skips a stored copy the registry holds, using the tenant-authorship mark the startup load already puts on every stored copy it registers. An item no package ships has an empty code layer before and after the load, and its flags come from the stored layer both times. This includes a stored copy whose own content claims a package's provenance: a claim is not a packaged definition. The deprecated layers flag on the by-name read, `GET /api/v1/meta/:type/:name`, answers the same. + + Packaged items keep their packaged code layer. An item registered at runtime with no package keeps its code layer, as before. The stored rows are not changed. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [b616c0a] +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [a093ce3] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [e651556] +- Updated dependencies [f379f57] +- Updated dependencies [5bed1f6] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [f29c83d] +- Updated dependencies [c6b37cd] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [31ed067] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [b84b240] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d1633f3] +- Updated dependencies [5e470f8] +- Updated dependencies [5e470f8] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [3693a1b] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [327391c] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [ce8a6d2] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [ee42f00] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [61455de] +- Updated dependencies [aa23e2c] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [b8191f7] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/lint@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/metadata@17.6.0 + - @objectstack/sdui-parser@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 67b0d02007a..cdc1f2feaa3 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack metadata management protocol: sys_metadata CRUD, draft/publish, locks, package ownership, diagnostics (ADR-0076).", "type": "module", diff --git a/packages/metadata/CHANGELOG.md b/packages/metadata/CHANGELOG.md index 5cf07565973..319ee13481e 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,267 @@ # @objectstack/metadata +## 17.6.0 + +### Patch Changes + +- 88b484e: feat(objectql): the runtime resolves a field's `picklist` onto its served options, validates writes against the resolved list, and merges `picklistExtensions` additively + + Clause-②: no + + - **Load.** `defineStack({ picklists })` and `defineStack({ picklistExtensions })` now register, from a manifest and from a nested plugin, through the same registration seam as every other collection. The compiled-artifact door registers `picklists` as `picklist` items, so `GET /meta/picklist` serves them on an artifact boot. + - **Merge.** A picklist's options are its own, followed by the options every `picklistExtensions` entry adds. A value the list already carries is refused with `422 INVALID_METADATA`, which names both declarations, whichever of the two registered first. The later declaration never replaces the earlier one. A package that registers again replaces its own extension. Uninstalling a package removes the values it added. + - **Serve.** A field with `picklist: 'NAME'` is served with the resolved options written onto it and `picklist` kept (`PicklistServedFieldSchema`), on every object read, including objects stored in `sys_metadata`. The list's translations (`picklists.NAME.options.VALUE`) relabel those options per request locale. An option marked `default: true` in the list fills an omitted field on insert, as an inline option does, and the import template reads it the same way. + - **Unknown name.** A packaged field that names a picklist no loaded package declares fails the boot at `kernel:ready` with `INVALID_METADATA`, and so does a `picklistExtensions` entry that extends such a list. The error names every such field or extension and the package that declared it. After boot, an artifact registered through the `manifest` service is checked before any of it registers. A field whose list does not resolve is served with no options and accepts no value. + - **Write validation.** The write door judges a picklist-bound field against the resolved options, and its refusal names the picklist. The wire code stays `invalid_option`. The validation message catalog gains three message keys for this (`invalid_option_picklist`, `invalid_option_value_picklist`, `invalid_option_picklist_unresolved`) in en, zh-CN, ja-JP and es-ES. They change the message text only, never the wire. + - **Writing the served body back.** The served body carries `picklist` and `options` together. Writing it back through the metadata door is still refused, with the prescription to drop `options`, as `FieldSchema` declares. Nothing strips it on the write side. + - **Ledger.** `field.picklist`, the `picklist` kind's rows and `translation.picklists` are `live`. `field.picklist` no longer carries `authorWarn`, so `os lint` / `os validate` stop warning an author who writes it. +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- c96beb2: fix(security): a flow's inbound-hook secret is withheld from every served flow definition, and a read → edit → republish round trip keeps it (#20552) + + Clause-②: yes (widening) + + **The widening.** `@objectstack/metadata-protocol` gains one public method, + `ObjectStackProtocolImplementation.getMetaItemsForExecution`. It returns the stored + bodies without the serving decorations, for in-process binders that execute what they + read. No door that answers a caller may use it. + + An `api` flow's start node carries its inbound hook's HMAC secret (`config.secret`, + ADR-0041), the one credential that hook has. Every read that served the flow's + definition served the secret with it, to any authenticated caller. It is now + withheld from what is SERVED, and from nothing the engine executes. + + **What no longer carries the secret.** The automation domain's flow-definition read + and the flow its `POST` / `PUT` / clone doors answer with; and on the metadata plane, + every read of a flow — item, list, layered, draft preview, published snapshot, diff, + audit — plus a package export. The key is removed, not masked: a mask is a non-blank + string the registration gate would accept as the secret. + + **Consequence for a reader.** A client that read the secret back from a definition + no longer can. A package exported from one deployment and imported into another + arrives without it, and its `api` flows are refused at registration until a secret is + set on the start node again. + + **The round trip.** A save that carries the projected form — no `secret` where the + read served none — keeps the stored secret, on both authoring surfaces (the metadata + plane's save door and the automation domain's `PUT` / `POST`). Only an explicit value + replaces it, so a rotation is written as before. The start node is matched by its + `id`, not its position, so an edit that reorders `nodes` keeps it too. The first save of an item that has no stored row yet, such as a code-authored flow or datasource, takes the value from the code layer the read served, for every type with a registered redactor. + + - `@objectstack/service-automation` owns the projection (`redactFlowCredentials`) and + registers it as the `flow` read-path redactor at plugin `init`. The engine keeps + binding with the stored secret: it now reads flows from the protocol's execution + face, because the served face no longer holds the credential its hooks verify + against. + - `@objectstack/metadata-protocol` gains `getMetaItemsForExecution` on + `ObjectStackProtocolImplementation` — the same flattened list `getMetaItems` + serves, without the serving decorations (no `_diagnostics`, no credential + redaction). It is for in-process engines that execute what they read; every door + that answers a caller keeps serving `getMetaItems`. `carryForwardRedactedValues` + now follows a redacted path through an array by the element's `id`. + - `@objectstack/metadata`'s `getPublished` applies the type's registered read-path + redactor to the body it returns. It was the one metadata read exit that served a + stored body without it. +- 8460592: fix: the whole-day bound on a bare `YYYY-MM-DD` upper bound is applied at the seams only — `DatabaseLoader.queryHistory` in driver mode becomes one, the engine seam lowers type-blind for an object with no field map, and `InMemoryDriver` drops its own copy (ADR-0053 D-D1 items 5 and 7, #20822) + + Clause-②: no + + - **`@objectstack/metadata` — `DatabaseLoader.queryHistory` in driver mode lowers its own filter.** With a raw `IDataDriver` (`MetadataManager.setDatabaseDriver`) the history filter reaches the driver without passing any seam. The loader now runs the shared `lowerFilterCondition` (`@objectstack/spec/data`) on it, typed by the history object it syncs: `until: 'YYYY-MM-DD'` reads `recorded_at < next day`, so every version recorded on that day is kept on every driver, and an instant `until` is kept as written. Engine mode is unchanged (the engine's own `where` seam lowers it). Before this, the whole day was kept only by each driver's own copy of the rule; with `@objectstack/driver-memory`'s copy deleted below, `until` = today would have gone from every version of the day to none. + - **`@objectstack/objectql` — an object with no field map is lowered type-blind.** The engine's `where` seam (on `find`, `findOne`, `count`, `update`, `delete` and `aggregate`'s `where` / `aggregations[i].filter`) reads the object's declared field map and rewrites a declared `datetime` column only. For an object the registry does not hold there is no declaration to read, and the seam now applies the whole-day rules to every column (a bare-day `$lte` becomes `$lt` the next day, a `$between` splits), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the declared type. It used to leave such an object to each driver's own copy. Visible on `SqlDriver`: a bare-day `$lte` on a non-`datetime` column of an unregistered object that holds ISO instant text now keeps the whole day; a `datetime` or `date` column answers as before. An object with a field map is unchanged. + - **`@objectstack/driver-memory` — `InMemoryDriver` compiles the comparison it is handed.** Its four copies of the whole-day rule are deleted (the `$lte` and `$between` arms of the filter translator, the `<=` and `between` arms of the AST-node translator). A read through the engine hands it a `where` the engine's seam has already lowered, so on that path a declared `datetime` column keeps the whole named day, and a declared `date` column answers as before. A row-level security `using` filter is not lowered by the engine's seam: the security middleware ANDs it into the query's `where` after that seam has run, and only the RLS compile seam lowers it, rewriting just the columns its field guard declares `datetime`. Two answers converge on what `SqlDriver` already returns (ADR-0053 D-D1 item 7's scope): on a registered object, a bare-day `$lte` / `$between` on a declared `text` column holding ISO instant text, or on a column the object does not declare, is now compared as written, where this driver used to widen it to the whole day. One path narrows outside those two: an RLS `using` policy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, is compiled with no field guard, so the RLS compile seam reads no column as `datetime` and the bound reaches this driver as written, where this driver used to widen it to the whole day; that holds until #20822 group 2 makes the RLS compile seam type-blind when it has no guard. A direct `find()` that passed no seam gets the comparison it wrote (item 5); lower the filter with `lowerFilterCondition` first to keep the whole-day reading. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- 61455de: `@objectstack/metadata` declares `js-yaml` `^5.4.1` (was `^5.2.3`), clearing GHSA-r3ph-w7gj-g6xm, which affects js-yaml releases before 5.4.1. The lockfile now resolves 5.4.2. `js-yaml` is the YAML parser behind the metadata loader, and it was this package's only importer of it. + + Clause-②: no + + No exported symbol, option key or accept/reject verdict of ours moves; the published surface is unchanged and grades `patch`. The change is a dependency-range floor, so a fresh install can no longer resolve a vulnerable 5.x copy. + + `osv-scanner.toml` keeps zero exemptions and is untouched. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/metadata-fs@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index dbe46b17dee..443f80d43df 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Metadata loading, saving, and persistence for ObjectStack", "type": "module", diff --git a/packages/objectql/CHANGELOG.md b/packages/objectql/CHANGELOG.md index acbd9663fe6..6cf608c0be6 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,973 @@ # @objectstack/objectql +## 17.6.0 + +### Minor Changes + +- 88b484e: feat(objectql): the runtime resolves a field's `picklist` onto its served options, validates writes against the resolved list, and merges `picklistExtensions` additively + + Clause-②: no + + - **Load.** `defineStack({ picklists })` and `defineStack({ picklistExtensions })` now register, from a manifest and from a nested plugin, through the same registration seam as every other collection. The compiled-artifact door registers `picklists` as `picklist` items, so `GET /meta/picklist` serves them on an artifact boot. + - **Merge.** A picklist's options are its own, followed by the options every `picklistExtensions` entry adds. A value the list already carries is refused with `422 INVALID_METADATA`, which names both declarations, whichever of the two registered first. The later declaration never replaces the earlier one. A package that registers again replaces its own extension. Uninstalling a package removes the values it added. + - **Serve.** A field with `picklist: 'NAME'` is served with the resolved options written onto it and `picklist` kept (`PicklistServedFieldSchema`), on every object read, including objects stored in `sys_metadata`. The list's translations (`picklists.NAME.options.VALUE`) relabel those options per request locale. An option marked `default: true` in the list fills an omitted field on insert, as an inline option does, and the import template reads it the same way. + - **Unknown name.** A packaged field that names a picklist no loaded package declares fails the boot at `kernel:ready` with `INVALID_METADATA`, and so does a `picklistExtensions` entry that extends such a list. The error names every such field or extension and the package that declared it. After boot, an artifact registered through the `manifest` service is checked before any of it registers. A field whose list does not resolve is served with no options and accepts no value. + - **Write validation.** The write door judges a picklist-bound field against the resolved options, and its refusal names the picklist. The wire code stays `invalid_option`. The validation message catalog gains three message keys for this (`invalid_option_picklist`, `invalid_option_value_picklist`, `invalid_option_picklist_unresolved`) in en, zh-CN, ja-JP and es-ES. They change the message text only, never the wire. + - **Writing the served body back.** The served body carries `picklist` and `options` together. Writing it back through the metadata door is still refused, with the prescription to drop `options`, as `FieldSchema` declares. Nothing strips it on the write side. + - **Ledger.** `field.picklist`, the `picklist` kind's rows and `translation.picklists` are `live`. `field.picklist` no longer carries `authorWarn`, so `os lint` / `os validate` stop warning an author who writes it. +- 05a7547: fix(core,objectql)!: a `datetime` value names a year from 1000 to 9999 at both engine doors, so one before year 1000 is refused as a written value and as a filter comparand; a `date` keeps 0001 to 9999 + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the engine accepts as a `datetime`. The one range function both doors ask, `isOutsideTemporalYearRange` in `@objectstack/core`, now takes a lower bound per kind: a `datetime` starts at year 1000 (its UTC year), a `date` stays at 0001, and both still end at 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What is refused now.** A `datetime` whose UTC year falls in 0001..0999, which both doors accepted before: + + - **The write door** (the record validator), in every spelling it took: an ISO instant string, a bare `YYYY-MM-DD` (midnight UTC), a zone-naive `YYYY-MM-DD HH:MM`, and a `Date`. It answers `VALIDATION_FAILED` with the field's `invalid_date` code, on `engine.insert`, `engine.update` (one row or many) and the dry-run `engine.validate`, so on `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and each row of `POST /api/v1/data/:object/import` too. A number was already refused there, because a `datetime` is stored as text. + - **The comparand door** (the temporal-comparand door), in every spelling: an ISO string, a `Date` and epoch milliseconds as a number. It answers `INVALID_FILTER` / 400 at `where`, at a per-aggregation `filter` and at `having`, on the engine and on `POST /api/v1/data/:object/query`. The analytics native-SQL strategy asks the same predicate, so it declines such a comparand and the query reaches this door. + - The year is the instant's UTC year: `1000-01-01T00:00:00+08:00` is year 999 in UTC and is refused, and `0999-12-31T23:00:00-02:00` is year 1000 in UTC and is read. + + **What an author sees.** The comparand refusal names the field, the value, its position and the range: "an instant whose UTC year falls outside the years 1000 to 9999, the years a datetime value may name". It then says why the floor sits at 1000, instead of the misorder words that a year past 9999 still gets: MySQL documents its `DATETIME` from year 1000 only, and reads one stored in the years 0001 to 0099 back a century late. A comparand in those years that was already refused for its spelling or its day (a non-ISO spelling, a day that does not exist) is now named by its year first, as one past 9999 already was. The write refusal is the existing `invalid_date` sentence for a `datetime` field. + + **Why.** MySQL documents `DATETIME` from year 1000, and it reads a stored `DATETIME` in 0001..0099 back a century late through its client's instant parser (`0009-03-04 10:00` comes back as `2004-09-03T10:00Z`), which ADR-0053 D-F2 keeps. The range is the contract on every backend, so SQLite, PostgreSQL and the in-memory driver, which held these years, refuse them too. No writer or query of a `datetime` before year 1000 was found. + + **A `datetime` already stored before year 1000.** Nothing rewrites it, and nothing shifts it into the range. It reads back as before, and on MySQL a year in 0001..0099 still presents a century late. To find such rows, filter the field with `$lt` on `1000-01-01T00:00:00.000Z`, the floor's first instant, which both doors admit; the comparison runs on the stored value, so it finds them on MySQL as well. An update that leaves the field out is accepted. A write that carries a year below 1000 is refused, so the field can be written again with an instant from year 1000 on, or with `null`, and the author decides which. + + **Unchanged.** A `date` keeps 0001..9999, padded to four digits as before. A `time` column still reads the time of day of an instant in 0001..0999, and a `time` comparand refused for another reason keeps that reason's words. Every year from 1000 to 9999 on a `datetime`, and every refusal outside 0001..9999 on either kind, answers as before, apart from the range the words name. +- 97005ae: fix(objectql)!: a plain object with no `$` operator where a scalar field's value belongs is refused with `INVALID_FILTER` / 400 at `where`, a per-aggregation `filter` and `having`, on every driver + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a filter may put beneath a scalar field. A plain object with no `$`-operator key — `{ "amount": { "a": 1 } }`, `{}` included — where a value of a field that holds scalar values belongs is refused by the engine before any driver is asked, where the in-memory driver answered it with no records (every record under `$not`) and the SQL driver refused it in its own words. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + The judged fields are the spec's scalar-valued classes: every type in `SCALAR_FILTER_HEAD_TYPES` (text-like, numeric, boolean, date, datetime, time, single-option, `autonumber`, `summary`), with or without `multiple: true`, and the multi-option types (`multiselect`, `checkboxes`, `tags`). A `having` column is judged by the type it carries: a `count` / `sum` / `avg` is a number, a groupBy or `min` / `max` column the type of its field, a date bucket a date or text label. + + The refusal names the field, its declared type, the object's keys and the position (`where.amount`, `aggregations[1].filter.amount`, `having.total`). No mechanical rewrite exists, because which value or operator the caller meant is not in the object; the fix is one line by hand: compare the field with a value (`{ "amount": 12 }`) or an operator (`{ "amount": { "$gt": 12 } }`), and to filter by a related record, name a relation field. + + Measured through `engine.find` / `engine.aggregate` and `POST /data/:object/query`, three rows: + + | position | filter | before: memory · SQLite · PostgreSQL 16 | now, on all three | + |:--|:--|:--|:--| + | `where` | `{ amount: { a: 1 } }` (number), `{ title: { a: 1 } }` (text), and the select, boolean, date, autonumber, multi-select and `multiple: true` select twins | no records · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400, the engine's words | + | `where` | `{ $not: { amount: { a: 1 } } }` | every record · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400 | + | per-aggregation `filter` | `{ amount: { a: 1 } }`, `{ amount: {} }` | count 0 on all three | `INVALID_FILTER` / 400 | + | `having` | `{ total: { a: 1 } }` (a `sum`), `{ title: { a: 1 } }` (a groupBy) | no group on all three | `INVALID_FILTER` / 400 | + | `where` | control: `{ owner: { region: "NA" } }` on a `lookup`, `{ meta: { a: 1 } }` on a `json` field | no records / one record · the driver's 400 · the driver's 400 | unchanged: reaches the driver as written | + + **Who is affected.** A caller that sends a no-operator object beneath a scalar field to the in-memory driver — a test suite, a local or embedded deployment on `InMemoryDriver`, a flow or hook calling the engine in-process — and read the empty answer as a real one. On `SqlDriver` the same filter was already a 400, now in the engine's words; at the per-aggregation `filter` and `having` it was a silent count of 0 or an empty group set on every driver. No existing test in `@objectstack/objectql` or `@objectstack/rest` sent the shape: both suites pass with no fixture changed. + + **Unchanged.** A relation field (`lookup`, `master_detail`, `user`, `tree`, single or multiple) keeps its nested-relation form, and a structured-JSON field (`json`, `composite`, `address`, …) its object comparand; both reach the driver as written, which answers them as before. File and media fields, `formula` (refused one door earlier, `INVALID_FIELD`), undeclared keys, a `{ $field }` reference and every operator bag are not judged by this refusal. A `Map` or a class instance keeps the comparand-type door's refusal in its own words. +- 2473e26: fix(core,objectql)!: a temporal filter comparand is refused with `INVALID_FILTER` / 400 exactly when the same value is refused as a written value — a day that does not exist (`"2026-02-30"`) is no longer rolled over or compared as text, and a non-ISO `datetime` spelling (`"07/15/2026 10:00"`) is no longer read in the server's zone (#20549); and a `time` comparand whose instant has no four-digit UTC year (`"+010000-01-01T10:00:00Z"`) is refused rather than compared as text (#20480) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a `date`, a `datetime` and a `time` field accept as a filter comparand. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). + + The record validator already refused the `date` / `datetime` classes as written values (`VALIDATION_FAILED` / `invalid_date`). The comparand door was wider, so a filter admitted what a write refused and answered the wrong rows. The two predicates moved into `@objectstack/core`'s `isUninterpretableTemporalComparand`, and both doors now ask that one rule. A comparand is refused with `INVALID_FILTER` / 400, naming the field, before any driver read, at `where`, a per-aggregation `filter` and `having`: + + - **A day that does not exist**, on a `date` or as the day part of a `datetime`: `"2026-02-30"`, `"2026-02-29"` (2026 is not a leap year), `"2026-04-31"`, `"2026-02-30T10:00:00Z"`. `"2028-02-29"` is a real day and is read. + - **A `datetime` string in any spelling but the ISO 8601 ones the platform writes**, after trimming: `YYYY-MM-DD` (midnight UTC); `YYYY-MM-DDTHH:MM[:SS[.fraction]]` followed by `Z`, a `±HH:MM` or `±HHMM` offset, or nothing (a zone-naive wall clock is UTC, ADR-0074); and `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no zone. Refused now, for example: `"07/15/2026 10:00"`, `"2026/07/15 10:00"`, `"15 July 2026 10:00"`, `"07/08/2026"`, `"Wed, 15 Jul 2026 10:00:00 GMT"`, `"2026-07-15 10:00:00+08:00"` (write it with a `T`), and a bare integer string such as `"2026"` or `"1784109600000"`. + - **An instant on a `time` column in either class above.** A `time` column reads a comparand that is not a bare wall clock as an instant, by the `datetime` rule, and keeps its UTC time of day — so `"07/15/2026 10:00"` was the host zone's time of day, and `"1784109600000"` a string of epoch milliseconds. A wall clock (`"10:00"`, `"10:00:00.5"`), an ISO instant, a `Date` and an epoch-millisecond number are read as before, in a four-digit year (next). + - **An instant on a `time` column whose UTC year has no four-digit spelling**, in every spelling (#20480): `"+010000-01-01T10:00:00Z"`, `"-000001-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` (year 10000 in UTC), and the epoch-millisecond number or `Date` of any of them. A `time` column keeps the UTC time of day of an instant only when that instant spells a four-digit year; any other one reached the driver as written and was compared with the stored `HH:MM:SS` text. No time of day is read from an extended year. Year 0 (`"0000-06-15T10:00:00Z"`) spells four digits, and its time of day is read as before. + + Epoch milliseconds stay a `datetime` comparand as a JSON number: `{ "$gt": 1784109600000 }` is read exactly as before. As a string, a bare integer was read as epoch milliseconds, so `"2026"` meant two seconds after 1970 and matched every later row; send the number, or an ISO instant. + + What a caller sees through `POST /api/v1/data/:object/query`, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`: + + | `where` | memory | SQLite | PostgreSQL | now, on all three | + |:--|:--|:--|:--|:--| + | `datetime` `$eq "2026-02-30T10:00:00Z"` | 200, the row stored at `2026-03-02T10:00:00.000Z` | the same | the same | 400 `INVALID_FILTER` | + | `datetime` `$eq "07/15/2026 10:00"`, `"2026/07/15 10:00"` | 200, the row at `2026-07-15T14:00:00.000Z`, the server process's zone | the same | the same | 400 `INVALID_FILTER` | + | `date` `$eq "2026-02-30"` | 200 `[]`, compared as text | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | + | `datetime` `$gt "2026"` | 200, every row (read as 2026 epoch milliseconds) | the same | the same | 400 `INVALID_FILTER` | + | `time` `$gt "+010000-01-01T10:00:00Z"`, rows `09:00` / `10:30` / `12:00` | 200, 3 of 3 (compared as text) | the same | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | + | `time` `$gt` the number of that instant | 200 `[]` | 200, 3 of 3 | 500 `DATABASE_ERROR` | 400 `INVALID_FILTER` | + + The refusal names the field and the value, says the filter was not applied, and names the spellings that are read. The rows a non-ISO comparand matched were a property of the deployment host: the same request answered differently on two servers. + + **Who is affected.** A caller that filters a `date` or `datetime` field with a string: a REST or SDK client, a saved report or view filter, a dashboard's analytics query (the raw-SQL strategy declines such a comparand, and the engine refuses it), an MCP `query_records` call written by a model. A `{placeholder}` such as `{30_days_ago}`, the empty string, a JS `Date` and an epoch-millisecond number are unchanged. + + **Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL: + + - a real leap day: `date` `"2028-02-29"`, `datetime` `"2028-02-29T10:00:00Z"`; + - each ISO spelling above, compared as the same instant whatever the host's zone: `"2026-07-15T14:00:00Z"`, `"2026-07-15T22:00:00+08:00"`, `"2026-07-15 14:00"` (UTC, not the host zone); + - a `date` comparand with a leading real `YYYY-MM-DD`, still compared as that day (`"2026-07-15T10:00:00Z"` on a `date` is July 15); + - the same wall clock as a 2026 instant on a `time` column: `$gt "2026-07-15T10:00:00Z"` answers the `10:30` and `12:00` rows, as does its epoch-millisecond number or `Date`; + - the year range 0001..9999, and every written value (the record validator now asks the same rule it copied, and answers exactly as before). +- 63bfe69: fix(objectql)!: a `time` field is a zone-less wall clock — a time of day written with a `Z` or an offset (`"10:00Z"`, `"10:00+08:00"`), and an instant whose UTC year has no four-digit spelling (`"+010000-01-01T10:00:00Z"`), are refused with `VALIDATION_FAILED` / 400 (`invalid_time`) instead of being stored verbatim on memory and SQLite and read back differently, or failing with a 500, on PostgreSQL (#20671) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a `time` field accepts as a written value. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). + + The record validator's `time` arm now asks `@objectstack/core`'s one temporal rule, the same one the `time` filter comparand door asks, so a value is refused as a written `time` exactly when it is refused as a `time` comparand. It reads two things: a bare wall clock `HH:MM[:SS[.fraction]]` in range, and an instant in one of the ISO 8601 spellings a `datetime` is written in, on a calendar day that exists, whose UTC year has four digits (its UTC time of day is stored). Everything else is refused with `VALIDATION_FAILED` / 400 and the field code `invalid_time`, naming the field, on insert, update, a multi-row update and `engine.validate`, before anything is written. + + Refused now, where they were accepted: + + - **A time of day with a zone suffix**: `"10:00Z"`, `"10:00+08:00"`, `"10:00:00+0800"`, `"10:00:00.250Z"`. A `time` field carries no zone. The refusal has its own sentence, which `@objectstack/spec`'s validation-message catalog now carries in all four locales (`invalid_time_zoned`; English: "… is a time of day with no time zone: drop the Z or offset (HH:MM or HH:MM:SS), or use a datetime field for an instant"). The wire code stays `invalid_time`. + - **An instant the rule does not read as a time of day**: an extended year (`"+010000-01-01T10:00:00Z"`, or a `Date` of it), an instant whose UTC year is 10000 (`"9999-12-31T23:00:00-02:00"`), a day that does not exist (`"2026-02-30T10:00:00Z"`), and a spelling the `datetime` arm already refuses (`"2026-07-15 10:00Z"`, a space and a zone; `"2026-07-15t10:00:00z"`, lower case). + + What a caller sees, before and after, through `POST /api/v1/data/:object` and a read-back, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`: + + | written to a `time` | memory | SQLite | PostgreSQL | now, on all three | + |:--|:--|:--|:--|:--| + | `"+010000-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` | 201, read back as written | the same | 500 `DATABASE_ERROR` | 400 `invalid_time` | + | `"10:00Z"`, `"10:00+08:00"`, `"10:00:00+0800"` | 201, read back as written | the same | 201, read back `"10:00:00"` | 400 `invalid_time`, the zone sentence | + | `"2026-07-15 10:00Z"` | 201, `"10:00:00"` | the same | the same | 400 `invalid_time` | + + **Who is affected.** A caller that writes a `time` field as a string with a `Z` or an offset, or as an out-of-range instant: a REST or SDK client, a flow, an MCP `create_record` / `update_record` call written by a model. A row already stored with such a value keeps it; nothing rewrites it. PostgreSQL stored a zone-suffixed time of day as its bare wall clock, so only a memory or SQLite deployment can hold one. An update that omits the field is not affected; one that sends the old value back is refused, naming the field. A `time` field whose literal `defaultValue` carries a `Z` or an offset has each insert that falls back to that default refused the same way. The server import (`POST /api/v1/data/:object/import`) turns a `time` cell into `HH:MM:SS` itself before the write, and already refused a zone-suffixed time-of-day cell, so its cells are unchanged. + + **Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through REST: + + - a bare wall clock: `"10:00"` and `"10:00:00"` read back `"10:00:00"`, `"10:00:00.250"` reads back `"10:00:00.250"`; + - a full ISO instant with a four-digit year, stored as its UTC time of day: `"2026-07-15T10:00:00Z"` and `"2026-07-15T18:00:00+08:00"` read back `"10:00:00"`; + - a `Date` with a four-digit year, still accepted; an epoch-millisecond number, a `{placeholder}` and an out-of-range clock (`"25:00"`), still refused with `invalid_time`; + - every `date` and `datetime` value, and every filter comparand. +- 4b4ee88: fix(objectql)!: a plain object with no `$` operator beneath a relation field, a structured-JSON field or an undeclared `id` column is refused with `INVALID_FILTER` / 400 at `where`, a per-aggregation `filter` and `having`, on every driver + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a filter may put beneath a relation or JSON-valued field. A plain object with no `$`-operator key — `{ "owner": { "region": "NA" } }` beneath a `lookup`, `{ "meta": { "a": 1 } }` beneath a `json` field, `{}` included — is refused by the engine before any driver is asked. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What an author sees now.** `400 INVALID_FILTER`, naming the field, its declared type, the object's keys (never its values), the position (`where.owner`, `aggregations[1].filter.owner`, `having.owner`) and the route that works, inside the first 500 characters the REST door keeps: + + - **A relation field** — `lookup`, `master_detail`, `user`, `tree`, single or multiple (the nested-relation form, a condition on the related record's own fields). No data-path driver follows a relation: the field stores the related record's id. Filter the related object first, then match the field against the ids it returns — `{ "owner": { "$in": ["ID", "..."] } }` on a single-valued field, `{ "owners": { "$contains": "ID" } }` per id on a multi-valued one (an `$or` of those for several ids; the SQL driver refuses `$in` on a multi-valued column). A dotted path (`"owner.region"`) is no route: it was already refused with `INVALID_FIELD` on every driver. + - **A structured-JSON field** — `json`, `composite`, `repeater`, `record`, `location`, `address`, `vector` (a whole-value match). The drivers share no meaning for it: the in-memory driver compared documents, the SQL driver refused the bind. Test the whole value's presence with `{ "meta": { "$null": false } }`, or store the part you filter on in a field of its own and filter that field. `$contains` is no route: it was already refused over a JSON value on every driver. + - **`id`, `created_at` or `updated_at` absent from the declared field map** — the platform provisions these columns, so they are judged by the type they store (text, datetime), in the scalar-field words: compare with a value or an operator. + + No mechanical rewrite exists, because which related records or which part of the JSON value the caller meant is not in the object; the fix is by hand, as above. + + Measured through `POST /api/v1/data/:object/query`, three rows (owner `u1`, region NA, on `d1` and `d3`): + + | position | filter | before: memory · SQLite · PostgreSQL 16 | now, on all three | + |:--|:--|:--|:--| + | `where` | `{ owner: { region: "NA" } }` on a `lookup`, and its `master_detail`, multiple-lookup, `user` and `tree` twins | no records (`d1`, `d3` were meant) · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400, the engine's words | + | `where` | `{ meta: { a: 1 } }` on a `json` field, and its `address` and `composite` twins | the deep-equal records · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400 | + | `where` | `{ id: { a: 1 } }` | no records · the driver's 400 · the driver's 400 | `INVALID_FILTER` / 400 | + | per-aggregation `filter` | `{ owner: { region: "NA" } }` | count 0 on all three | `INVALID_FILTER` / 400 | + | per-aggregation `filter` | `{ meta: { a: 1 } }` | count 1 on all three (the engine's own deep equality) | `INVALID_FILTER` / 400, one answer per filter at every position | + | `having` | `{ owner: { region: "NA" } }` over a lookup groupBy | no group on all three | `INVALID_FILTER` / 400 | + | `where` | route `{ owner: { $in: ["u1"] } }`; `{ owners: { $contains: "u1" } }` | `d1`, `d3` on all three | unchanged | + + **Who is affected.** A caller that sends the nested-relation form or a JSON object comparand to the in-memory driver — a test suite, a local or embedded deployment on `InMemoryDriver`, a flow or hook calling the engine in-process — and read the empty (or deep-equal) answer as a real one; and a per-aggregation `filter` that matched a JSON value by deep equality. On `SqlDriver` the `where` forms were already a 400, now in the engine's words. + + **Supersedes** the "Unchanged" paragraph of the scalar-field refusal entry (`20546-no-operator-object-on-scalar`) for relation and structured-JSON fields: they are judged now, in words of their own. File and media fields (a legacy stored value is an inline object), `formula` (refused one door earlier, `INVALID_FIELD`), any other undeclared key, a `{ $field }` reference and every operator bag are still not judged by this refusal. +- 157baa7: fix(objectql)!: a `groupBy` on a structured-JSON field is refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `aggregate` accepts as a grouping target. A `groupBy` entry that names a declared field of the structured-JSON class (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) is refused by the engine before any driver is asked. Both entry spellings are judged, the field name and the `{ field }` object, a `dateGranularity` bucket included. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, or `groupBy[0].field` for the object form), the field and its declared type, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own. The thrown error carries `field`, `fields`, `object` and `param: 'groupBy'`. + + **Why a refusal.** The drivers share no meaning for a JSON document as a group key. Measured through `POST /api/v1/data/:object/query` over three rows with different documents under the grouped field: the in-memory driver answered 200 with one group holding every row, SQLite answered 200 with one group per serialized document, and PostgreSQL answered 500 `DATABASE_ERROR`. A `vector` field split the same three ways, and a date bucket over a `json` field answered one `null` bucket on memory and SQLite and 500 on PostgreSQL. No producer that groups by a structured-JSON field was found (no dataset, cube, view grouping or `groupBy` in the example apps names one), so no meaning is defined for it here. + + **Who is affected.** A caller of `engine.aggregate` or of the REST query door that grouped by such a field on the in-memory driver or on SQLite and read the merged or per-serialization groups as real ones. On PostgreSQL the same query was already a 500. The analytics service's aggregate path (a cube query the native-SQL strategy declines, such as a time dimension with a granularity, or any cube query on the in-memory driver) reaches the engine and answers this refusal too. + + **Unchanged.** A `groupBy` on any other type (`text`, `number`, a `multiple: true` select, a file field), a structured-JSON field as an AGGREGATED column (`count`, `count_distinct`, `min`, `max`), and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. +- ca5408c: feat(objectql): the nested-relation filter `{ relation: { field: value } }` is served in `where`, lowered at the engine's filter seam — the drivers receive `$in` / `$contains` and are unchanged + + Clause-②: yes (widening) + + A condition on a related record's own fields, written beneath a relation field of the queried object — `{ "account": { "industry": "tech" } }` beneath a `lookup` — is now answered by the engine in `where`, on every verb that takes one (`find`, `findOne`, `count`, `aggregate`, `update`, `delete`) and by `judgeFilter`. It was refused with `INVALID_FILTER` / 400 until now; this supersedes the relation-field paragraph of the pending `20745-nested-object-door` entry. + + **How it is answered.** The engine reads the related object with the condition, then matches the relation field against the ids that read returns, and the drivers receive only that: `{ "account": { "$in": [ids] } }` on a single-valued relation, and on a multi-valued one (`multiple: true`) an `$or` of one `$contains` per id, so it matches on any member. The relation types are `lookup`, `master_detail`, `user` and `tree`. It composes as written inside `$and` / `$or` / `$not`, and the `FilterArray` sugar lowers to it too. No related record matching selects no rows; under `$not`, a record whose relation is empty satisfies the negation. + + **As the caller.** The related read is the engine's own `find` on the related object with the caller's execution context, so that object's access check, row scope and field permissions apply exactly as they do to a direct read of it. A condition on a field the caller cannot read is refused by the same check that refuses a direct filter on it (`PERMISSION_DENIED` / 403, naming the field), never answered with an empty list; a related record the caller cannot see matches no condition. + + **Bounded.** At most `RELATION_FILTER_ID_CAP` (1,000, exported) related ids feed one condition. A condition matching more is refused with `INVALID_FILTER` / 400, naming the cap, the related object and the two-step route — never run over a cut-off list. + + **Still refused, in the engine's words (`INVALID_FILTER` / 400, before any read):** a second level (a relation condition beneath the related object's own relation field, or a dotted key inside the condition), a key the related object does not declare, an empty condition `{}`, and a related object that is not registered. An aggregation's own `filter` and `having` keep refusing the form, and their words now name `where` as the place it is served. The dotted spelling `{ "account.industry": "tech" }` stays refused with `INVALID_FIELD` / 400, and its words now name the nested form to write instead. The structured-JSON and scalar-field refusals are unchanged. + + Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 (owner `u1`, region NA, on `d1` and `d3`; `d4` has no owner): + + | `where` | before | now | + |:--|:--|:--| + | `{ owner: { region: "NA" } }` on a `lookup`, and its `master_detail` and multiple-lookup twins | `INVALID_FILTER` / 400 | `d1`, `d3` | + | `{ parent: { title: "a" } }` on a `tree` field | `INVALID_FILTER` / 400 | `d2`, `d3` | + | `{ $not: { owner: { region: "NA" } } }` | `INVALID_FILTER` / 400 | `d2`, `d4` | + | `{ owner: { region: "APAC" } }` (no owner matches) | `INVALID_FILTER` / 400 | no rows | + + SQLite, PostgreSQL and the in-memory driver match the element of a multi-valued relation, so an id that is a substring of another stored id (`u1` inside `u10`) does not match it. +- b280546: A caller-supplied value for a `formula` field is stripped on every engine write path, in every context, and reported through `droppedFields` / `onFieldsDropped` under a new `reason`, `computed`; and `ObjectQL.validate` runs the write's own field doors, so a dry run built on it predicts what the write will do (#20805). + + Clause-②: yes (narrowing) + + + + **BREAKING**: `ObjectQL.validate` now refuses a row that carries a key the object does not declare, exactly as `insert` and `update` refuse it: the call throws `INVALID_FIELD` / 400 naming the field. It used to answer `valid: true` for that row while the write it previews refused it, so the protocol's `validateData` and the import dry run built on it said "ok" for rows the commit then failed. It ships as `minor` under the launch-window convention; the widening half is the new `reason` arm. + + **A formula value is stripped, never refused.** A `formula` field is computed on every read and no driver has a column for it, so a full read returns the key and a record written back carries it: a form save, a flow's `update_record`, a `GET` then `PUT`. The key used to reach the driver, and the driver decided. On SQL drivers the whole write failed with the driver's own error (`SqliteError` "table … has no column named …", with no `status` and no `field`; the REST door relabelled it `400 INVALID_FIELD` "Unknown field" for a field the object declares). On the in-memory driver the value was stored as a shadow column nothing reads. Now the engine takes the value out before the defaults, the hooks and the other strips, completes the write, and reports one `{ reason: 'computed' }` event per call. That holds on `insert` (one row or a batch), `insertMany`, and `update` by id and by predicate, on every driver, and in every context, `isSystem` included: there is no column for any caller's value to land in. Measured on SQLite and the in-memory driver through `protocol.createData`, `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and `engine.update`: each now answers success with `droppedFields: [{ fields: ['doubled'], reason: 'computed' }]`, the stored row carries no such key, and the read still returns the computed value. + + - **`computed` is not `readonly`.** `isSystem` exempts the static `readonly` strip and does not exempt this one. A `formula` field also declared `readonly: true` is reported once, as `computed`. + - **`strictReadonlyWrites` refuses it.** That option's coverage is derived from what `onFieldsDropped` reports, so a caller that passes it and sends a formula value now gets `ERR_READONLY_FIELD_REJECTED` with a `computed` drop in `drops`, and nothing is written, `isSystem` included. The refusal message names the reason and its remedy; a refusal without a `computed` drop reads exactly as before. + - **Hooks are handed the payload that will be stored.** A `beforeInsert` / `beforeUpdate` hook no longer sees the formula key in `ctx.input.data`; `ctx.submitted` on update still carries the caller's submission as sent. + - **Consumers of `DroppedFieldsEvent['reason']` must handle `computed`.** The contract requires a branch on `reason` to be exhaustive. In this release the strict refusal message (`@objectstack/objectql`) and the flow `create_record` / `update_record` step warning (`@objectstack/service-automation`) word it. + + **What `validate` runs now.** Before judging a row, `ObjectQL.validate` runs the write's own doors, by the same functions the write calls: the declared-field door (the refusal above), the computed-field strip, and the caller-write strips, under the write's `isSystem` gate (on `insert` mode the runtime-owned strip and the static `readonly` strip with its re-default; on `update` mode the static `readonly` strip, where a supplied `id` is the address the write binds and is never judged). What the write would drop is reported through a new optional `onFieldsDropped` listener on `validate`'s options, in the same events the write emits. One consequence for verdicts: a reference field declared static `readonly` is now stripped in the preview as it is on the write, so a validation rule that reads through it answers the same on both. + + **Unchanged.** A `summary` field keeps its column: a caller-supplied roll-up value is still stored as sent and overwritten by the next write of a child record. The REST layer's own handling of a missing column (schema drift) is unchanged. +- 975b248: fix(objectql,spec)!: a `groupBy` on a multi-value field and a `count_distinct` on a JSON-stored field are refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver, and the aggregate × field-type table stops accepting `count_distinct` over the JSON-stored types + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, in two positions, on every driver and for every caller that reaches the engine (the REST query door, a flow or hook, and the analytics strategy that lowers a cube query onto `engine.aggregate`). Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + - A `groupBy` entry that names a **multi-value** field: an inherently-multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. Both entry spellings are judged, the field name and the `{ field }` object. + - A `count_distinct` aggregation over a **JSON-stored** field: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently-multi option type, or a multi-capable field declared `multiple: true`. + + **BREAKING** (`@objectstack/spec`): `AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct` no longer lists the ten JSON-stored types (the structured-JSON seven and `multiselect`, `checkboxes`, `tags`), so `isAggregateCompatibleWithFieldType('count_distinct', type)` answers `false` for them. Every reader of the table refuses those pairs now: the dataset-measure lint rule (`measure-aggregate-field-type-refused`, run by `os validate` and at a runtime dataset save), the analytics dataset compile leg (`400 DATASET_INVALID`), and the engine door above. The `count` row is unchanged. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, `groupBy[0].field`, or `aggregations[0].field`), the field and its declaration, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps. For a multi-value field the route is to filter by one member: `where` with `$contains` on the field, one query per member. For a structured-JSON field it is to store the part you count in a field of its own, or to count rows with `count`. The thrown error carries `field`, `fields`, `object` and `param` (`groupBy` or `aggregations`). + + **Why a refusal.** Every SQL driver stores these values in a JSON column, and the drivers share no meaning for one as a group key or a distinct key. Measured through `POST /api/v1/data/:object/query` over three rows: grouping by any of the eight multi-value declarations answered one group per array on the in-memory driver, one group per serialized array on SQLite, and 500 `DATABASE_ERROR` on PostgreSQL 16. `count_distinct` over any structured-JSON or multi-value field answered 3 on the in-memory driver (equal values counted apart), 2 on SQLite (serialized text compared), and 500 on PostgreSQL (no equality operator for `json`). No example app and no published stack groups by a multi-value field or counts one distinct, so no meaning is defined for either here. + + **What to write instead.** A dataset measure or a query that counted a JSON-stored field distinct: use `count` over it, or store the scalar part you meant to count in a field of its own and `count_distinct` that field. A grouping by a multi-value field: filter by each member with `$contains` and count. + + **Who is affected.** A caller that grouped by a multi-value field, or counted a JSON-stored field distinct, on the in-memory driver or on SQLite and read the answer as a real one; on PostgreSQL both were already a 500. A dataset whose measure pairs `count_distinct` with a JSON-stored field is refused by the lint rule and the compile leg. + + **Unchanged.** (Two shapes the structured-JSON `groupBy` entry of this same release lists as unchanged are narrowed here: a `multiple: true` select as a group key, and `count_distinct` over a structured-JSON field. This entry is the later word on both.) A `groupBy` or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; the `having`, filter and sort positions; and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. + + `@objectstack/lint`: the dataset-measure refusal's hint no longer says `count_distinct` accepts every type. + + `@objectstack/service-analytics`: the dataset compile leg's refusal of a `count_distinct` measure over a JSON-stored field says why it diverges (the drivers compare the values for equality three ways) and prescribes `count`, or a scalar field for the part being counted; its other refusals no longer say `count_distinct` accepts every type. +- dcd3309: fix(core,objectql)!: a relative-date placeholder that resolves outside its field's years is refused `INVALID_FILTER` / 400, naming the placeholder and the year it resolved to, instead of reaching the driver and answering the wrong rows + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the engine answers for a filter carrying a relative-date placeholder. A date macro is resolved after the temporal-comparand door, which steps around a placeholder, so the year range that door asks of a literal never saw the value one resolved to. It does now, through the same function, core's `isOutsideTemporalYearRange`, by the column's kind: a `date` takes the years 0001 to 9999 and a `datetime` 1000 to 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What is refused now.** A date macro whose resolved value falls outside its column's years, on a declared `date` or `datetime` field (or, on a `time` field, one that resolves to an instant whose UTC year has no four-digit spelling), at `where` (on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`), at a per-aggregation `filter`, at `having` (by the aggregated column's kind), and through `judgeFilter`. On REST that is `POST /api/v1/data/:object/query` and every other door that reads through the engine. Measured before this on InMemoryDriver and SqlDriver on SQLite, over a `datetime` field with a row in 2026 and a row in 1500: + + - `$gt {8000_years_from_now}` answered both rows, and the right answer was none; + - `$lt {2027_years_ago}` answered the 1500 row, because the resolver spelled year -1 as `-1-10-01` and that text was read as a day in 2001, and the right answer was none; + - `$lt {1977_years_ago}` resolved to year 49, below the `datetime` floor of 1000, which now applies to a resolved placeholder as it does to a literal; + - on a `time` field, `$gt {8000_years_from_now}` answered every row: the `time` rule keeps no time of day from an instant whose UTC year has no four-digit spelling, so it compared as text. Such a placeholder is refused now in the words a literal of that instant gets. + + **What an author sees.** The refusal names the field, the placeholder as written, its position, the value it resolved to and that value's year, in the temporal-comparand door's words for the year class: `filter on 'opened_at' compares a declared datetime field against "{8000_years_from_now}" at where.opened_at.$gt, a relative-date placeholder that resolved to "+010026-10-01" (the year 10026), an instant whose UTC year falls outside the years 1000 to 9999 …`. It ends by asking for a placeholder whose offset lands inside those years. + + **The resolver's spelling** (`@objectstack/core`). A date macro that lands on a day outside 0001..9999 now resolves to that day in the expanded-year form of ECMAScript's date time string format, `+010026-10-01` or `-000001-10-01` (year 0 is `0000-10-01`). It used to take the storage rule's unpadded spelling, `10026-10-01` or `-1-10-01`, which `Date.parse` reads through the host's legacy parser in the host's zone, so a day in year -1 read as one in 2001 and could not be judged. Every consumer of `resolveFilterToken` and `resolveFilterTokens` sees the new spelling for such a day only. A day inside 0001..9999 and a sub-day placeholder's instant are spelled as before. + + **Unchanged.** A placeholder that resolves inside its column's years answers as before; a `date` keeps the years 0001 to 0999, which a `datetime` refuses, and a `time` field reads the time of day of any instant with a four-digit year, year 0 included. A placeholder on a column with no temporal kind (text, number) and a context placeholder such as `{current_user_id}` are not judged by this range. Every literal comparand answers as before. +- a75311d: fix(objectql)!: the engine's `aggregate` asks the aggregate × field-type table for every aggregation over a declared field, so `min` / `max` / `avg` over a type the table refuses answer `INVALID_FIELD` / 400 on every driver instead of one answer per driver + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, on every driver and for every caller that reaches the engine — the REST query door, a flow or hook, a roll-up summary's recompute, and the analytics strategy that lowers a cube query onto `engine.aggregate`. Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + FROM → TO, per aggregation `{ function, field }` naming a declared field: + + - `min` / `max` over a type outside the numeric, temporal and boolean classes — the structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), the multi-option types (`multiselect`, `checkboxes`, `tags`), the string family (`text`, `email`, `url`, `phone`, …), the option and reference types (`select`, `radio`, `lookup`, `master_detail`, `tree`, `user`), `autonumber`, the file family and `formula` — and over any `select`, `lookup`, `user`, `file` or `image` declared `multiple: true`: FROM whatever the driver answered (a document or an array in memory, the serialized text on SQLite, a 500 on PostgreSQL for a JSON-stored field; a collation-dependent string for a text field) TO `400 INVALID_FIELD`. + - `avg` over a type outside the numeric and boolean classes — a `date`, `datetime` or `time` field included: FROM `null` in memory, a coerced number on SQLite (the average YEAR for a datetime), a 500 on PostgreSQL, TO `400 INVALID_FIELD`. + - `count_distinct` is unchanged: it was already refused over the JSON-stored types, in the same words. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`aggregations[0].field`), what the function does and the field with its declaration (`takes the max of 'meta', a declared json field — a structured-JSON value`), saying the query was not run, and naming the types the function accepts, read off the table, inside the first 500 characters the REST door keeps. The thrown error carries `field`, `fields` (every offending aggregation), `object` and `param` (`aggregations`). + + **Why a refusal.** `AGGREGATE_FIELD_TYPE_COMPATIBILITY` already declares which pairs every backend answers the same way, and the dataset compile and lint legs refuse the rest; the engine door asked only its `count_distinct` row. Measured through `engine.aggregate` over two rows: `max` over a `json` field answered `{ a: 1 }` in memory, the string `'{"b":1}'` on SQLite and 500 `DATABASE_ERROR` on PostgreSQL 16 (`function max(json) does not exist`); a `tags` field and a `multiple: true` select or lookup split the same way; `avg` over a `datetime` answered `null`, `2026` and a 500. One query, three answers. + + **What to write instead.** Aggregate a field of a type the function accepts — for `min` / `max`: `number`, `currency`, `percent`, `rating`, `slider`, `progress`, `summary`, `date`, `datetime`, `time`, `boolean` or `toggle`; for `avg`: the same minus the temporal three. A question that was counting in disguise is `count` (or `count_distinct` over a scalar-stored field). A first or last record by a text value is a sort on a list, not an aggregate. A quantity stored as text or JSON belongs in a numeric or temporal field of its own, aggregated there. + + **Who is affected.** A caller that asked `min` / `max` / `avg` of such a field on the in-memory driver or SQLite and read the answer as a real one; on PostgreSQL a JSON-stored field was already a 500. Metadata that lowers onto `engine.aggregate` takes the same verdict at run time: a roll-up summary (`summaryOperations`) whose `min` / `max` / `avg` names such a child field records a failed recompute, a grouped list view's server-side header summary is refused, and a chart or metric component's `aggregate` over such a field is refused. No example app and no published stack authors such a pair. + + **Not judged yet: `sum`.** The `sum` row of the table is held back at this door: a published stack authors a `sum` column summary over a `formula` field, a pair the table refuses, so that row awaits its own decision. `sum` over any field reaches the driver as before. + + **Unchanged.** Every pair the table accepts; `count` over any field, a JSON-stored one included; an aggregation that names no field; an undeclared name or a relationship path, which this door does not judge (the REST door answers an unknown name `INVALID_FIELD` before the engine is reached); a field whose declared type is outside `FieldType`. The structured-JSON `groupBy` entry of this same release lists a structured-JSON field as an aggregated `min` / `max` column as unchanged; this entry is the later word on that shape. + + `@objectstack/spec`: the TSDoc of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` no longer says the engine's `aggregate` door reads only the `count_distinct` row. The table itself is unchanged. +- d98bf24: fix(objectql)!: the engine's `aggregate` judges the `sum` row of the aggregate × field-type table too, so `sum` over a type the table refuses answers `INVALID_FIELD` / 400 on every driver instead of `0` in memory and on SQLite and a 500 on PostgreSQL + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, on every driver and for every caller that reaches the engine — the REST query door, a flow or hook, a roll-up summary's recompute, and the analytics strategy that lowers a cube query onto `engine.aggregate`. Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + This completes the change of this same release that made the engine's `aggregate` ask the table for `min`, `max` and `avg`, and that held the `sum` row back. Its paragraph "Not judged yet: `sum`" is superseded: this entry is the later word, and the door now asks every row of the table. + + FROM → TO, per aggregation `{ function: 'sum', field }` naming a declared field: + + - `sum` over a type outside `number`, `currency`, `rating`, `slider`, `progress`, `summary`, `boolean` and `toggle` — a `percent` (a rate does not add), the temporal types (`date`, `datetime`, `time`), the string family (`text`, `email`, `url`, `phone`, …), the option and reference types (`select`, `radio`, `lookup`, `master_detail`, `tree`, `user`), `autonumber`, the file family, the structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), the multi-option types (`multiselect`, `checkboxes`, `tags`) and `formula` — and over any `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`: FROM whatever the driver answered TO `400 INVALID_FIELD`. Measured through `engine.aggregate` over two rows: a `json`, `text`, `select` or `tags` field summed to `0` in memory and on SQLite and answered 500 `DATABASE_ERROR` on PostgreSQL 16 (`function sum(json) does not exist`); a `datetime` field summed to `0` in memory, to the years added on SQLite and a 500 on PostgreSQL; a `formula` field summed to `0` in memory and was already refused `400 INVALID_FIELD` by both SQL drivers, which have no column for it; a `percent` field added the rates on all three. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`aggregations[0].field`), what the function does and the field with its declaration (`sums 'meta', a declared json field — a structured-JSON value`), saying the query was not run, and naming the types `sum` accepts, read off the table, inside the first 500 characters the REST door keeps. The thrown error carries `field`, `fields` (every offending aggregation), `object` and `param` (`aggregations`). + + **What to write instead.** Sum a field of a type `sum` accepts: `number`, `currency`, `rating`, `slider`, `progress`, `summary`, `boolean` or `toggle`. A rate stored as a `percent` is averaged (`avg` accepts it), or the quantity it is a rate of is summed. A value computed by a `formula` is stored in a numeric field of its own when it must be summed on the server. A question that was counting in disguise is `count`. + + **Who is affected.** A caller that asked `sum` of such a field on the in-memory driver or SQLite and read the `0` as a real total, and a caller that summed a `percent` field on any driver. On PostgreSQL the other measured pairs were already refused (a 500, or a 400 for a `formula`), and on SQLite so was a `formula`. Metadata that lowers onto `engine.aggregate` takes the same verdict at run time: a roll-up summary (`summaryOperations`) whose `sum` names such a child field records a failed recompute, a grouped list view's server-side header summary is refused, and a chart or metric component's `sum` over such a field is refused. No example app authors such a pair. One published stack authors a `sum` list-column summary over a `formula` field; that summary is computed client-side and does not reach `engine.aggregate`. + + **Unchanged.** Every pair the table accepts, `sum` over the eight types above included; `count` over any field; an aggregation that names no field; an undeclared name or a relationship path, which this door does not judge (the REST door answers an unknown name `INVALID_FIELD` before the engine is reached); a field whose declared type is outside `FieldType`. + + **A correction to the earlier entry of this release.** It listed the multi-capable types declared `multiple: true` as `select`, `lookup`, `user`, `file` or `image`; the list is `select`, `radio`, `lookup`, `user`, `file` and `image` (`MULTI_CAPABLE_TYPES`). A `radio` declared `multiple: true` was refused by `min` / `max` / `avg` there all the same, by its type's own row, and it is refused by `sum` here. + + `@objectstack/spec`: the TSDoc of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` states that the engine's `aggregate` door asks every row of the table, where it said "the other rows" while one was held, and names `radio` among the multi-capable types. The table and the predicate are unchanged. +- 657b6b7: The dry run and the partial-success batch insert now say which row lost which field. `ObjectQL.validate` (and `validateData`, which relays it) answers `droppedFields` on each accepted row of `results`, and `ObjectQL.insertMany` (and `insertManyData`, which passes it through) answers `droppedFields` on each `ok` outcome: the caller-supplied fields the engine legally strips from that row, one `DroppedFieldsEvent` per reason, in the engine's own reason vocabulary (`computed` for a `formula` value, `readonly` for a static `readonly` or runtime-owned field). The key is absent when nothing was taken from the row. + + - **Recorded at the strips, never inferred from the union.** Each strip records what it takes from each row as it runs. A `beforeInsert` hook that assigns a protected key on one row keeps it there, so that row is not named, while a sibling row that supplied the same key and lost it is. + - **A row the write does not complete carries none.** A preview row the verdict refuses, and an `ok: false` outcome, carry no `droppedFields`: a drop means the write completed without the field. + - **The dry run and the commit agree.** On `insert` mode the preview runs the same strips the write runs, so a row's preview drops and its outcome drops are the same list. One gap is unchanged: the preview runs no hooks, so a key a `beforeInsert` hook assigns is reported by the preview and kept by the write. An `update`-mode preview does not run the `readonlyWhen` or primary-key strips, which judge a prior record the preview does not read. + - **Unchanged:** the `onFieldsDropped` listener on `insert`, `insertMany` and `validate` still reports the batch-level union, one event per reason, naming no row. So does `insertManyData`'s top-level `droppedFields`. `insert(object, rows[])` still returns the records, with no per-row slot. `strictReadonlyWrites` still refuses the whole batch before any outcome is built. + + Graded `minor` in both packages: each widens a published method's declared answer with a new optional key (`InsertManyRowOutcome` gains `droppedFields`, and so does each outcome of `insertManyData`'s return type), which is an additive widening of the public surface. Nothing is removed, renamed or refused. The keys on the wire, `ValidateDataResponseSchema.results[].droppedFields` and `ImportRowResultSchema.droppedFields`, were already declared in `@objectstack/spec`. +- c35436c: fix(objectql)!: a per-aggregation `filter` and a `having` refuse a non-boolean `$exists` / `$null` with `INVALID_FILTER` / 400, in the words every driver's `where` refuses it in, instead of reading `$exists` by truthiness and dropping `$null` (#20981) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `aggregate` accepts in two positions, `aggregations[i].filter` and `having`, on every driver. A `$exists` or `$null` comparand that is not a boolean (a string such as `"false"`, a number, `null`, an array) is now refused with `INVALID_FILTER` / 400, before any driver is asked for a row, so an empty table refuses it too, at any depth under `$and` / `$or` / `$not`. A plain object or `undefined` there is refused first by the comparand-type check, in its own words, as before; a `{ $field }` reference there, already refused as a reference outside a scalar comparison, is now refused in this entry's words. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way, per row: it throws the same refusal for a row its per-aggregation filter judges on the flag, with or without a `fields` map (an empty `rows` array, or a row a `$or` branch settles first, is not judged there; `engine.aggregate` judges the whole filter once before any row). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **Why a refusal.** `FieldOperatorsSchema` declares both flags as booleans, and every driver's `where` refuses any other comparand. The engine evaluates a per-aggregation `filter` and a `having` itself, and it read one anyway. Measured through `engine.aggregate` on the in-memory driver and on `SqlDriver` (SQLite), with identical answers: `$exists` was read by truthiness, so `"yes"`, `1` and the string `"false"` selected the rows and groups WITH a value, and `0` / `null` the ones without; and `$null` tested only `true` / `false`, so any other value constrained nothing, and every row and every group came back. + + **What an author sees now.** The message `driver-sql` gives the same flag, beginning `Operator "$exists" on field "FIELD" requires a boolean comparand (true or false).`, naming what arrived and the position (`aggregations[1].filter.stage.$exists`, `having.stage.$null`). Unlike a `where` on `SqlDriver`, the field and the value are not withheld: a per-aggregation `filter` and a `having` never carry a merged read scope. + + **What to write instead.** Write the boolean itself. `"$exists": true` and `"$null": false` match a field that has a value; `"$exists": false` and `"$null": true` match one that has none. + + **Who is affected.** A caller that reaches `engine.aggregate` without the REST query door's schema parse (server-side code, a flow or hook, the analytics bridge that lowers a dataset measure's filter into an aggregation filter, a host calling `applyInMemoryAggregation` directly) and read the count as a real answer. `POST /api/v1/data/:object/query` already refused all three positions with 400 `VALIDATION_FAILED` before the request reached the engine, and still does. + + **Unchanged.** `$exists: true` / `false` and `$null: true` / `false` answer exactly as before, on both positions. `$empty` and every other operator, and `where`. +- a11faee: fix(objectql)!: a per-aggregation `filter` refuses `$in` / `$nin` / `$eq` / `$ne` / an ordering / `$between` / implicit equality on a declared JSON-stored field with `INVALID_FILTER` / 400, in the words `where` refuses them in, instead of counting rows the stored arrays cannot support + + Clause-②: yes (widening) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts in one position, `aggregations[i].filter`, on every driver and for every caller that reaches the engine: the REST query door (`POST /api/v1/data/:object/query`), a flow or hook, and the analytics strategy that lowers a dataset measure's filter onto `engine.aggregate`. The published `applyInMemoryAggregation(rows, ast, timezone, fields)` narrows the same way when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What is refused.** On a field the object declares JSON-stored (a structured-JSON type such as `json` or `address`, an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`), a per-aggregation `filter` that compares the field with `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin` or implicit equality (`{ "owners": "u1" }`) is refused with `INVALID_FILTER` / 400, whatever the comparand (`null` and an empty list included), at any depth under `$and` / `$or` / `$not`, and before any driver is asked for a row, so an empty table refuses it too. That is the set `driver-sql`'s `where` refuses on such a column, for the same reason. + + **What an author sees now.** The same 400 body the same filter gets as a `where`: the filter WAS NOT APPLIED, the comparison can never equal one member of a stored list, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership, or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message, as they are for `where`, and the full diagnostic, naming both and the aggregation position, goes to the server log. + + **Why a refusal.** The engine evaluates a per-aggregation filter itself, and it compared the whole stored array against a scalar. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16 over six rows of a `multiple: true` lookup, two of them holding `u1`: `{ owners: { $in: ['u1', 'u9'] } }` counted 0, `{ owners: { $nin: ['u1', 'u9'] } }` counted all 6, the two rows it was asked to exclude among them, `$gt` / `$lte` / `$between` counted 4 / 1 / 5, and `{ tags: { $eq: 'red' } }` counted the row holding `['red']` by JS loose equality. The same filters in `where` were 400 on both dialects. + + **Who is affected.** A dashboard, report, dataset measure or caller whose per-aggregation filter compares a JSON-stored field with one of those operators and read the count as a real answer. Also a host calling `applyInMemoryAggregation` directly with a `fields` map: it now judges each `aggregations[i].filter` against that map before any row (an empty `rows` array included) and throws the same `INVALID_FILTER` / 400. It takes an optional fifth argument, `reportWithheld(diagnostic)`, which receives the withheld field, operator and position; without it the diagnostic is dropped. A call without `fields` judges nothing, as before. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. + + **Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored; `having`; `where`; and a host whose engine has no declaration for the object, where nothing is judged. + + **`@objectstack/core`** (three new root exports): `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, `jsonColumnOperatorRefusalText(field, op, bare)` and its return type `JsonColumnOperatorRefusalText` (`{ message, diagnostic }`). They are the operator set and the two texts (the withheld message and the full diagnostic) of the JSON-column refusal, so `driver-sql`'s `where` and the engine's per-aggregation filter refuse with one set and one sentence. + + **`@objectstack/driver-sql`**: no behaviour change. Its JSON-column gate reads the set and the text from `@objectstack/core`; every refusal it prints is byte for byte what it printed before. +- 2c1cef3: fix(core)!: a filter that aims `$startsWith`, `$endsWith`, `$icontains`, `$like` or `$ilike` at a field stored as a JSON column is refused with `INVALID_FILTER` / 400, as `$eq` / `$in` / `$nin` already are, instead of matching the field's serialized text or failing at query time + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows which filters are answered on a field stored as a JSON column, on every face that reads `@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`: `driver-sql`'s `where` (and `driver-sqlite-wasm` and `driver-turso`'s local transport, which inherit it) on every read and write face that lowers a filter, and the engine's per-aggregation `filter`. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What is refused.** On a field declared multi-valued (an inherently multi-value option type such as `tags`, `multiselect` or `checkboxes`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`) or structured-JSON (`json`, `address`, …), a filter using `$startsWith`, `$endsWith`, `$icontains`, or the staged pattern pair `$like` / `$ilike`, is refused with `INVALID_FILTER` / 400, at any depth under `$and` / `$or` / `$not`. The per-aggregation `filter` refuses the three declared ones; it already refused `$like` / `$ilike` as operators it does not evaluate. Through the engine, a structured-JSON field was already refused all seven text operators by the text-operator declared-type door, which still answers first there, in its own words; what moves for it is a direct driver call. + + **What an author sees.** The body the equality family already gets there, byte for byte: the filter WAS NOT APPLIED, and the spelling to use, `{ "FIELD": { "$contains": "a" } }` for membership or an `$or` of `$contains` for any-of. The field and the operator are withheld from the message and named in the server-log diagnostic; a filter positively marked as the caller's own reads them named. + + **Why a refusal.** Such a column stores the serialization `["u1","u2"]`, and none of these five operators has a membership reading. Measured through `POST /api/v1/data/:object/query` on a multi-value lookup and a `tags` field: on SQLite `$startsWith: "["` and `$endsWith: "]"` matched every row with a value, `$startsWith: "u1"` matched none of the rows holding `u1`, and `$icontains: "U1"` also matched the row holding only `u10`; on PostgreSQL 16 every one failed at query time with a `500` `DATABASE_ERROR`, a `json` column having no `LIKE` operator; the per-aggregation `filter` counted 0 for each. No membership reading is invented for a prefix, suffix or case-folded test. + + **Who is affected.** A saved filter, list view, dashboard widget, report or caller that aims one of these operators at a multi-valued or JSON-stored field. On SQLite it read rows that matched the stored brackets and quotes; it now gets the 400. On PostgreSQL it already failed, with a 500. Write `$contains` for "holds this member", an `$or` of `$contains` for "holds any of these", and `$not` around either for the exclusion. + + **`@objectstack/objectql`: `$search` over a multi-valued field answers by membership.** The search expander (`$search` on `find`, `findOne` and `aggregate`, the REST `search` / `$search` parameter included) used to emit `$in` for a term matching a `select` option label and `$icontains` for any other term, against every field in the resolved search set. On a multi-valued field both are refused by the gate above, so one such field in the set failed the whole search: a label term answered 400 on every dialect, and any other term answered 500 on PostgreSQL and, with this change, 400 on SQLite. The auto-default set includes a `select` declared `multiple: true`, as in `examples/app-todo`'s `todo_task.tags`, and `searchableFields` may name a `tags` field or a multi-valued lookup. Such a field is now matched by membership: a term matching option labels becomes one `$contains` per matched option value, and any other term, or any term on a field with no options, becomes `$contains` of the term. No search answers 400 or 500 for it any more. **The visible cost:** to hit a multi-valued field, a term must now equal one of its members or match one of its option labels; SQLite used to match substrings of the stored array's serialized text as well, so a term like `wood` found a row tagged `redwood`, and it no longer does. Scalar fields are searched exactly as before. + + **Unchanged.** `$contains` and `$notContains` (membership on such a field), `$exists`, `$null` and `$empty`; every operator on a field that is not JSON-stored, the scalar text column included; `driver-memory`; and `driver-turso`'s remote transport, which compiles its own filters. +- cb45469: fix(service-analytics)!: the analytics native-SQL strategy declines an object an engine middleware is registered for, so the engine serves it and that object's read gates apply; the engine answers which objects carry one (`IObjectQLEngine.hasObjectMiddleware`) (#21080) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics doors serve for one class of query. It ships as `minor` under the launch-window convention for narrowings. + + **What changes.** On a SQL driver, `NativeSQLStrategy` compiled a query to SQL and ran it through the driver's raw-SQL seam, so no engine operation ran and no engine middleware did. It applied the security service's object admission and read filter and nothing else, so the read gates that live in the engine as per-object middlewares did not apply there: a caller admitted to such an object at object level read grouped results and counts over every row, rows about parent records that caller cannot read included. It now declines a query that reads (as its base object, a declared join, or through a relationship path) an object the data engine holds a middleware registered for. The ObjectQL strategy serves it through the engine with the caller's context, so the engine's middlewares run, and the analytics answer for that caller equals the data door's. On the stock composition the objects that move off the native path are `sys_comment`, `sys_activity` and `sys_attachment` (read gates), `sys_approval_request` (the snapshot redaction), and `sys_user_position` and `sys_permission_set` (write-side middlewares, which move as a side effect: a middleware does not declare its operation). No shipped dataset or dashboard reads any of them. + + **What is newly refused.** A query on such an object that the ObjectQL strategy cannot serve is refused with that strategy's existing `400`, where the native strategy used to serve it: for example a dimension reached through a relationship path combined with a measure that cannot be recombined across it (`avg`, `count_distinct`). Correctness wins over the fast path for a gated object. + + **It fails closed.** `AnalyticsServicePlugin` asks the data engine. An engine without `hasObjectMiddleware`, or no engine, cannot say, and the strategy declines then too: every query on such a host is served by the ObjectQL strategy, and the plugin says so once at `warn`. A host that constructs `AnalyticsService` with `executeRawSql` and without the new `hasObjectMiddleware` config member keeps the native path for every object and is told so once at construction. + + **New, additive.** `IObjectQLEngine.hasObjectMiddleware?(objectName): boolean` (`@objectstack/spec`), `ObjectQL.hasObjectMiddleware(objectName)` (`@objectstack/objectql`): whether a `registerMiddleware(fn, { object })` names the object; a global registration (no `object`, or `'*'`) is keyed to none and is not counted. `AnalyticsServiceConfig.hasObjectMiddleware` (`@objectstack/service-analytics`), which the plugin fills from the data engine. + + **Unchanged.** Objects no middleware names keep the native path. The middleware chain, `registerMiddleware` and every gate are unchanged. + + **What to do after upgrading.** Nothing on the stock composition. A host whose `"data"` service is not ObjectQL should implement `hasObjectMiddleware` to keep the native path for ungated objects. A host that builds `AnalyticsService` itself with `executeRawSql` should pass `hasObjectMiddleware` from its engine. +- 336e191: fix(security)!: stored metadata bodies are projected or refused at the audit, analytics, realtime and data-door filter/sort exits too + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what three doors accept or serve for the two stored-metadata tables — the generic data door refuses a filter or sort on the body column, the analytics door refuses it as a dimension / measure / filter / sort member, and the realtime event and the audit/activity copy now carry the body as its type's read projection instead of the stored bytes. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** + + - **Audit / activity copy (`@objectstack/plugin-audit`).** The audit writer copies a `sys_metadata` / `sys_metadata_history` row into `sys_audit_log.new_value` / `old_value` and `sys_activity.metadata`. That copy now projects the body through the shared redactor, so stored credential material is withheld from the second store too. A new `os migrate audit-metadata-bodies` command rewrites the copies already at rest (dry run by default, `--apply` to write, idempotent). + - **Analytics (`@objectstack/service-analytics`).** A query naming the stored body column of these objects as a dimension, measure, filter or sort is refused with `400 INVALID_FIELD`, before any strategy runs — the posture analytics already takes for a member it will not evaluate. + - **Realtime (`@objectstack/objectql`).** A `data.record.*` event projects its `after` / `changes` body through the same redactor, so a subscriber to these objects' events receives no stored credential. + - **Data door filter / sort (`@objectstack/metadata-protocol`).** A filter or sort on the body column is refused with `400 INVALID_FIELD`, the same family and shape as the existing groupBy refusal. + + **What stays answerable.** Every scalar column of these objects — `type`, `name`, `scope`, `state`, timestamps — is still grouped, filtered, sorted, counted and served; only the body column is affected. Every other object is unchanged. + +### Patch Changes + +- c9d234c: The number-comparand refusal now says "a numeric aggregated column" at `having`, and names PostgreSQL's server error only where a driver actually binds the comparand + + Clause-②: no + + **Two false phrases, at two positions.** At `having`, filtering a `count` / + `sum` / `avg` result (or a groupBy column) against a non-numeric comparand + answered `filter on 'total' compares a declared number field …` — `total` is + the aggregated row's own column, not a declared field of the object; the + verdict is handed the numeric class the column belongs to, which has no + `FieldType` of its own. And at `having` and the per-aggregation `filter`, the + `not-a-number`, `boolean` and `date` clauses each named "(PostgreSQL with a + server error)", a fact about `where`: the engine evaluates both of those + clauses itself, on every driver, before any row is read, so a comparand there + never reaches a driver bind and PostgreSQL never answers it. + + **Measured, unchanged: the per-aggregation `filter`'s column IS a declared + field.** That position narrows the object's RAW rows before any aggregation + runs, against the object's real field map — so its refusal keeps "a declared … + field", exactly as `where`'s does. Only the PostgreSQL clause moves there, + because the engine evaluates that position itself too. + + **FROM** `filter on 'total' compares a declared number field against "abc" at + having.total.$gt, which is not a number: it has no numeric reading, and + backends answer it differently (PostgreSQL with a server error). …` + + **TO** `filter on 'total' compares a numeric aggregated column against "abc" + at having.total.$gt, which is not a number: it has no numeric reading. …` + + The `where` message is unchanged, byte for byte, and so is the accept set: no + comparand that was refused before is now accepted, and none that passed is now + refused. This is a wording fix. + + **What moved to carry it.** `NumberComparandRefusalSite` (`@objectstack/spec`) + gains two optional fields the engine door already knew and now passes along: + `aggregated` (the column is an aggregated-row column, not a declared field — + `having` sets it; `where` and the per-aggregation `filter` do not) and + `boundByDriver` (this position reaches a live driver bind — `where` alone sets + it true; unset defaults to `true`, so a site built before this change, or any + caller who never sets these fields, renders exactly as it always has). + `@objectstack/objectql`'s door passes both explicitly at each of its three + call sites; no second rule and no driver-level change. +- 5a23096: Warnings, refusals and hints that cited a tracker number now say what was decided + + Clause-②: no + + Several runtime strings an author or operator reads sent the reader to an issue-tracker number for + the reason behind them. Each now states that reason in the sentence itself: + + - `@objectstack/objectql`: the two data-event warnings. A write that names no single record publishes + no per-record event rather than one with an empty `recordId`; a predicate (`multi: true`) write + publishes its own `data.records.*` event carrying the affected-row count and nothing else, so a + driver result that is not a count publishes no bulk event either. + - `@objectstack/service-automation`: the warning for a pausing node type that never declares + `resumeAuthority`, the generic-route resume refusal (its log line and its error text), and the + refusal of a suspension from a type that declares `supportsPause: false`. An undeclared + `resumeAuthority` resolves to `'service'` (fail-closed), so the generic resume route refuses those + pauses; guessing `'any'` is how a raw resume once walked past an approval decision no service had + recorded. + - `@objectstack/runtime`: the endpoint step's `NOT_IMPLEMENTED` message and its two hints (the + composed runtime always threads the policy context and the execution wiring, because execution is + reachable only past the policy chain), and the endpoint mapping refusals (the publish gate rejects + the same shapes, so a declaration that reaches the runtime check was stored without passing it). + + Text only: no error code, field name, status or behaviour changes. +- a94f3ba: objectql refusals, log lines and metadata text no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages the query engine shows to authors, administrators and operators ended with an + issue-tracker number where the reason belonged. The number goes, and where the sentence did not + already say what was decided, it now does: + + - Refusals: the bulk update and bulk delete row-scoping refusals now name the seed they are missing + (the AST seeded before the middleware chain, which RLS and sharing compose their row-scoping onto, + so a bulk write reaches only the rows the caller may edit); the hook-target rebind refusal says + why `delete()` stopped honouring a rebind (a handler that silently redirects which row gets + deleted is a trap) and names the `dispatchUnscopedMultiWrite` registration the whole-operation + dispatch goes to, on update and delete alike. The unknown-option, filter-array, + credential-aggregation, HAVING-operator, empty-hook-target, strict read-only and system-write + organization refusals lose only the citation, because their sentences already said it. + - Metadata text: the lifecycle `retention_overrides` setting description and the search companion + field description lose their citation. + - Log lines: the non-atomic cascade warning says a single-datasource cascade is now one + transaction; the system-ledger transaction line calls the ledger the one class carved out of the + cross-datasource write refusal; the dangling-reference audit summary says findings are reported, + never rewritten, because a system-context write is exempt from the write-time reference check; + the legacy `apiMethods` warning says the authorable values are the six primitives only, every + other operation being derived from them or retired; the two unevaluable-rule warnings say such a + rule fails closed and is never skipped. The ADR-0104 value-shape gate lines, the delegated + protocol-assembly line and the read-only and runtime-owned strip warnings lose only the citation. + + The `findOne` no-predicate refusal keeps its citation for now: `@objectstack/metadata-core` + carries a byte-identical copy that this package's tests compare against, and both move together. + + Text only: no error code, field name, status or behaviour changes. +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- b785c3b: fix: `sum` / `avg` answer the same double on every face the platform owns, added with one compensated fold that `@objectstack/core` now exports as `compensatedSum` (#20544) + + Clause-②: yes + + **New export.** `@objectstack/core` exports `compensatedSum(nums)`: the sum of + `nums`, added in order with Kahan-Babuska-Neumaier compensation, which is the + summation SQLite (3.43 and later) uses for its own `sum` and `avg`. It moved + here from `@objectstack/objectql`'s rows path (`in-memory-aggregation.ts`), + which now imports it instead of keeping a private copy. + + **What changed.** Three folds still added a group's values naively, and now call + the same function: + + - `@objectstack/driver-memory`'s `aggregate()` and `find()` with aggregations, + the path `engine.aggregate` takes on an in-memory datasource; + - `@objectstack/driver-memory`'s analytics face (`MemoryAnalyticsService`), + whose `sum` / `avg` measures are now a `$group` `$accumulator` in place of + mingo's `$sum` / `$avg`; + - `@objectstack/service-analytics`' draft preview. + + Over a `number` column holding `0.1`, `0.2` and `0.3`, each of them answered + `0.6000000000000001` / `0.20000000000000004`. They now answer `0.6` / + `0.19999999999999998`, as SQLite and the engine's rows path do. Over + `1e16, 1, -1e16` they answered `0` and now answer `1`. On driver-memory, + `engine.aggregate` gave two answers depending on its path: `having { s: { $eq: + 0.6 } }` kept the group on the rows path and dropped it on the native path. It + now keeps it on both. + + **What did not move.** Two addends, integers whose running total stays within + 2^53, and a non-finite total give the same answer as before. Which values count + as addends did not change either: booleans as 1 / 0, and nulls and non-numeric + strings left out, as each face already had it. `count`, `min` and `max` are + untouched. The analytics face's pipeline dump (`result.sql`) now renders the + accumulator's functions by name, so a `sum` measure and an `avg` measure still + dump differently. + + **Residual.** PostgreSQL and MySQL add their doubles natively without + compensation, and the platform does not wrap that arithmetic. So over three or + more fractions their native path can still differ from these faces in the last + place. An exact `$eq` on a fractional sum compares doubles; compare with a range. +- 4bf4e7e: Provenance comments in `@objectstack/objectql` cite the commits and ADRs that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes, or the + ADR that records it (ADR-0029 D9.2a, ADR-0104's 2026-09-05 addendum, ADR-0126 §7.2, ADR-0130 D3). + Some of these docblocks sit on exported members, so the reworded text appears in the published + `index.d.ts` / `index.d.mts`, `core.d.ts` / `core.d.mts` and the shared type chunk, and comments that + esbuild keeps appear in the JavaScript output. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- cd6d8a5: fix(objectql,platform-objects,metadata-protocol)!: the platform's `sys_migration` primary-key lookups go through `findOne`, so an existing deployment no longer prints "Paged read of 'sys_migration' is NOT deterministic" on every boot and every `os migrate plan` (#20648) + + Clause-②: no (narrowing) + + + + The deployment ledger is read one row at a time, by primary key. Five readers + spelled that read as `find(sys_migration, { where: { id }, limit: 1 })`: the + engine's migration-gate read (`readMigrationFlagVerified`, behind + `haveFileColumnsMoved`, `isFileReferencesMigrationVerified` and + `isValueShapesMigrationVerified`), the engine's deviation marker and + creation-attestation revocation, `readDataMigrationFlag` in + `@objectstack/platform-objects/system`, and the seed-tenancy repair's receipt. + The SQL driver cannot tell that read from page one of a walk. The engine's gate + read runs at boot before the schema pass registers `sys_migration` with the + driver, and on a table the driver has not registered an unsorted paged read + warns that its pages may repeat or skip rows. Measured on a SQLite database + created by 17.4.0: every 17.5.0 boot and every `os migrate plan` printed that + warning once, for a lookup that cannot return two rows. All five readers now use + `findOne`, the single-row route the driver already exempts. The driver's check is + unchanged: an unsorted `limit` read on a table the driver did not create still + warns. + + **BREAKING**: this narrows what two published engine interfaces accept. The + first is `MigrationFlagEngine` in `@objectstack/platform-objects/system`. It is + the parameter type of `readDataMigrationFlag`, `isDataMigrationVerified`, + `mayActIrreversibly`, `recordDataMigrationRun`, `recordFileColumnMove` and + `attestFreshDatastore`, and part of `FilesToReferencesEngine` in + `@objectstack/service-storage`. The second is `SeedTenancyLedger` in + `@objectstack/metadata-protocol`, the type of a `SeedTenancySeam`'s `ledger`. + Each now requires `findOne` where it required `find`, so a hand-written stand-in + that provides only `find` no longer satisfies either type. It ships as `minor` + under the launch-window convention for accept-set narrowings. The ObjectQL engine + has both methods, so a host that passes the engine needs no change. + + **Your fix:** a stand-in that implemented `find` for these helpers implements + `findOne(object, options)` instead, answering the row whose `where.id` matches, + or `null`. + + At run time, a stand-in that still provides only `find` fails the read. + `readDataMigrationFlag` then answers `null`, the same answer as a missing row, so + the gates it feeds stay closed. `resolveSeedTenancySeam` now attaches a `ledger` + only for a host that has `getObject`, `findOne`, `insert` and `update`. For a + find-only host the seam's `ledger` is `undefined`, and when the seed-tenancy + repair applies, it says at `warn` that it could not record its receipt. +- 8460592: fix: the whole-day bound on a bare `YYYY-MM-DD` upper bound is applied at the seams only — `DatabaseLoader.queryHistory` in driver mode becomes one, the engine seam lowers type-blind for an object with no field map, and `InMemoryDriver` drops its own copy (ADR-0053 D-D1 items 5 and 7, #20822) + + Clause-②: no + + - **`@objectstack/metadata` — `DatabaseLoader.queryHistory` in driver mode lowers its own filter.** With a raw `IDataDriver` (`MetadataManager.setDatabaseDriver`) the history filter reaches the driver without passing any seam. The loader now runs the shared `lowerFilterCondition` (`@objectstack/spec/data`) on it, typed by the history object it syncs: `until: 'YYYY-MM-DD'` reads `recorded_at < next day`, so every version recorded on that day is kept on every driver, and an instant `until` is kept as written. Engine mode is unchanged (the engine's own `where` seam lowers it). Before this, the whole day was kept only by each driver's own copy of the rule; with `@objectstack/driver-memory`'s copy deleted below, `until` = today would have gone from every version of the day to none. + - **`@objectstack/objectql` — an object with no field map is lowered type-blind.** The engine's `where` seam (on `find`, `findOne`, `count`, `update`, `delete` and `aggregate`'s `where` / `aggregations[i].filter`) reads the object's declared field map and rewrites a declared `datetime` column only. For an object the registry does not hold there is no declaration to read, and the seam now applies the whole-day rules to every column (a bare-day `$lte` becomes `$lt` the next day, a `$between` splits), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the declared type. It used to leave such an object to each driver's own copy. Visible on `SqlDriver`: a bare-day `$lte` on a non-`datetime` column of an unregistered object that holds ISO instant text now keeps the whole day; a `datetime` or `date` column answers as before. An object with a field map is unchanged. + - **`@objectstack/driver-memory` — `InMemoryDriver` compiles the comparison it is handed.** Its four copies of the whole-day rule are deleted (the `$lte` and `$between` arms of the filter translator, the `<=` and `between` arms of the AST-node translator). A read through the engine hands it a `where` the engine's seam has already lowered, so on that path a declared `datetime` column keeps the whole named day, and a declared `date` column answers as before. A row-level security `using` filter is not lowered by the engine's seam: the security middleware ANDs it into the query's `where` after that seam has run, and only the RLS compile seam lowers it, rewriting just the columns its field guard declares `datetime`. Two answers converge on what `SqlDriver` already returns (ADR-0053 D-D1 item 7's scope): on a registered object, a bare-day `$lte` / `$between` on a declared `text` column holding ISO instant text, or on a column the object does not declare, is now compared as written, where this driver used to widen it to the whole day. One path narrows outside those two: an RLS `using` policy with a bare-day upper bound, on an object whose declared fields the security plugin cannot resolve, is compiled with no field guard, so the RLS compile seam reads no column as `datetime` and the bound reaches this driver as written, where this driver used to widen it to the whole day; that holds until #20822 group 2 makes the RLS compile seam type-blind when it has no guard. A direct `find()` that passed no seam gets the comparison it wrote (item 5); lower the filter with `lowerFilterCondition` first to keep the whole-day reading. +- e18fea6: fix(objectql,driver-mongodb,formula): the `having` and per-aggregation evaluator compiles the whole-day comparison it is handed, and `$contains` asks membership on a JSON-stored field in `MongoDBDriver` and in `matchesFilterCondition` (ADR-0053 D-D1 items 5 and 9; the `FILTER_OPERATORS` `$contains` contract, #20822) + + Clause-②: no + + - **`@objectstack/objectql`: the aggregate evaluator's own whole-day copy is deleted.** The walker behind `having` and `aggregations[i].filter` no longer widens a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, on a `datetime` column to the whole day, and no longer drops the bound on `9999-12-31`. Through `engine.aggregate` nothing changes: the engine's seam lowers both positions with the shared `lowerFilterCondition` (`@objectstack/spec/data`) before the walker runs, by the object's declared `datetime` fields for the per-aggregation `filter` and by the aggregated column's type for `having`. A caller that passes no seam gets the comparison it wrote: `applyInMemoryAggregation(rows, ast, tz, fields)` called directly now counts `{ at: { $lte: '2026-02-01' } }` against that day's midnight. To keep the seam's reading on a direct call, lower each `filter` first with `lowerFilterCondition(filter, { isDatetimeColumn })`. + - **`@objectstack/driver-mongodb`: `$contains` / `$notContains` ask membership on a declared JSON-stored field.** On a field `syncSchema` recorded as `multiple: true`, a multi-option type (`tags`, `multiselect`, `checkboxes`) or a JSON type, `translateFilter` (every verb, and the aggregation `$match`) now emits an array-only `$elemMatch` over the members the comparand names, with the candidate rule the SQL dialects bind (`jsonMembershipCandidates`, `@objectstack/core`): `'1'` names the string `'1'` or the number `1`, `'true'` the string or `true`. It used to emit a `$regex`, which MongoDB applies to each element, so `{ owners: { $contains: 'u1' } }` matched a stored `['u10']` and `{ tags: { $contains: 'red' } }` a stored `['redwood']`. `$notContains` is the exact complement, and still admits a row with no value. A scalar column, and a field whose declaration the driver does not hold (an object never synced, a standalone `translateFilter` call), keep the substring `$regex`. + - **`@objectstack/formula`: `matchesFilterCondition` asks membership of a JSON-stored column.** When the caller supplies `options.fields` and it names the column, the declaration decides: membership on a JSON-stored column, substring on any other. Otherwise the stored value decides: an array asks membership, anything else substring. A stored array used to fail `$contains` and pass `$notContains` whatever it held. + - **The RLS write check, which evaluates a policy with this function, moves with it.** Under a `check` such as `record.tags.contains('x')` on a multi-valued field, a write whose post-image holds `['x']` (a row the same policy's read shows) is now admitted; it was refused `PERMISSION_DENIED` / 403. `['xy']` stays refused, and the read hides it. + - A scalar written to a declared multi-valued field is judged as written, before the write door wraps it in a list. So `tags: 'xy'`, which the check used to admit while the read hides the stored `['xy']`, is now refused 403. And `tags: 'x'` is now refused 403 too, although the read shows the stored `['x']`. Send the list, `tags: ['x']`. + - **`@objectstack/spec`: docblock only, in the shipped `src/data/filter.zod.ts`.** The three pointers to the deleted `SqlDriver.calendarDayUpperBoundRewrite` / `calendarDayBetweenRewrite` now name the shared `lowerFilterCondition` at the seams, and the `FILTER_OPERATORS` `$contains` implementation-status list gains `driver-mongodb` and `formula`. No schema, type or export changes. + - No exported name changes. +- f6ccca4: fix(objectql,rest): a `date` or `datetime` value refused for its year says so — "must be a date in the years 0001 to 9999" / "must be a datetime whose UTC year falls in the years 1000 to 9999" — instead of "must be a valid date (ISO-8601)", which was false for a value such as `0500-07-15T10:00:00Z` (#20846) + + Clause-②: yes (widening) — one new export on `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`. No value's verdict moves and no wire key moves: the field code stays `invalid_date` and its `constraint` stays `{ type }`. + + `POST` / `PATCH /api/v1/data/:object` and each row of `POST /api/v1/data/:object/import` + refuse a `date` outside the years 0001 to 9999 and a `datetime` whose UTC year falls + outside 1000 to 9999. When the value itself is readable — an ISO 8601 string such as + `0500-07-15T10:00:00Z` or `+010000-01-01`, or a `Date` — the refusal's message now + names the kind's years. An author who read "not valid ISO" rewrote the spelling, and no + spelling of that year is admitted. + + - `@objectstack/spec`: the validation message catalog gains `invalid_date_range` and + `invalid_datetime_range` in `en`, `zh-CN`, `ja-JP` and `es-ES`. They are two more + sentences of the `invalid_date` code, never a wire value. The years are the template + parameters `{{firstYear}}` / `{{lastYear}}`. A deployment that overrides a message + under `validation.field.invalid_date` or `validation.field.invalid_datetime` does not + cover these values. To override their text, define + `validation.field.invalid_date_range` / `validation.field.invalid_datetime_range`. + - `@objectstack/core`: `SUPPORTED_TEMPORAL_YEARS` (`{ date: { first: 1, last: 9999 }, + datetime: { first: 1000, last: 9999 } }`, frozen) is the range + `isOutsideTemporalYearRange` judges by. It is exported so a refusal names the range + from the source the doors use, never a copy of its numbers. + - `@objectstack/objectql` and `@objectstack/rest`: the record validator and the import's + cell reader choose the range sentence for such a value. An import cell with more than + four year digits (`+010000-01-01`) is refused by the import's reader. It used to read + "is not a valid date" and now gets the same range sentence as the write door. + + **What is not affected.** Which values are refused is unchanged, and so is the refusal's + code (`invalid_date`) and `constraint`. A value that is not readable keeps its sentence: + "must be a valid date (ISO-8601)" at the write door, `"…" is not a valid date` at the import. + So does a number, which is never a written `date` or `datetime`. +- d67b942: fix(objectql): a per-aggregation `filter` with `$contains` / `$notContains` on a multi-valued field counts the rows the same `where` finds (#20873) + + Clause-②: no + + `engine.aggregate({ aggregations: [{ …, filter }] })` — and so `POST /api/v1/data/:object/query` + with a per-aggregation `filter` — evaluates that filter in the engine, not in the driver. Its + `$contains` arm failed every value that was not a string, so on a `multiple: true` lookup, + `multiselect`, `checkboxes` or `tags` field a stored array never matched: + `{ owners: { $contains: 'u1' } }` counted 0 on every driver where the same condition as a `where` + found 2 rows, and `$notContains` counted every row, the rows holding the member included. + + On a declared JSON-stored field (a multi-valued field, or a structured-JSON type) both operators + now ask MEMBERSHIP, the reading `FILTER_OPERATORS`' `$contains` docblock declares and `where` gives + on every SQL dialect: `'u1'` is a member of `['u1', 'u2']` and not of `['u10']`, and a member stored + as a number or boolean is named by its text (`'1'` finds `[1, 2]`). `$notContains` is the exact + complement, and a row with no value still satisfies it. A scalar text field keeps the substring + test, unchanged, and so does `having`. + + No query that was refused now answers, and none that answered is refused: only the count of a + per-aggregation `filter` on a multi-valued field moves, to the `where` count. +- 682873d: fix(core): the refusal a filter gets for a scalar comparison or text operator on a multi-value or JSON field reads true on every backend that prints it, and reaches a REST caller whole + + Clause-②: no + + The `INVALID_FILTER` / 400 refusal `driver-sql`'s `where`, the engine's per-aggregation `filter` and `driver-memory` all print (`jsonColumnOperatorRefusalText`) explained itself with `driver-sql`'s storage ("a field this driver stores as a JSON TEXT column") and the two wrong answers SQL used to give. That is untrue on the engine and on `driver-memory`. The message was also 748 characters, and the REST envelope cuts a 4xx message at 499 plus an ellipsis, so callers on SQLite and PostgreSQL read `…Refused rather than compiled because the answ…` and never reached the sentence saying the field and the operator were withheld. + + The message now reads, on every backend, in 486 characters: `A constraint in this filter WAS NOT APPLIED: it aims a scalar comparison or text operator at a multi-value or JSON field, which it cannot test for one member.`, then the same `$contains` / `$or` of `$contains` remedy, then `For no value, use "$null" or "$empty".` (a `null` comparand such as `{ f: null }`, `$eq: null` or `$ne: null` is refused too, and `$contains` could not express it), then `The field and the operator are withheld from the message; the full diagnostic is in the server log.` The diagnostic (the server-log text, and what a filter's own author is shown) gives the same reason with the operator named, names the field, and spells the remedy with the field's name. It drops the storage and the SQL history too, and is now whole on the wire for field names up to 26 characters (it was 643 characters or more and always cut). + + Code, status, the refused operator set and the `$contains` remedy are unchanged. A client that matched on the old words `JSON TEXT column` or `Refused rather than compiled` should match on `code: "INVALID_FILTER"` instead. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- d2bc644: fix(plugin-security): a row-level `check` judges a lone scalar written to a declared multi-valued field as the one-member list it is stored as, so the write and the read the same policy scopes give one answer for one row (#21238) + + Clause-②: yes (widening) + + The write door stores a lone scalar sent to a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `lookup` / `user` / `file` / `image` flagged `multiple: true`) as a one-member list: `tags: 'x'` is stored as `["x"]`. The row-level write `check` judged the value as sent on the insert and on a by-id update, because both images are formed before the write door runs. Measured through `ObjectQL.insert` with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set, with the same predicate as `using` and `check`: + + | `check` | written | write, before | stored | read | + |---|---|---|---|---| + | `record.tags.contains('x')` | `'x'` | 403 | `["x"]` | shown | + | `!record.tags.contains('x')` | `'x'` | admitted | `["x"]` | hidden | + | `record.tags.contains('x')`, a by-id update | `'x'` | 403 | `["x"]` | shown | + + Now the image's value on every field the object declares multi-valued goes through the same rule the write door stores it by, before the check is judged. The first and third rows are admitted. The second is refused: a policy that forbids a member from tagging a row `x` can no longer be passed by sending `'x'` instead of `['x']`. A lone scalar now gets exactly the verdict its stored list gets, on the insert, a by-id update and a predicate update. That includes a policy that compares such a field with a scalar comparison (`==`, `!=`, `in`, an ordering), which the read refuses with `INVALID_FILTER` / 400: there `'x'` used to get the opposite of the verdict `['x']` got, and now gets the same one. + + Unchanged: a field the object does not declare multi-valued is judged as written; a list, `null`, a blank string and an object are judged as written, as the write door leaves them; the check's comparands are left as written, since `contains` takes one member; and refusals keep their code and status (`PERMISSION_DENIED` / 403). + + **`@objectstack/core`** (one new root export, so `minor`; this export is the widening the `Clause-②: yes (widening)` line declares): `multiValueStorageForm(value)`, the rule itself. It wraps a string, a number or a boolean into a one-member list and returns every other value as the same value. `@objectstack/objectql`'s `normalizeMultiValueFields` now calls it, with no change in what the write door stores (`patch`). `@objectstack/plugin-security` is `minor` because the set of writes its check admits widens (the first and third rows above); that is a security-floor behaviour change, not the declared widening. +- cfa9315: feat(spec, objectql, plugin-security): one shared filter lowering, run once at the engine and RLS seams (ADR-0053 D-D1, amended) + + Clause-②: yes + + `@objectstack/spec/data` exports `lowerFilterCondition(filter, options?)` and its `FilterLoweringOptions` type. It is not exported from the package root entry. It is a pure `FilterCondition → FilterCondition` rewrite that applies three rules once: + + - `$between` becomes `$gte` its minimum and `$lte` its maximum. + - A `$lte` whose comparand is a bare `YYYY-MM-DD` day becomes `$lt` the next day, in the calendar-string domain. On the last supported day (`9999-12-31`) a lone `$lte` becomes `{ $null: false }`, and a `$between` keeps only its minimum. + - The NULL-polarity guards the drivers already compile. A `$ne` of a value, a `$nin` or a `$notContains` holds for a row with no value. Every leaf of a `$not` operand is made total. + + The rewrite is copy-on-write, idempotent and never refuses. A node it rewrites keeps its filter-subtree provenance mark. With `options.isDatetimeColumn` (a typed seam), the first two rules change only a declared `datetime` column. Without it they apply to every column. + + As ADR-0053 D-D1 (amended 2026-09-30) requires, the seams now run it once, after the comparand doors and after filter-token resolution: + + - **`@objectstack/objectql`** runs it on every filter position, typed by the object's declared fields. That covers `where` on `find`, `findOne`, `count`, `update` and `delete`, and `aggregate`'s `where`, `aggregations[i].filter` and `having`. `having` is typed by the aggregated row's columns, so `max` of a `datetime` field counts as a `datetime`. Drivers receive the lowered filter. A date macro such as `{today}` is resolved before the lowering reads it. + - **`@objectstack/plugin-security`** runs it on every compiled RLS policy filter (`using` and `check`), right after the two comparand faces. `SecurityPlugin` now hands the compile seam the object's declared `datetime` columns (`RlsFieldGuard.datetime`). A guard without that set treats no column as `datetime`. + + Row answers stay the same on every driver. Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input. One result changes. The engine evaluates `aggregate`'s `aggregations[i].filter` and `having` itself, and that evaluator now treats a row or group with no value the way every driver's `where` already does. It no longer counts such a row in a `$between` on a `datetime` column. It now keeps such a row under a `$not` over an ordering such as `$lt`. + + Nothing is removed or renamed, and there is nothing to migrate. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [b531c7b] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [fe463b4] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [a7d9768] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [31ed067] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [76bd58f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [4d0b9cd] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [b1aee33] +- Updated dependencies [05be352] +- Updated dependencies [250dec8] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [0c5a71b] +- Updated dependencies [75519e1] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [25f2e64] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [94990a2] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [d34aa58] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [cfad7de] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [61455de] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/metadata@17.6.0 + - @objectstack/metadata-protocol@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 7fd6467e6ad..7f169fa2ef3 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "main": "dist/index.js", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index b51062e21d4..fef7816d664 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,135 @@ # @objectstack/observability +## 17.6.0 + +### Patch Changes + +- 820d3f4: A provenance comment in `@objectstack/observability` was re-anchored + + The `SEMCONV` comment beside the retired `http_request_errors_total` entry + cited a tracker number that no longer resolves on GitHub. It now cites the + commit in this repository's history that moved `http_request_duration_ms` to + the transport seam. Comment only: no metric name, label, export, type or + runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index c5dfebc0cef..8d891d10c86 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Observability contracts and exporters for ObjectStack — MetricsRegistry, ErrorReporter, Logger plus noop/console/OTLP-HTTP exporters. Deployment-target neutral; runtime and services depend on this so the same instrumentation works on Cloudflare Workers, Node, and self-hosted Kubernetes.", "type": "module", diff --git a/packages/platform-objects/CHANGELOG.md b/packages/platform-objects/CHANGELOG.md index 4f38e4191d0..bcb47b2deea 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,288 @@ # @objectstack/platform-objects +## 17.6.0 + +### Minor Changes + +- cd6d8a5: fix(objectql,platform-objects,metadata-protocol)!: the platform's `sys_migration` primary-key lookups go through `findOne`, so an existing deployment no longer prints "Paged read of 'sys_migration' is NOT deterministic" on every boot and every `os migrate plan` (#20648) + + Clause-②: no (narrowing) + + + + The deployment ledger is read one row at a time, by primary key. Five readers + spelled that read as `find(sys_migration, { where: { id }, limit: 1 })`: the + engine's migration-gate read (`readMigrationFlagVerified`, behind + `haveFileColumnsMoved`, `isFileReferencesMigrationVerified` and + `isValueShapesMigrationVerified`), the engine's deviation marker and + creation-attestation revocation, `readDataMigrationFlag` in + `@objectstack/platform-objects/system`, and the seed-tenancy repair's receipt. + The SQL driver cannot tell that read from page one of a walk. The engine's gate + read runs at boot before the schema pass registers `sys_migration` with the + driver, and on a table the driver has not registered an unsorted paged read + warns that its pages may repeat or skip rows. Measured on a SQLite database + created by 17.4.0: every 17.5.0 boot and every `os migrate plan` printed that + warning once, for a lookup that cannot return two rows. All five readers now use + `findOne`, the single-row route the driver already exempts. The driver's check is + unchanged: an unsorted `limit` read on a table the driver did not create still + warns. + + **BREAKING**: this narrows what two published engine interfaces accept. The + first is `MigrationFlagEngine` in `@objectstack/platform-objects/system`. It is + the parameter type of `readDataMigrationFlag`, `isDataMigrationVerified`, + `mayActIrreversibly`, `recordDataMigrationRun`, `recordFileColumnMove` and + `attestFreshDatastore`, and part of `FilesToReferencesEngine` in + `@objectstack/service-storage`. The second is `SeedTenancyLedger` in + `@objectstack/metadata-protocol`, the type of a `SeedTenancySeam`'s `ledger`. + Each now requires `findOne` where it required `find`, so a hand-written stand-in + that provides only `find` no longer satisfies either type. It ships as `minor` + under the launch-window convention for accept-set narrowings. The ObjectQL engine + has both methods, so a host that passes the engine needs no change. + + **Your fix:** a stand-in that implemented `find` for these helpers implements + `findOne(object, options)` instead, answering the row whose `where.id` matches, + or `null`. + + At run time, a stand-in that still provides only `find` fails the read. + `readDataMigrationFlag` then answers `null`, the same answer as a missing row, so + the gates it feeds stay closed. `resolveSeedTenancySeam` now attaches a `ledger` + only for a host that has `getObject`, `findOne`, `insert` and `update`. For a + find-only host the seam's `ledger` is `undefined`, and when the seed-tenancy + repair applies, it says at `warn` that it could not record its receipt. + +### Patch Changes + +- addbbf0: feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518) + + Clause-②: yes (widening) + + - **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused. + - **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source. + - **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. The runtime resolves the reference onto that served field; see the picklist runtime entry of this release. + - **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package. + - **Translation.** `TranslationData` gains `picklists..{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them. + - **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES). + - **Extraction.** `os i18n extract` walks `picklists.NAME.{label, options.VALUE}`, including an extension's options under the list it extends, and `os lint` reports an untranslated option under its own rule, `i18n/missing-picklist`. + - **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column. +- fa0a4b6: fix(platform-objects,plugin-audit): Setup and Studio navigation entries for the console's Audit Log and Integrations & APIs pages (#20142) + + The console retired its System Hub card wall and its Developer Hub, which had been the only in-app links to several pages, and registered each page under a component-registry key instead. Framework navigation reaches a console page only through a `type: 'component'` item that names such a key, and no item named them, so each page was reachable only by a typed URL. Two entries now name the pages whose capability ships in the open framework: + + | Entry | App / group | `componentRef` | Contributed by | Gate | + | --- | --- | --- | --- | --- | + | `nav_audit_log_browser` ("Audit Log Browser") | Setup / Diagnostics, directly under Audit Logs | `audit:log` | `@objectstack/plugin-audit` | none: it lives and dies with the plugin that owns `sys_audit_log` | + | `nav_integrations` ("Integrations & APIs") | Studio / Developer, after Public Forms | `developer:integrations` | `@objectstack/platform-objects` | none beyond Studio's own `studio.access` | + + **Two audit entries, on purpose.** The existing Audit Logs entry (the `sys_audit_log` object view) stays. It carries the named list views, search, and the actor and tenant rendered as resolved lookups. The new page adds one filterable table whose detail drawer pretty-prints a change's before and after JSON, where the record page shows `old_value` / `new_value` as raw text. Neither surface replaces the other. + + **No entry for the console's AI Approvals page (`ai:approvals`) here.** Under ADR-0029 D7, each capability plugin contributes its own navigation entries into a Setup slot, and the Setup shell does not enumerate capability objects. The AI pending-action queue belongs to the AI capability, whose provider (`@objectstack/service-ai`) ships in Cloud/Enterprise, not in the open framework. Its entry is therefore that capability's to contribute. + + The keys are the ones the console registers at the objectui commit this release's console is built from. Labels ship in all four locales (en, zh-CN, ja-JP, es-ES), with their source hashes recorded. Nothing is removed or renamed, and there is nothing to migrate. +- 3fbf3ca: Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words + + Clause-②: no + + Many messages these packages show to authors, administrators and operators ended with an issue-tracker + number where the reason belonged. The number goes, and where the sentence did not already say what was + decided, it now does. Where an ADR stood beside the number, the ADR stays. + + - Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals + (the contract refuses loudly and names the mismatch, never coerces a value into storability), the + kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB + filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api` + source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the + function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate + refusal says the builder used to sum an unrecognised name before this refusal existed. + - The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s + `engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same. + - The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`, + `MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card; + the sentence above it already says the driver refuses rather than run or answer unisolated. + - Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN + translations), the `sys_email` headers help and the SCIM credential store's protection reason. + - Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's + excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore + attestation info lines. + + Text only: no error code, field name, status or behaviour changes. +- f4ce10c: fix(platform-objects): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20539) + + Clause-②: no + + A translated object leaf that a translator wrote by hand is kept as written + when its English source changes later, so some leaves went on saying what the + old source said. On a ja-JP, es-ES or zh-CN console the `sys_two_factor` record + page described `backup_codes` as JSON-serialized, where the English help says + the codes are one opaque ciphertext and not readable JSON. + + Nineteen leaves whose meaning contradicted the current English now match it: + + - all three locales: `sys_two_factor.backup_codes` help (an opaque ciphertext, + not JSON), the `sys_notification` description (one notification event per + `emit()`, not a per-user inbox entry), the `sys_job_run` description (job run + history, not an audit trail), and the `sys_metadata.environment_id` label + (Environment, not Project); + - es-ES only: seven `sys_business_unit` / `sys_business_unit_member` labels and + help texts that still named the business unit a department. + + Leaves whose English source only gained detail or was reworded, without + retracting what the translation says, are unchanged. Values only: no key is + added or removed, and no provenance table changes. +- 5757463: fix(platform-objects): the ja-JP, es-ES and zh-CN metadata-form descriptions, help texts and labels that contradicted their current English source are re-translated (#20666) + + Clause-②: no + + A translated metadata-form leaf that a translator wrote by hand is kept as + written when its English source changes later, so some leaves went on saying + what the old source said. On a ja-JP or es-ES console the permission-set form's + Tab & Row-Level Security section still offered custom context variables, and + the agent form's Capabilities section still offered tools; `en` dropped both + when the keys were removed. + + Twelve leaves whose meaning contradicted the current English now match it: + + - all three locales: the agent form's Capabilities section (skills and + knowledge sources, no tools), the permission-set form's Tab & Row-Level + Security section (tab visibility and RLS policies: ja-JP and es-ES no longer + offer custom context variables, and zh-CN no longer names the section after + sharing rules), and the report form's `blocks` help (dataset-bound + sub-reports, not a join of several objects); + - ja-JP and es-ES: the email-template form's Identity section (the template is + resolved by its `name` through `IEmailService.sendTemplate`, not by an `id`, + and the section carries no content type); + - zh-CN: the report form's Joined blocks section label, which named the section + after related objects. + + Leaves whose English source only gained detail or was reworded, without + retracting what the translation says, are unchanged. Values only: no key is + added or removed, and no provenance table changes. +- 31c3996: Clause-②: no + + The field form offers `useGrouping` on `number` fields: one plain boolean row beside `scale`, gated to `number` as `scale` is, whose control copies the field form's `allowCreate` row (the same `z.boolean().optional()` node, no default). The key was declared by `FieldSchema` and graded `live` by the liveness ledger once the console's number display began to answer an authored value first, but no form offered it, so an author's only door was the Source tab. The help text follows the key's own description: unset lets the renderer decide, off never groups (a year or an ID), on always groups. It also says that an untouched switch writes nothing, so it reads off even where the renderer groups. + + ⛔ **No schema accept set moves and no export changes.** What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles, whose two new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/metadata-core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index ae7fc6b188c..9115b4c94ff 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "main": "dist/index.js", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index bfacdbaf00a..7360b85716c 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,128 @@ # @objectstack/embedder-openai +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 7af5faca32c..6e89ed2fb16 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "OpenAI-compatible embedder for ObjectStack — works against OpenAI, 阿里通义 DashScope, 智谱 BigModel, 硅基流动 SiliconFlow, 火山引擎 Doubao, MiniMax, Ollama, and any drop-in OpenAI-shape endpoint.", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index 8c6a99b5148..770f3d8ae8a 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,151 @@ # @objectstack/knowledge-memory +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [e952cff] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-knowledge@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 17447e77232..cde7d4f710f 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index a4d25e1d3d2..6839e181aca 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,151 @@ # @objectstack/knowledge-ragflow +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [e952cff] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-knowledge@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index f10a1e90869..fdf76b4df1f 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "main": "dist/index.js", diff --git a/packages/plugins/organizations/CHANGELOG.md b/packages/plugins/organizations/CHANGELOG.md index 9134364c835..c09eb1d4842 100644 --- a/packages/plugins/organizations/CHANGELOG.md +++ b/packages/plugins/organizations/CHANGELOG.md @@ -1,5 +1,159 @@ # @objectstack/organizations +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [4d04b6b] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [55012df] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/plugin-auth@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/organizations/package.json b/packages/plugins/organizations/package.json index b70929b0f94..90d2b75d498 100644 --- a/packages/plugins/organizations/package.json +++ b/packages/plugins/organizations/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/organizations", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Multi-organization runtime for ObjectStack — registers the `org-scoping` service that turns single-database row-level Organization isolation on: `organization_id` auto-stamp on insert, per-org seed replay, default-organization bootstrap, and the walled-posture membership-policy gate.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index cc35e0b4cb2..6680c4a7ee9 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,204 @@ # @objectstack/plugin-approvals +## 17.6.0 + +### Minor Changes + +- 2f2fa11: fix(approvals): a snapshot field the reader is served masked on the data plane is no longer served as stored (#20964) + + Clause-②: yes (widening) + + The approval payload snapshot is redacted at serve time by the security service's read projection, `getReadableFields`. That projection counts a field whose `maskingRule` applies to the reader as readable, because the data plane serves the column with its value replaced. So the snapshot kept such a field and served it as captured at submission. The redaction now also reads the security contract's `getQueryableFields`, which differs from the read projection by exactly the fields the reader is served masked, and drops those fields, with their derived labels, on both read doors: the approvals inbox reads and the generic data door on the request object. A reader for whom the masking rule is lifted still sees the stored value. The full snapshot stays at rest. + + The field is dropped rather than masked. The contract names which fields are masked for a reader, not the masked value, and reproducing the mask in this plugin would be a second copy of the masking rule. + + The one public-surface addition is an optional `getQueryableFields(object, context)` member on the field-visibility source that `ApprovalServiceOptions.fieldVisibility` and `ApprovalService.attachFieldVisibility` accept. + + A host that constructs `ApprovalService` itself and passes its own `fieldVisibility` source: that source must now also answer `getQueryableFields` (delegate it to the `security` service). A source without it cannot say which readable fields are masked for the reader, so the redaction fails closed and serves no snapshot field. The approvals plugin's own wiring already forwards it. +- 1ecb871: fix(plugin-audit,plugin-approvals)!: a query over the activity stream's value-bearing columns, the compliance ledger's before/after snapshots, or an approval request's snapshot is refused for a reader withheld a field of the objects the query can reach — the one parent object it names, or every object when it names none (#21154) + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on three engine read middlewares, shipped as `minor` under the launch-window convention. + + **What was wrong.** An activity row carries field values of the record it is about in three columns: its one-line summary, its record label and its recorded change. A compliance-ledger row carries them in its before and after snapshots. An approval request carries the submitted record's snapshot. A read-time redaction narrows such values on the rows a reader is SERVED, after the driver has answered. A filter over the same columns was evaluated at rest, before that, so row presence answered whether the stored text held a value the reader is served masked or not at all, one guess at a time. A grouping by one of them handed the stored text back as the group key. + + **What is refused now.** For a non-system caller, on every door that reaches these objects through the engine (the list and query doors, record export, and any other `find` / `count` / `aggregate`), a query that filters, searches, sorts, groups or aggregates by one of those columns is refused with `403 PERMISSION_DENIED`, in the engine's own words for a field the caller may not query, unless the caller is served every field, as the security service answers it, of the objects the query can reach: + + - when the query names exactly one parent object, by equality on the column that names it, at the root of its filter (or inside a root `$and`): that object; + - when it names none: every object registered in the deployment, the set these rows can concern, read from metadata and never from the rows. + + A grouping or aggregation by such a column answers the engine's aggregate refusal; every other position answers its predicate refusal. Both are followed by one sentence naming the remedy. + + **Who is affected.** A caller withheld any field of the parent object (served masked, gated by a capability it does not hold, or not granted by its permission sets) can no longer filter, search, sort or group by those columns of that object's activity rows, ledger rows or approval requests. A caller withheld any field of any registered object can no longer do so in a query that names no parent object, or names one only inside an alternative — that includes a free-text search over the activity stream or the compliance ledger, whose searched sets include those columns. A caller served every field of the parent it names, or, for a query naming none, of every object (an administrator in a stock deployment), queries as before; the latter costs three security-service calls per registered object per such query. + + **One-line fix:** a caller withheld some field names one parent object it is served in full, by equality in the query's filter; for a parent it is withheld a field of, it reads the rows unfiltered by those columns. + + **Unchanged.** A query that names none of those columns answers as before, for every caller. System-context reads are not judged. A deployment without the security service answers as before: the columns are served whole there, so a filter over them discloses nothing the rows do not. The approvals service door's own search keeps its own rule for the snapshot. + +### Patch Changes + +- cbaf04c: Provenance comments in `plugin-approvals` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 3a8b8874b4d..2062b301cbf 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index addee1d80d2..cff9b56cace 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,332 @@ # @objectstack/plugin-audit +## 17.6.0 + +### Minor Changes + +- 6f57888: fix(plugin-audit)!: an engine read of `sys_activity` returns only the rows whose parent record the caller can read, the same way an engine read of `sys_comment` is narrowed + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what an engine read of `sys_activity` returns to a caller that is not system context. It ships as `minor` under the repo's launch-window convention for narrowings. + + **What changes.** `AuditPlugin` now mounts a read gate on `sys_activity`, beside the one `sys_comment` already has. It is an engine middleware, so it narrows what passes through the engine: `find`, `findOne`, `count` and `aggregate`, which on the generic data doors are the list, its `total`, the by-id read and both query shapes. There a row is returned only when the caller can read the record it is about. That answer is the one the comment gate asks: the caller's own engine read of the parent record, so the parent object's sharing, RLS and object-level permissions decide. Parent reads are batched, one per parent object. + + **Rows that are left out**, failing closed exactly as the comment gate does for its threads: + + - a row about a record the caller cannot read; + - a row about a record that no longer exists; + - a row that names no record, or names an object the engine does not know; + - a row that names `sys_activity` itself. + + **Unchanged.** A caller who can read every record (an admin) keeps every row about a record that exists. System-context reads, including the audit writer's own, are not narrowed. The object, its fields and what the CRUD mirror writes are unchanged, and nothing stored is rewritten. +- 336e191: fix(security)!: stored metadata bodies are projected or refused at the audit, analytics, realtime and data-door filter/sort exits too + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what three doors accept or serve for the two stored-metadata tables — the generic data door refuses a filter or sort on the body column, the analytics door refuses it as a dimension / measure / filter / sort member, and the realtime event and the audit/activity copy now carry the body as its type's read projection instead of the stored bytes. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** + + - **Audit / activity copy (`@objectstack/plugin-audit`).** The audit writer copies a `sys_metadata` / `sys_metadata_history` row into `sys_audit_log.new_value` / `old_value` and `sys_activity.metadata`. That copy now projects the body through the shared redactor, so stored credential material is withheld from the second store too. A new `os migrate audit-metadata-bodies` command rewrites the copies already at rest (dry run by default, `--apply` to write, idempotent). + - **Analytics (`@objectstack/service-analytics`).** A query naming the stored body column of these objects as a dimension, measure, filter or sort is refused with `400 INVALID_FIELD`, before any strategy runs — the posture analytics already takes for a member it will not evaluate. + - **Realtime (`@objectstack/objectql`).** A `data.record.*` event projects its `after` / `changes` body through the same redactor, so a subscriber to these objects' events receives no stored credential. + - **Data door filter / sort (`@objectstack/metadata-protocol`).** A filter or sort on the body column is refused with `400 INVALID_FIELD`, the same family and shape as the existing groupBy refusal. + + **What stays answerable.** Every scalar column of these objects — `type`, `name`, `scope`, `state`, timestamps — is still grouped, filtered, sorted, counted and served; only the body column is affected. Every other object is unchanged. +- 1ecb871: fix(plugin-audit,plugin-approvals)!: a query over the activity stream's value-bearing columns, the compliance ledger's before/after snapshots, or an approval request's snapshot is refused for a reader withheld a field of the objects the query can reach — the one parent object it names, or every object when it names none (#21154) + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on three engine read middlewares, shipped as `minor` under the launch-window convention. + + **What was wrong.** An activity row carries field values of the record it is about in three columns: its one-line summary, its record label and its recorded change. A compliance-ledger row carries them in its before and after snapshots. An approval request carries the submitted record's snapshot. A read-time redaction narrows such values on the rows a reader is SERVED, after the driver has answered. A filter over the same columns was evaluated at rest, before that, so row presence answered whether the stored text held a value the reader is served masked or not at all, one guess at a time. A grouping by one of them handed the stored text back as the group key. + + **What is refused now.** For a non-system caller, on every door that reaches these objects through the engine (the list and query doors, record export, and any other `find` / `count` / `aggregate`), a query that filters, searches, sorts, groups or aggregates by one of those columns is refused with `403 PERMISSION_DENIED`, in the engine's own words for a field the caller may not query, unless the caller is served every field, as the security service answers it, of the objects the query can reach: + + - when the query names exactly one parent object, by equality on the column that names it, at the root of its filter (or inside a root `$and`): that object; + - when it names none: every object registered in the deployment, the set these rows can concern, read from metadata and never from the rows. + + A grouping or aggregation by such a column answers the engine's aggregate refusal; every other position answers its predicate refusal. Both are followed by one sentence naming the remedy. + + **Who is affected.** A caller withheld any field of the parent object (served masked, gated by a capability it does not hold, or not granted by its permission sets) can no longer filter, search, sort or group by those columns of that object's activity rows, ledger rows or approval requests. A caller withheld any field of any registered object can no longer do so in a query that names no parent object, or names one only inside an alternative — that includes a free-text search over the activity stream or the compliance ledger, whose searched sets include those columns. A caller served every field of the parent it names, or, for a query naming none, of every object (an administrator in a stock deployment), queries as before; the latter costs three security-service calls per registered object per such query. + + **One-line fix:** a caller withheld some field names one parent object it is served in full, by equality in the query's filter; for a parent it is withheld a field of, it reads the rows unfiltered by those columns. + + **Unchanged.** A query that names none of those columns answers as before, for every caller. System-context reads are not judged. A deployment without the security service answers as before: the columns are served whole there, so a filter over them discloses nothing the rows do not. The approvals service door's own search keeps its own rule for the snapshot. +- 30c530e: fix(plugin-audit)!: a read of the compliance ledger returns only the rows about records the caller can read, the same way a read of the activity stream is narrowed (#21175) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a read of `sys_audit_log` returns to every caller that is not system context, admins included. Some rows an admin was served before are no longer served. It ships as `minor` under the repo's launch-window convention for narrowings. + + **What changes.** `AuditPlugin` now mounts the activity stream's parent-record read gate on the compliance ledger. It is an engine middleware, so it narrows `find`, `findOne`, `count` and `aggregate`, which on the generic data doors are the list, its `total`, the by-id read and both query shapes. A ledger row that names a record (`object_name`, `record_id`) is returned only when the caller's own engine read of that record finds it, so the parent object's sharing, RLS and object-level permissions decide. Parent reads are batched, one per parent object. The gate's mechanism is one module shared with the activity stream's gate. + + **Rows no longer served:** + + - to a caller who cannot read the record a row is about: that row; + - to every caller that is not system context, admins included, because the gate has no readable record to judge them by: + - a row about a record that no longer exists: every `delete` row, and every other row about a deleted record; + - a sign-out row, and a sign-in row whose session has since been removed (sign-out removes the session the row names); + - a create, read, update or delete row that names no record, a row naming an object the engine does not know, and a row naming the ledger itself. + + **Unchanged.** The rows stay stored, and system-context reads still return every row. Rows about no record are served as before, under the ledger's own grant: `config_change` rows, the run-level user-import row, the platform-admin standing rows, and an auth event that carried no session id. A caller who can read a record keeps every row about it, and the field-level redaction of the before/after snapshots applies to the rows that are served, as before. A broad read whose pre-scan reaches the gate's 2,000-row bound fails closed and logs a warning, as the activity stream's does. + + **Migration.** No metadata, code or configuration change is needed. A view or report that lists deletions or sign-outs from `sys_audit_log` through the data API now shows fewer rows. A server-side job that must read every ledger row reads it under system context, which this gate does not narrow. + +### Patch Changes + +- fa0a4b6: fix(platform-objects,plugin-audit): Setup and Studio navigation entries for the console's Audit Log and Integrations & APIs pages (#20142) + + The console retired its System Hub card wall and its Developer Hub, which had been the only in-app links to several pages, and registered each page under a component-registry key instead. Framework navigation reaches a console page only through a `type: 'component'` item that names such a key, and no item named them, so each page was reachable only by a typed URL. Two entries now name the pages whose capability ships in the open framework: + + | Entry | App / group | `componentRef` | Contributed by | Gate | + | --- | --- | --- | --- | --- | + | `nav_audit_log_browser` ("Audit Log Browser") | Setup / Diagnostics, directly under Audit Logs | `audit:log` | `@objectstack/plugin-audit` | none: it lives and dies with the plugin that owns `sys_audit_log` | + | `nav_integrations` ("Integrations & APIs") | Studio / Developer, after Public Forms | `developer:integrations` | `@objectstack/platform-objects` | none beyond Studio's own `studio.access` | + + **Two audit entries, on purpose.** The existing Audit Logs entry (the `sys_audit_log` object view) stays. It carries the named list views, search, and the actor and tenant rendered as resolved lookups. The new page adds one filterable table whose detail drawer pretty-prints a change's before and after JSON, where the record page shows `old_value` / `new_value` as raw text. Neither surface replaces the other. + + **No entry for the console's AI Approvals page (`ai:approvals`) here.** Under ADR-0029 D7, each capability plugin contributes its own navigation entries into a Setup slot, and the Setup shell does not enumerate capability objects. The AI pending-action queue belongs to the AI capability, whose provider (`@objectstack/service-ai`) ships in Cloud/Enterprise, not in the open framework. Its entry is therefore that capability's to contribute. + + The keys are the ones the console registers at the objectui commit this release's console is built from. Labels ship in all four locales (en, zh-CN, ja-JP, es-ES), with their source hashes recorded. Nothing is removed or renamed, and there is nothing to migrate. +- ba4648d: `sys_comment.reactions` and `sys_comment.mentions` now describe the shape they actually store (#20558) + + The two field descriptions are served metadata (field help in the console, and what an AI client reads before it seeds a comment), and both named a shape no producer writes: + + | Field | Description was | Description is now | Stored value | + | --- | --- | --- | --- | + | `reactions` | `JSON array of emoji reaction objects` | `JSON object mapping each emoji to the list of user ids who reacted` | `{"👍":["usr_1","usr_2"]}` | + | `mentions` | `JSON array of @mention objects` | `JSON array of the user ids @mentioned in the comment` | `["usr_1","usr_2"]` | + + The console's record discussion panel reads and writes `reactions` as that map, and writes `mentions` as that list of ids; the `collab.mention` notification hook reads the ids. + + Description text only: no stored value, validation rule or hook changes, and nothing to migrate. The English translation bundle is regenerated from the source description, and the zh-CN, ja-JP and es-ES help texts for both fields are rewritten to match (values only, no key added or dropped). +- 4dfff17: Provenance comments in `plugin-audit` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- 7184436: fix(plugin-webhooks,plugin-audit,plugin-security): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20653) + + Clause-②: no + + A translated object leaf that a translator wrote by hand is kept as written + when its English source changes later, so some leaves went on saying what the + old source said. On a ja-JP, es-ES or zh-CN console the `sys_webhook` record + page told an admin that `definition_json` carries the full headers / auth / + retry / payload configuration, where the English help says credentials are not + stored there: the signing secret and the custom headers live in the encrypted + `signing_secret` and `headers_secret` fields. + + Eighteen leaves (six paths, in all three locales) whose meaning contradicted + the current English now match it: + + - `@objectstack/plugin-webhooks`: the `sys_webhook.definition_json` help (no + credentials in the JSON) and the `sys_webhook` description (dispatched by the + webhook auto-enqueuer onto the shared HTTP outbox, not executed by an HTTP + connector plugin; declared through `defineStack({ webhooks })` too); + - `@objectstack/plugin-audit`: the `sys_activity.environment_id` label and help + (Environment, not Project), and the `sys_audit_log.user_id` label (User: the + object's separate `actor` field is the actor); + - `@objectstack/plugin-security`: the `sys_position` description (positions + distribute capability, not definitions for RBAC access control). + + Leaves whose English source only gained detail, was reworded, or was + title-cased (the `@objectstack/plugin-approvals` status and action options) + are unchanged. Values only: no key is added or removed, and no provenance + table changes. +- 2488b98: fix(plugin-audit): an activity row serves a parent field's value only to a reader the security service serves that field (#21081) + + Clause-②: no + + The activity stream's CRUD mirror composes each row once, at write time, as the system. The row's summary, its record label and its recorded change can carry the values of the parent record's fields. The activity read gate keeps a row for every reader who can read the parent record, so a reader who may not read one of that record's fields was served the field's stored value through the row. This held for a field served masked to the reader, a field gated by `requiredPermissions` the reader does not hold, and a field a permission set the reader holds marks non-readable. The data plane answered the same reader masked or without the key. + + The rows are now redacted at read time, keyed on the reading caller, through the security service's own answer: the read projection intersected with the query-side answer, whose difference the contract defines as exactly the fields served masked. The recorded change drops every key the reader is not served. The summary and the record label are each served whole or dropped whole: the mirror now declares, in the row, which parent fields each was composed from, and a text composed from a field the reader is not served is dropped. A text composed only from served fields is kept. The full row stays at rest, and system reads are unchanged. + + Rows written before this release carry no such declaration. Their summary and record label are served only to a reader who is served every field of the parent record, until the rows age out with the stream's retention. Rows an app writes itself are served as written, except that a recorded change in the mirror's shape is narrowed the same way. +- fbcc05f: fix(plugin-audit): the compliance ledger's before/after snapshots serve a parent field's value only to a reader the security service serves that field (#21155) + + Clause-②: no + + The CRUD mirror writes one `sys_audit_log` row per record write, once, as the system. A create row's after-snapshot, an update row's before/after snapshots of each changed field, and a delete row's before-snapshot carry the parent record's stored field values. The ledger is read through the generic data doors under its own object grant and tenant wall, so a reader whose permission sets grant the ledger read was served every snapshot key. This held for a field served masked to the reader, a field gated by `requiredPermissions` the reader does not hold, and a field a permission set the reader holds marks non-readable. The data plane answered the same reader masked or without the key. + + Ledger readers are not field-unrestricted by default. The snapshots of create, update and delete rows are now narrowed at read time, keyed on the reading caller, through the security service's own answer: the read projection intersected with the query-side answer, whose difference the contract defines as exactly the fields served masked. Every key the reader is not served is dropped. A reader served every field reads the snapshots byte-identical to the row at rest, and system reads are unchanged. An auditor who must see every field is granted that by a permission set that unmasks those fields. + + Rows of other actions are served as written: their snapshot columns are empty, a settings digest, or the administrator roster, and none of them is a parent record's field map. A create, update or delete row whose snapshot cannot be judged key by key (not a JSON object, or no parent object named) loses that snapshot. The activity stream's redaction and this one now share one served-fields helper. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d67b942] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index ae46d828452..0fd6af3be38 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "main": "dist/index.js", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index b0c20dc89cc..024e2ca8c86 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,264 @@ # Changelog +## 17.6.0 + +### Minor Changes + +- 33b6e8b: feat(plugin-auth): a host declares its own sign-in handoff route with `hostSignInHandoff`, and the `no_sign_in_account_at_boot` boot report stops calling that deployment a dead end (#20861) + + Clause-②: yes (widening) + + `AuthPluginOptions` gains one option, `hostSignInHandoff?: boolean` (default + `false`). The HOST that constructs the plugin sets it when it signs people in + through a handoff route of its own: a route that is not a login-page provider + and that creates the session without writing a `sys_account` row. A hosted + kernel whose owner signs in through the control plane is the case it is for. + That owner can still sign in when the login page shows no platform sign-in + button: + + ```ts + new AuthPlugin({ /* … */ hostSignInHandoff: true }); + ``` + + With it declared, human `sys_user` rows and zero `sys_account` rows are that + deployment's normal state. The boot report then logs the shape at `debug` and + names `hostSignInHandoff` as the reason. It no longer logs an `error` saying + nobody can sign in. The option's only reader is that boot report. + + - It is a declaration, not a detection. The option is the only way to set it: + there is no environment variable or setting. Nothing infers it from an + environment's name, from a control plane's platform-SSO flag, or from missing + rows. + - The login page is not changed. `getPublicConfig()` returns the same value with + or without the option, and no provider is registered. + - Set it only where the host really serves such a route. On a deployment with + no such route, the option turns the error for a deployment nobody can sign in + to into a quiet `debug` line. + - A deployment that does not declare it gets the same report as before. That + includes every self-hosted deployment with no delegated sign-in path. + +### Patch Changes + +- 4d04b6b: Provenance comments in `plugin-auth` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- e47355b: Auth, webhook and outbound-delivery refusals, warnings and field help no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these three packages show to operators, administrators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/plugin-auth`: an unrecognised audience posture is refused because it must not fall through to a more permissive posture than the one intended; the `ObjectQL` adapter's case-insensitive warning says the `$ieq` operator is deliberately deferred until there is demonstrated pull for it; the `internal`-column refusal says the column is withheld from every ordinary read and recovered only through the engine's accessor; the 2FA re-enrollment errors say a re-enrolled TOTP secret may be live at sign-in without having been confirmed; the walled-owner boot warning says a declared owner is stamped verified only when an operator-provisioned path creates the account; the OTP send-budget lines name the budget without a number. + - `@objectstack/plugin-webhooks`: the parked-event record says the event is recorded rather than delivered unsigned (or without its authored headers) and rather than discarded without a trace; the redeliver refusals say a delivery that cannot be signed is refused rather than sent unsigned; the zero-trigger warning says the `api` trigger was removed because nothing could fire it; the seed and legacy-migration warnings say a credential is never stored in cleartext instead and that a failed migration leaves it cleartext in `definition_json`. + - `@objectstack/service-messaging`: the `sys_http_delivery` field help for `attempts` (in every shipped locale) says a parked row is not redeliverable because it carries no signature; the `headers_json` and `error` help and the outbox refusals drop their citations; the notification `ack()` refusal says cancelling a pending row is not part of the outbox contract until a live consumer needs it. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 432c8ab: fix(plugin-auth): the OIDC discovery documents answer on every boot, including one whose first request arrives before the auth instance is built + + Clause-②: no + + - `GET /.well-known/openid-configuration` and `GET /.well-known/oauth-authorization-server` used to be mounted only after the better-auth instance finished building, in the background. When the server answered any request before that (a readiness probe, for example), the router was already sealed. The late mount failed, the failure was logged, and both documents answered 404 until the process restarted. The RFC 8414 path-inserted alias and the two RFC 9728 protected-resource documents had the same problem. + - All five routes are now mounted while the auth routes are registered, before the server opens its socket. Each request waits for the auth instance and then serves the document. A route that cannot be mounted now fails the boot instead of being logged and skipped. + - When the OIDC provider plugin is degraded, these paths answer as they did before, as if they were not mounted. A boot-time error line still reports this. + - If the auth instance cannot be built, a discovery request answers a server error, and the next request tries the build again. Before, these paths kept answering 404. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- 55012df: fix(plugin-auth): the compliance-ledger rows the admin identity endpoints write record the admin's decisions, never a value of a field of the user (#21174) + + Clause-②: no + + The admin create-user and set-user-password endpoints each write their own `sys_audit_log` row beside the rows plugin-audit's CRUD mirror writes for the same call. That row's free `metadata` copied values the call had just written into fields of the user. The ledger's read side narrows the mirror's before/after snapshots to what each reader is served, but it cannot narrow free metadata without deriving masking a second time, so a ledger reader the data plane withholds one of those fields from was served its value through the explicit row. + + The explicit row now carries only the admin's decisions — which operation ran, whether the password was generated, whether the account's address is a generated placeholder, whether the membership was bound and to which organization — plus its reference to the user (`object_name` and `record_id`). The values the call writes into the user's fields are recorded where they already were: on the mirror's `create` and `update` rows for those same writes, in the snapshot columns the read side narrows per reader. The decision set is a closed type, so a field value no longer compiles into the row. + + Migration: a reader that took a user field's value from the explicit row's metadata reads it from the mirror's row for the same write instead (its after-snapshot), served according to the reader's field access. Rows written before this release are stored data and are not rewritten. +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [6f1f1c1] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [eb4b17c] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [67c1b11] +- Updated dependencies [72f8c38] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [165c1d4] +- Updated dependencies [d7b9817] +- Updated dependencies [f80e2a6] +- Updated dependencies [22e584c] +- Updated dependencies [c8111a5] +- Updated dependencies [7afdc5c] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [f115b1f] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [8f78495] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [e161ad3] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [bafb8c9] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [04b202e] +- Updated dependencies [422db78] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/rest@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-messaging@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 3373e963b46..60f73db1d05 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "main": "dist/index.js", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index 5e24c7d9ed2..a1ec4bc0d3c 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,260 @@ # @objectstack/plugin-dev +## 17.6.0 + +### Patch Changes + +- 49d2a24: The `verify --rls` report and messages, the dev plugin's tenancy and no-auth messages and the Hono server's no-API warning no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Strings these three packages show to operators, and print in verification reports, sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/verify`, the `objectstack verify --rls` report: the header reads `=== objectstack verify (RLS / cross-owner by-id-write invariant) — ===`, and the position-persona line reads `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed`. + - `@objectstack/verify`, an `rls-hole` verdict's detail: it says the by-id write bypassed RLS, and that a caller that cannot read a record must not be able to write it. + - `@objectstack/verify`, the refusal when the RLS probe persona cannot be provisioned (no ObjectQL engine): it says a by-id write that bypasses RLS is what becomes unreachable. The matching position-persona refusal drops its citation. + - `@objectstack/verify`, the records the probe writes: the probe permission set's row-level-security policy description, the probe `sys_permission_set` row's description and the position persona's `sys_user_position` reason drop their citations. Each already said what it is for. + - `@objectstack/verify`, the `bootStack` refusal for `multiTenant: true` when the app does not declare `@objectstack/organizations`: the citation beside "a package merely reachable through NODE_PATH or a hoisted workspace store is not accepted" goes. + - `@objectstack/plugin-dev`, the `REST API NOT enabled` warning for a stack that mounts no auth: it says anonymous access to object data is always denied, with no setting that turns that off. + - `@objectstack/plugin-dev`, the two refusals for an `OrganizationsPlugin` that refused to be constructed or failed to initialize: they drop their citations. Each already says `OS_ALLOW_DEGRADED_TENANCY` covers only an absent multi-org runtime, not a present one that declined. + - `@objectstack/plugin-hono-server`, the boot warning for a server with no data or discovery API mounted: it drops its citation. It already says the plugin is a transport adapter that serves neither. + + Text only: no status, error code, exit code, route, field, export, verdict or count moves. A log filter or script that matched the old text (for example the report header's `RLS / #NNNN` spelling) needs the new spelling. +- f7c6d65: Provenance comments in `@objectstack/plugin-dev` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no service, boot-log line, warning text, type, export or runtime + behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [6f1f1c1] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [7001918] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [eb4b17c] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [6e3aa75] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [4d04b6b] +- Updated dependencies [9a4b2bb] +- Updated dependencies [9b384f6] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [67c1b11] +- Updated dependencies [72f8c38] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [7184436] +- Updated dependencies [ed54768] +- Updated dependencies [d3f88fa] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [165c1d4] +- Updated dependencies [d7b9817] +- Updated dependencies [f80e2a6] +- Updated dependencies [22e584c] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [7afdc5c] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [f115b1f] +- Updated dependencies [7c5a311] +- Updated dependencies [49d2a24] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [76bd58f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [8fec76a] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [95fed33] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [cb4c31d] +- Updated dependencies [05be352] +- Updated dependencies [d67b942] +- Updated dependencies [f8178ff] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [8f78495] +- Updated dependencies [a3dc817] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [de8cd58] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [e161ad3] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [58a77db] +- Updated dependencies [a9d36d5] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [2bddb19] +- Updated dependencies [bafb8c9] +- Updated dependencies [9c8b65a] +- Updated dependencies [665cab3] +- Updated dependencies [665cab3] +- Updated dependencies [45ce12a] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [ef96c9e] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [f0cc16e] +- Updated dependencies [55012df] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [01e78dc] +- Updated dependencies [1741c5d] +- Updated dependencies [04b202e] +- Updated dependencies [a186aea] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/rest@17.6.0 + - @objectstack/plugin-security@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-realtime@17.6.0 + - @objectstack/driver-memory@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/plugin-auth@17.6.0 + - @objectstack/service-storage@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/plugin-hono-server@17.6.0 + - @objectstack/account@17.6.0 + - @objectstack/setup@17.6.0 + - @objectstack/service-i18n@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 91f4db32021..bb22c675d00 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "main": "dist/index.js", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index 87d854450c3..ef2c90bfa8e 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,162 @@ # @objectstack/plugin-email +## 17.6.0 + +### Patch Changes + +- cba417a: Provenance comments in `plugin-email` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/formula@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 37d9e93b176..e041bf23a1a 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index a10375b92f4..fa909e49255 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,192 @@ # @objectstack/plugin-hono-server +## 17.6.0 + +### Patch Changes + +- 49d2a24: The `verify --rls` report and messages, the dev plugin's tenancy and no-auth messages and the Hono server's no-API warning no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Strings these three packages show to operators, and print in verification reports, sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/verify`, the `objectstack verify --rls` report: the header reads `=== objectstack verify (RLS / cross-owner by-id-write invariant) — ===`, and the position-persona line reads `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed`. + - `@objectstack/verify`, an `rls-hole` verdict's detail: it says the by-id write bypassed RLS, and that a caller that cannot read a record must not be able to write it. + - `@objectstack/verify`, the refusal when the RLS probe persona cannot be provisioned (no ObjectQL engine): it says a by-id write that bypasses RLS is what becomes unreachable. The matching position-persona refusal drops its citation. + - `@objectstack/verify`, the records the probe writes: the probe permission set's row-level-security policy description, the probe `sys_permission_set` row's description and the position persona's `sys_user_position` reason drop their citations. Each already said what it is for. + - `@objectstack/verify`, the `bootStack` refusal for `multiTenant: true` when the app does not declare `@objectstack/organizations`: the citation beside "a package merely reachable through NODE_PATH or a hoisted workspace store is not accepted" goes. + - `@objectstack/plugin-dev`, the `REST API NOT enabled` warning for a stack that mounts no auth: it says anonymous access to object data is always denied, with no setting that turns that off. + - `@objectstack/plugin-dev`, the two refusals for an `OrganizationsPlugin` that refused to be constructed or failed to initialize: they drop their citations. Each already says `OS_ALLOW_DEGRADED_TENANCY` covers only an absent multi-org runtime, not a present one that declined. + - `@objectstack/plugin-hono-server`, the boot warning for a server with no data or discovery API mounted: it drops its citation. It already says the plugin is a transport adapter that serves neither. + + Text only: no status, error code, exit code, route, field, export, verdict or count moves. A log filter or script that matched the old text (for example the report header's `RLS / #NNNN` spelling) needs the new spelling. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- 01e78dc: Provenance comments in `@objectstack/plugin-hono-server` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no route, error code, refusal text, type, export or runtime behaviour + changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/observability@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 9bb02bc2aec..18959b4af1a 100644 --- a/packages/plugins/plugin-hono-server/package.json +++ b/packages/plugins/plugin-hono-server/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-hono-server", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index 0ee83cbe22b..235c10e9bdc 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,77 @@ # @objectstack/plugin-pinyin-search +## 17.6.0 + +### Patch Changes + +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [88b484e] +- Updated dependencies [05a7547] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [889139c] +- Updated dependencies [4bf4e7e] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [89801cd] +- Updated dependencies [cd6d8a5] +- Updated dependencies [63bfe69] +- Updated dependencies [bbcd20c] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [d67b942] +- Updated dependencies [d1633f3] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [657b6b7] +- Updated dependencies [c35436c] +- Updated dependencies [58a77db] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [097ef80] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [336e191] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [00f045d] + - @objectstack/objectql@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 8dec24ae853..6eadfce3831 100644 --- a/packages/plugins/plugin-pinyin-search/package.json +++ b/packages/plugins/plugin-pinyin-search/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-pinyin-search", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index aa59f334c6b..0a608ff479e 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,500 @@ # @objectstack/plugin-security +## 17.6.0 + +### Minor Changes + +- e5c7d07: The `security` service implements `getWritableFields(object, context)` (#18386). It uses the same permission sets, field grants, `requiredPermissions` check and on-behalf-of delegator intersection as the write gate. A field is in the answer exactly when a write naming it passes the field-level-security check. `getReadableFields` now shares that derivation, and its answers are unchanged. +- de8cd58: fix(plugin-security)!: a field the caller may not read is refused as a cross-field comparand exactly as it is refused as a filter key (#20932) + + Clause-②: no (narrowing) + + + + **BREAKING for queries that compare a column against a field the caller may not read.** + + **What changed.** The security layer refuses a query that filters, sorts, groups or aggregates by a field the caller's field-level permissions hide, with `403 PERMISSION_DENIED`: which rows answer would disclose the value that the field mask withholds from the result. A cross-field comparand (`FieldReferenceSchema`, "compare against another column of the same row") reads the field it names in the same way, and it is now collected into the same set and judged by the same rule. A hidden field named as a comparand, in any position the filter grammar admits for one, in `where`, `having` or a per-aggregation `filter`, answers the same `403 PERMISSION_DENIED`, in the same words, as the same field written as a filter key. This covers `engine.find`, `findOne`, `count`, `aggregate` and the bulk `update` / `delete` predicate, and every route that reaches them. Before, such a comparison was answered. + + **What is not affected.** A comparand naming a field the caller may read answers as before. A system context, and a caller with no permission sets, are unaffected. Row-level policies may still compare against fields the caller cannot read, because they are applied after the guard. A comparand the filter grammar refuses is still refused; when it names a hidden field, that refusal may now be the `403` rather than `400 INVALID_FILTER`, as it already was for a hidden filter key. + + **If a query stopped answering for some users,** it compares against a field those users may not read. Grant that field's read permission to the users who need it, or compare against a field they can read. +- 83480c6: The `security` service implements `getQueryableFields(object, context)` (#20935). A field is in the answer exactly when a query naming it as a filter, a sort key, a group key or an aggregate input passes the engine's field guards: the answer is read from the one field map the predicate guard and the aggregate-input guard now share (permission sets, field grants, the `requiredPermissions` check, the on-behalf-of delegator intersection, and every field whose masking rule applies to the caller). The two guards refuse exactly what they refused before. +- a9d36d5: fix(plugin-security)!: a field whose masking rule applies is served masked to a caller who resolves no permission set, and that caller may not filter, sort, group or aggregate on it (#20995) + + Clause-②: no (narrowing) + + + + **BREAKING for callers who resolve no permission set.** + + **What changed.** A field that declares `maskingRule` is masked for every + non-system caller unless the caller holds all of the field's + `requiredPermissions`. A caller who carries a principal but resolves no + permission set holds no capability, so the rule applies to it, but the runtime + served that caller the stored value and let it filter, sort, group and + aggregate on the field. That caller is now served the masked value. A filter, + sort key, group key or aggregate that names the field is refused with + `403 PERMISSION_DENIED`, as it already was for any other masked caller. A write + that sends the masked placeholder back is refused with `400 VALIDATION_ERROR`, so + a client that saves the record it was served cannot overwrite the stored value + with its mask. + + The published field answers agree with what is served. + `ISecurityService.getQueryableFields` no longer lists such a field for this + caller, so a door that compiles its own query refuses it the same way. + `getReadableFields` still lists it, because a masked field is a served column. + + **Who this reaches.** A caller who resolves no permission set but carries a + position, a named permission set or a user id. A caller with none of the three + is handed through untouched, as before, and the field projections say so. A + system context is unaffected. + + **One more refusal, by the same rule.** If the object's security posture cannot + be read, this caller's request is now refused, as every other caller's already + is. The masking rules come from that posture, so they cannot be known without + it. + + **What to do.** Nothing, unless such a caller needs the stored value. A field's + `requiredPermissions` are the gate that lifts its mask, so give the caller a + permission set that holds all of them, or drop the `maskingRule`. A query that + must sort or search on the field needs the same. +- 9c8b65a: fix(plugin-security)!: the record an anonymous public-form submit echoes back passes the result masker, so a field whose masking rule applies is echoed masked (#21062) + + Clause-②: no (narrowing) + + + + **BREAKING for the anonymous public-form submit's response.** + + **What changed.** A public form's submission is authorized by the ADR-0056 + declaration-derived grant, which admits the create and the read-back on the + form's declared object and passes before any permission set is resolved. The + grant handed the operation to the engine and returned before the result masker + ran, so the record echoed in the `201` body carried every field whose + `maskingRule` applies as stored: the field the form collects, and a field + filled from its `defaultValue` that the form never shows. + + The grant now hands what it returns to the same result masker the data plane + uses, for the caller it stands in for: the permission sets resolved for the + grant's context (the deployment's guest set when it registers one, otherwise + none) and the object posture those sets read. A field whose masking rule + applies is echoed masked. A field the caller's sets mark unreadable, or whose + `requiredPermissions` they do not hold, is masked the way the data plane masks + it for that caller. The read-backs the grant admits are masked the same way. + + **What did not change.** The grant admits exactly what it admitted: the create + and the read-back on the form's declared object, and nothing else. The + server-managed fields are still stripped from the submitted row. The stored row + is unchanged; only the echo is masked. + + **One more refusal, by the same rule.** If the caller's permission sets or the + object's security posture cannot be read, the submission is now refused with + `403 PERMISSION_DENIED` before anything is written, as every other caller's + request already is. The masking rules come from that posture, so the echo + cannot be masked without it. + + **What to do.** Nothing, unless a client reads a masked field's stored value + back out of the submit response. The response now carries the masked value, as + every other non-system read does. A field's `requiredPermissions` are the gate + that lifts its mask, so a deployment whose guest set holds all of them is + echoed the stored value; otherwise drop the `maskingRule`. +- 665cab3: fix(plugin-security)!: a field that declares `requiredPermissions` is not served to a caller who resolves no permission set, and that caller may not query on it or write it (#21063) + + Clause-②: yes (narrowing) + + + + **BREAKING for callers who resolve no permission set.** + + **What changed.** A field that declares `requiredPermissions` is masked on read + and denied on write unless the caller holds all of them (ADR-0066 D3). A caller + who carries a principal but resolves no permission set holds no capability, so + the gate applies to it, but the runtime served that caller the stored value, + let it filter, sort, group and aggregate on the field, and accepted a write + that named it. The explain engine already reported the field hidden for that + caller. Now the field is not served to it (a field that also declares a + `maskingRule` is served masked, as before). A filter, sort key, group key or + aggregate that names the field is refused with `403 PERMISSION_DENIED`, and so + is a write payload that names it, as for any other caller who lacks the + capability. + + The published field answers agree with what is served and refused. + `ISecurityService.getReadableFields`, `getQueryableFields` and + `getWritableFields` no longer list such a field for this caller, and neither + does `getMetadataReadableFields` when the deployment's fallback set resolves to + nothing. The write preview answers such a payload as the write path does. + + **Who this reaches.** A caller who resolves no permission set but carries a + position, a named permission set or a user id. A caller with none of the three + is handed through untouched, as before, and the field projections say so. A + caller who resolves at least one permission set is unaffected, and so is a + system context. Whether this caller may read or write the object at all is + unchanged. + + **What to do.** Nothing, unless such a caller needs the field. A field's + `requiredPermissions` name the capabilities that open it, so give the caller a + permission set that holds all of them, or drop the requirement from the field. +- 62b90d7: fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope, and the security contract says so (#21079) + + Clause-②: yes (narrowing) + + + + **BREAKING for callers who resolve no permission set.** Shipped as `minor` under the launch-window convention. + + **What changed.** ADR-0056 D2 gives an unauthenticated principal the deny baseline, not "no checks", and ADR-0090 D9 gives a guest the `guest` position and nothing else. A non-system caller that carries a principal (a position, a named permission set or a user id) but resolves no permission set was instead admitted to every object no set grants, for reads and writes, and read with the record-sharing predicate as its only row scope. Now an empty set list grants nothing: + + - **Object admission refuses it.** Every engine operation (find, findOne, count, aggregate, insert, update, delete) is refused with `403 PERMISSION_DENIED`, the same refusal any caller gets for an object its sets do not grant. `ISecurityService.canReadObject` and `canExport`, and the write preview's admission, answer `false` for it. + - **Its row scope is the deny filter.** `ISecurityService.getReadFilter` answers the filter that matches zero rows for it, as it already did on a resolution failure. + - **The second principal of a delegated request is held to the same answer.** An agent acting on behalf of a delegator who resolves no permission set was already refused by the engine; `canReadObject`, `canExport` and the write preview now refuse it too. + + The field answers for this caller (`getReadableFields`, `getQueryableFields`, `getWritableFields`, `getMetadataReadableFields`) are unchanged: they are field-level answers, and the contract now says that object admission is not part of them. The `ISecurityService` docblocks in `@objectstack/spec/contracts` that stated the old zero-set admission (`canReadObject`, `canExport`, the metadata-plane field projection) and the deny cases of `getReadFilter` narrow to match. No method, parameter or return type changes. + + **Who this reaches.** + + - An unauthenticated request carried as the guest envelope, on a deployment that grants anonymous callers no permission set. + - A context that names only permission sets the deployment does not register. + - A signed-in user on an embedder that switches the baseline off (`fallbackPermissionSet: null`) and grants that user nothing. + + A context that carries no principal at all (no position, no named set, no user id) is handed through as before; ADR-0096 stages it separately. A caller who resolves at least one permission set is decided by its sets, as before, and so is a system context. The public form submit is unaffected: its declaration-derived grant admits the create and its read-back ahead of object admission. Signed-in users of a stock `objectstack serve` deployment are unaffected: it applies the member baseline to every one of them, so none resolves an empty list. + + **Migration.** A caller that resolves no permission set is refused object admission and reads nothing. An app-declared anonymous endpoint (`authRequired: false`) can no longer read or write objects until the `guest` anchor's bindings are resolved for anonymous callers (#21158). An embedder that sets `fallbackPermissionSet: null` must grant its signed-in users a set explicitly. + + **For implementers of `ISecurityService`.** Answer `canReadObject`, `canExport` and the object-admission half of a write `false`, and `getReadFilter` with your deny filter, for a non-system caller that carries a principal and resolves no permission set; admit only the principal-less context. +- d2bc644: fix(plugin-security): a row-level `check` judges a lone scalar written to a declared multi-valued field as the one-member list it is stored as, so the write and the read the same policy scopes give one answer for one row (#21238) + + Clause-②: yes (widening) + + The write door stores a lone scalar sent to a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `lookup` / `user` / `file` / `image` flagged `multiple: true`) as a one-member list: `tags: 'x'` is stored as `["x"]`. The row-level write `check` judged the value as sent on the insert and on a by-id update, because both images are formed before the write door runs. Measured through `ObjectQL.insert` with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set, with the same predicate as `using` and `check`: + + | `check` | written | write, before | stored | read | + |---|---|---|---|---| + | `record.tags.contains('x')` | `'x'` | 403 | `["x"]` | shown | + | `!record.tags.contains('x')` | `'x'` | admitted | `["x"]` | hidden | + | `record.tags.contains('x')`, a by-id update | `'x'` | 403 | `["x"]` | shown | + + Now the image's value on every field the object declares multi-valued goes through the same rule the write door stores it by, before the check is judged. The first and third rows are admitted. The second is refused: a policy that forbids a member from tagging a row `x` can no longer be passed by sending `'x'` instead of `['x']`. A lone scalar now gets exactly the verdict its stored list gets, on the insert, a by-id update and a predicate update. That includes a policy that compares such a field with a scalar comparison (`==`, `!=`, `in`, an ordering), which the read refuses with `INVALID_FILTER` / 400: there `'x'` used to get the opposite of the verdict `['x']` got, and now gets the same one. + + Unchanged: a field the object does not declare multi-valued is judged as written; a list, `null`, a blank string and an object are judged as written, as the write door leaves them; the check's comparands are left as written, since `contains` takes one member; and refusals keep their code and status (`PERMISSION_DENIED` / 403). + + **`@objectstack/core`** (one new root export, so `minor`; this export is the widening the `Clause-②: yes (widening)` line declares): `multiValueStorageForm(value)`, the rule itself. It wraps a string, a number or a boolean into a one-member list and returns every other value as the same value. `@objectstack/objectql`'s `normalizeMultiValueFields` now calls it, with no change in what the write door stores (`patch`). `@objectstack/plugin-security` is `minor` because the set of writes its check admits widens (the first and third rows above); that is a security-floor behaviour change, not the declared widening. +- cfa9315: feat(spec, objectql, plugin-security): one shared filter lowering, run once at the engine and RLS seams (ADR-0053 D-D1, amended) + + Clause-②: yes + + `@objectstack/spec/data` exports `lowerFilterCondition(filter, options?)` and its `FilterLoweringOptions` type. It is not exported from the package root entry. It is a pure `FilterCondition → FilterCondition` rewrite that applies three rules once: + + - `$between` becomes `$gte` its minimum and `$lte` its maximum. + - A `$lte` whose comparand is a bare `YYYY-MM-DD` day becomes `$lt` the next day, in the calendar-string domain. On the last supported day (`9999-12-31`) a lone `$lte` becomes `{ $null: false }`, and a `$between` keeps only its minimum. + - The NULL-polarity guards the drivers already compile. A `$ne` of a value, a `$nin` or a `$notContains` holds for a row with no value. Every leaf of a `$not` operand is made total. + + The rewrite is copy-on-write, idempotent and never refuses. A node it rewrites keeps its filter-subtree provenance mark. With `options.isDatetimeColumn` (a typed seam), the first two rules change only a declared `datetime` column. Without it they apply to every column. + + As ADR-0053 D-D1 (amended 2026-09-30) requires, the seams now run it once, after the comparand doors and after filter-token resolution: + + - **`@objectstack/objectql`** runs it on every filter position, typed by the object's declared fields. That covers `where` on `find`, `findOne`, `count`, `update` and `delete`, and `aggregate`'s `where`, `aggregations[i].filter` and `having`. `having` is typed by the aggregated row's columns, so `max` of a `datetime` field counts as a `datetime`. Drivers receive the lowered filter. A date macro such as `{today}` is resolved before the lowering reads it. + - **`@objectstack/plugin-security`** runs it on every compiled RLS policy filter (`using` and `check`), right after the two comparand faces. `SecurityPlugin` now hands the compile seam the object's declared `datetime` columns (`RlsFieldGuard.datetime`). A guard without that set treats no column as `datetime`. + + Row answers stay the same on every driver. Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input. One result changes. The engine evaluates `aggregate`'s `aggregations[i].filter` and `having` itself, and that evaluator now treats a row or group with no value the way every driver's `where` already does. It no longer counts such a row in a `$between` on a `datetime` column. It now keeps such a row under a `$not` over an ordering such as `$lt`. + + Nothing is removed or renamed, and there is nothing to migrate. + +### Patch Changes + +- 7001918: fix(plugin-security): `security/explain` answers with enforcement's refusal for a row-level policy that compares two fields of no shared comparison class, instead of a record verdict (#20431) + + Clause-②: no + + A row-level policy can compare two fields that share no comparison class: text against a number, or any field against a file field, a formula field, or a field that holds a list or an object. The platform defines no answer for such a comparison. The SQL driver refuses to compile it, so every find the policy scopes answers `INVALID_FILTER` / 400. A by-id update or delete fails closed at its row-level gate, because that gate's pre-image read is the same refused read. + + The explain engine's record attribution judged the same predicate in-process, without the object's declared columns. So it compared the two raw values, and it reported `record.visible` as `true` or `false` depending on how those values happened to compare. For one ordering of a pair, it reported the record visible where enforcement refuses the read. + + The record matcher now receives the object's declared columns, as the RLS write check already does, and it refuses the comparison the way the driver does. A record-grained explanation (`recordId`) under such a policy is now refused with the matcher's envelope: `INVALID_FILTER` / 400, the same envelope the find answers with. No record verdict is reported. The message names the policy and both fields with their declared types. This is the answer explain already gives to the matcher's other `INVALID_FILTER` refusals, including a field-to-field comparison against a field that holds a list. Both orderings of one pair now get this one answer. + + Unchanged: + + - Enforcement admits and refuses exactly what it did before. + - A comparison between two fields of one class keeps its record verdict. + - A schema that cannot be read hands over no columns, so the matcher judges values only, as before. + - An object-level explanation (no `recordId`), which runs no record matcher. +- 6e3aa75: A permission-set resolution with no active organization now reads the organization-less permission sets only. A permission set scoped to an organization applies only while that organization is active. This is the rule `resolveUserAuthzGrants` already applies to grant rows. + + Clause-②: no + + Before, the by-name `sys_permission_set` read carried no organization when the caller had none active. Every organization's row of each requested name came back, and the first one won. The names requested include the principal's positions. So a permission set another organization had authored under the name of a built-in role could reach an organization-less principal's resolved sets and effective object map. The same read could also resolve another organization's same-named copy of a set the principal holds through a global grant. + + - **Unchanged:** a principal with an active organization resolves exactly as before. That read was already scoped to the organization and the organization-less rows, and it still prefers the organization's own row. Global grants still apply everywhere. A global position folded onto a global permission set of the same name still resolves with no organization active, and so does a global user grant. Permission sets declared in metadata or bootstrap resolve as before, because they never reach this read. + - **If a principal relied on it:** make the organization active, or grant the permission set globally (no organization) when it is meant to apply everywhere. +- 889139c: fix(plugin-security): `security/explain` resolves the user it explains in the organization enforcement resolves them in, so a member whose membership in the caller's organization has ended is no longer explained holding that organization's grants (#20580) + + When an administrator explains another user, the explanation is computed in the administrator's own organization. Enforcement does one more thing for that same user first: under a walled tenancy posture (`isolated` or `group`), it drops an organization claim that no current membership backs, and the user resolves with no active organization, so only their global grants apply. The explainer skipped that check. For a user whose membership in the administrator's organization had ended, the explanation listed that organization's grants, and the verdicts they decide, while enforcement applied none of them. + + The explainer now asks the same check before it resolves the user, and resolves them where it says. `@objectstack/core` exports that check as `vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds, tenancyPosture)`. It returns the claimed organization while a current membership backs it or while no wall is enforced, and `undefined` once the claim is dropped. `resolveAuthzContext` asks the same function for a session's claim, so the two cannot disagree. This is a new export with no behaviour change to `resolveAuthzContext`. + + Unchanged: + + - Enforcement admits and refuses exactly what it did before. + - A current member's explanation. + - The `single` posture, where no claim is dropped on either side. + - Explaining yourself, and a caller with no active organization. +- 9a4b2bb: Provenance comments in `plugin-security` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the record in this repository that decided + the matter (an ADR where one exists, otherwise the commit in this repository's + history), and say in their own words what was decided. Comments only: no type, + schema, export, log or refusal text, or runtime behaviour changes. +- cd901d7: fix(plugin-security): `security/explain` answers enforcement's refusal at the object level too, and explains another user in the organization they are resolved in (#20604) + + Clause-②: no + + Two answers of `POST /api/v1/security/explain` disagreed with what the same principal's own request gets from enforcement. + + **A row-level policy that compares two fields of no shared comparison class** (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object). The SQL driver refuses to compile such a read, so the find answers `INVALID_FILTER` / 400. A by-id update or delete fails closed at its row-level gate, and an insert whose check judges the policy is refused with `INVALID_FILTER` / 400. An object-level explanation (no `recordId`) still answered `allowed: true`, the `rls` layer `narrows`, and the predicate as `readFilter`, for every operation. A `recordId` that no row carries was answered `visible: false` with no deciding layer. Both are now refused with the envelope a record-grained explanation already gives: `INVALID_FILTER` / 400, with the message that names the policy and both fields. A request that the capability gate or the CRUD grant denies is still explained as denied there. + + **Another user explained by an administrator.** The explanation now carries the organization the user is resolved in, as enforcement's context for that user does. Before, a current member of the administrator's organization was explained with no organization. Under `isolated`, that member was reported denied on a tenant object their own find reads. Under every posture, a permission set that their organization authored (a `sys_permission_set` row scoped to that organization) was missing from the explanation and from the verdicts it decides. + + `@objectstack/core`: the API-key arm of `resolveAuthzContext` asks `vetOrganizationClaim` for its membership rule, as the session arm does. This is a refactor with no behaviour change. A key whose owner is no longer a member of its organization is still refused. + + Unchanged: + + - Enforcement admits and refuses exactly what it did before. + - A comparison between two fields of one class keeps its verdicts, at the object level and per record. + - Explaining yourself. + - A removed member's explanation (no organization, as enforcement resolves them). +- 7184436: fix(plugin-webhooks,plugin-audit,plugin-security): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20653) + + Clause-②: no + + A translated object leaf that a translator wrote by hand is kept as written + when its English source changes later, so some leaves went on saying what the + old source said. On a ja-JP, es-ES or zh-CN console the `sys_webhook` record + page told an admin that `definition_json` carries the full headers / auth / + retry / payload configuration, where the English help says credentials are not + stored there: the signing secret and the custom headers live in the encrypted + `signing_secret` and `headers_secret` fields. + + Eighteen leaves (six paths, in all three locales) whose meaning contradicted + the current English now match it: + + - `@objectstack/plugin-webhooks`: the `sys_webhook.definition_json` help (no + credentials in the JSON) and the `sys_webhook` description (dispatched by the + webhook auto-enqueuer onto the shared HTTP outbox, not executed by an HTTP + connector plugin; declared through `defineStack({ webhooks })` too); + - `@objectstack/plugin-audit`: the `sys_activity.environment_id` label and help + (Environment, not Project), and the `sys_audit_log.user_id` label (User: the + object's separate `actor` field is the actor); + - `@objectstack/plugin-security`: the `sys_position` description (positions + distribute capability, not definitions for RBAC access control). + + Leaves whose English source only gained detail, was reworded, or was + title-cased (the `@objectstack/plugin-approvals` status and action options) + are unchanged. Values only: no key is added or removed, and no provenance + table changes. +- ceee88f: fix(driver-sql, driver-turso, plugin-security, spec)!: `SqlDriver` and `TursoDriver` compile the filter they are handed — their copies of the whole-day bound and of the NULL-safe `$not` rewrite are deleted, and the RLS compile seam lowers type-blind when it cannot read the declared types (ADR-0053 D-D1 items 5, 7 and 9, #20822) + + Clause-②: no (narrowing) + + + + **BREAKING**: `SqlDriver` in `@objectstack/driver-sql` loses three `protected` methods: `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite`. They were the driver's copy of the whole-day bound, which the shared lowering now applies once, at the seams, before a driver sees the filter. A subclass of `SqlDriver` that calls one of them, or overrides one with the `override` modifier, no longer compiles (TS2339, TS4113). That includes a subclass of `SqliteWasmDriver` or `TursoDriver`, which extend `SqlDriver`. A subclass that re-declares one without `override` still compiles, but the driver never calls it, so the rule it carried stops applying. It ships as `minor` under the launch-window convention. The class's public methods are unchanged. + + FROM → TO: a `SqlDriver` subclass that called `this.calendarDayUpperBoundRewrite(table, field, op, value)`, `this.calendarDayBetweenRewrite(table, field, value)` or `this.calendarDayExclusiveUpperBound(table, field, value)`, or overrode one of them, lowers the filter with `lowerFilterCondition` from `@objectstack/spec/data` instead, before the driver compiles it: `lowerFilterCondition(where, { isDatetimeColumn })`, where `isDatetimeColumn` answers which columns get the whole-day bound. + + **Supersedes two sentences of this release's shared-lowering entry** (`lowerFilterCondition`, #5930), which this change makes false: + + - "A guard without that set treats no column as `datetime`." Now: when the RLS compile seam has no field guard, or one without a `datetime` set, it cannot read which columns are `datetime`, so it applies the whole-day rule to every column (the `@objectstack/plugin-security` entry below). + - "Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input." Now: `SqlDriver`, `SqliteWasmDriver` and `TursoDriver` keep no copy of the whole-day bound or of the NULL-safe `$not` rewrite. A read through the engine or the RLS compile seam gets the lowered answer, and a call on the driver itself gets the comparison it wrote (the `@objectstack/driver-sql` and `@objectstack/driver-turso` entries below). + + - **`@objectstack/plugin-security` — an RLS policy compiled with no field guard is lowered type-blind.** The RLS compile seam runs the shared `lowerFilterCondition` on every compiled `using` and `check` filter. When the security plugin could not resolve the object's declared fields (no field guard), or a caller of `RLSCompiler.compileFilter` passes a guard without a `datetime` set, the seam cannot read which columns are `datetime`, and it now applies the whole-day rule to every column (a bare-day upper bound becomes `$lt` the next day), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the type. It used to read no column as `datetime`, which left the bound to each driver's own copy of the rule. Visible on a `using` policy such as `record.signed_on <= '2026-01-05'` on such an object: every row of that day is kept on every driver, including `InMemoryDriver`, which had compared it as written since its own copy was deleted. A guard with a `datetime` set is unchanged, and so are the NULL-polarity guards. + - **`@objectstack/driver-sql` — `SqlDriver` keeps no copy of the rules the seams apply.** Deleted: the whole-day rewrite of a bare-day `$lte` and of a `$between` maximum on a `datetime` column, on the plain and the legacy-normalised column paths, including the last supported day (the protected methods `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite` are removed from the class); and the NULL-safe rewrite of a `$not` operand (`nullSafeNegationOperand` and its polarity tables, module-private). A read through the engine or the RLS compile seam is unchanged: the seam hands the driver a filter the shared lowering has already rewritten, and the deleted copies gave the same answer on that input. A caller that passes no seam — `find`, `findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` or `findWithWindowFunctions` called on the driver itself — now gets the comparison it wrote: a bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, `$lte '9999-12-31'` compares against that midnight, and a `$not` is SQL's three-valued negation, so a row whose compared column is NULL is not returned by it. `$ne`, `$nin` and `$notContains` keep their NULL-safe form, which this emitter spells for the operator itself. The refusal of an `undefined` comparand (`INVALID_FILTER` / 400) is kept: without it some positions would answer instead of refusing. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. A subclass that called or overrode one of the three removed methods: see **BREAKING** above. + - **`@objectstack/driver-sqlite-wasm` — `SqliteWasmDriver` inherits the `SqlDriver` change above**, with the same answers on a seamed read and on a direct call. + - **`@objectstack/driver-turso` — both faces of `TursoDriver` compile the filter they are handed.** Local and replica mode inherit the `SqlDriver` change. Remote mode: `toRemoteFilter` no longer widens a bare-day `$lte` or a `$between` maximum (it still splits a two-bound `$between` into the `$gte` / `$lte` pair the remote transport compiles, both ends inclusive, and still converts each comparand to storage form), and `RemoteTransport` no longer rewrites a `$not` operand (its copy of the polarity tables is deleted). The two faces still answer every filter alike, on a seamed read and on a direct call. The remote transport keeps its refusal of an `undefined` comparand, worded as `driver-sql`'s, so both faces refuse it in one sentence. The same one line keeps the seam's reading on a direct call. + - **`@objectstack/spec` — the ADR-0087 ledger records the removal.** The protocol-18 step of `MIGRATIONS_BY_MAJOR` gains the semantic entry `driver-sql-calendar-day-methods-removed`, which names the three removed methods with their replacement and acceptance criteria. Every upgrade channel that projects protocol 18 carries it. `spec-changes.json` and the generated upgrade guide stop at the current protocol, 17, so neither changes in this release. A subclass that re-declares one of the methods without `override` still compiles and is never called, so the ledger, not the compiler, is the notice that reaches it. +- 05be352: fix(formula,plugin-security): the refusal of a field-to-field comparison across comparison classes now leads with its remedy, so the remedy reaches REST callers (#20869) + + Clause-②: no + + A row-level policy that compares two fields of no shared comparison class (text against a number, or any field against a file field, a formula field, or a field that holds a list or an object) is refused with `INVALID_FILTER` / 400. The REST door keeps a 4xx message under 500 characters by cutting it to its first 499 characters plus an ellipsis. Both messages for this refusal put the remedy last, so the remedy was always cut off, and a caller read the diagnosis but never the fix: + + - The record matcher's message (`@objectstack/formula`, raised by the RLS write check on an insert or update through `/data`) was 972 characters, with the remedy starting at character 825. + - The explain engine's message (`@objectstack/plugin-security`, answered by `GET` / `POST /api/v1/security/explain`) put the remedy after the policy names and the diagnostic. Those have no length limit, so the message was 601 characters with a short policy name and longer with longer names. + + Both messages now start with the remedy. It is the same sentence as before and has only moved: + + - The record matcher's message is 494 characters and reaches the wire whole. In order it says: the remedy; that the two columns share no class, and which classes exist; why the comparison is refused; and why the columns are not named. It still names no column, operator or policy; the server log names them. + - The explain engine's message starts with the remedy, then names the policy and both columns, then gives the reason. Whatever the names' length, the remedy sits in the first 125 characters. With long names the REST door may cut the reason at the end. + + Unchanged: the error code (`INVALID_FILTER`), the status (400), which comparisons are refused, the refusal a find answers with (driver-sql's read refusal, 383 characters, which already reached the wire whole), and every other refusal. +- ef96c9e: fix(plugin-security): a row-level `check` judges a `date`, `datetime` or `time` column as the row will be stored, so the write and the read the same policy scopes give one answer for one row (#21109) + + Clause-②: no + + The write check evaluates the compiled `check` filter in-process against the write's post-image. That image held each value as the caller or a hook wrote it, while the read compares the value the driver stored, in its column's storage form, against a comparand put into the same form. On a temporal column the two forms differ, so one row could get two answers. Measured through `ObjectQL.insert` with `SecurityPlugin` on SQLite, as a member resolving a permission set, with the same predicate as `using` and `check`: + + | `check` | written | write, before | stored | read | + |---|---|---|---|---| + | `record.due_on == '2026-01-05'` | `'2026-01-05T15:00:00Z'`, or a `Date` on that day | 403 | `2026-01-05` | shown | + | `record.start_time == '09:00'` | `'09:00:00'` | 403 | `09:00:00` | shown | + | `record.due_at == '2026-01-05T10:00:00Z'` | `'2026-01-05T18:00:00+08:00'` | 403 | `2026-01-05T10:00:00.000Z` | shown | + | `record.due_on > '2026-01-05'` | `'2026-01-05T15:00:00Z'` | admitted | `2026-01-05` | hidden | + + Now, before the check is judged, every column the object declares `date`, `datetime` or `time` is put into `@objectstack/core`'s `temporalStorageForm`, the rule the drivers write and compare those columns by. That applies to the post-image's value and to the check's value comparands on the column (`$eq`, `$ne`, the orderings, `$in`, `$nin`, `$between`). The first three rows above are now admitted. The last is now refused, which is the read/write agreement this change buys: the read never showed that row, so a write that stores a day the predicate excludes is no longer admitted on the strength of the time of day it was sent with. Every insert, by-id update and predicate update takes the same step. + + Unchanged: a column the object does not declare temporal is judged as written, whatever its value looks like; an object whose schema cannot be loaded is judged as before; a value the storage rule cannot read is judged as written; and refusals keep their code and status (`PERMISSION_DENIED` / 403). +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 6b9281845c0..1947a69dee9 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index 0c38d94d616..c80327310eb 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,181 @@ # @objectstack/plugin-sharing +## 17.6.0 + +### Patch Changes + +- b80ab57: Provenance comments in `plugin-sharing` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d67b942] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index b7d89ae606c..e6f145a1bd3 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index a265d098fe3..27508c69eee 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,197 @@ # @objectstack/plugin-webhooks +## 17.6.0 + +### Patch Changes + +- 7184436: fix(plugin-webhooks,plugin-audit,plugin-security): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20653) + + Clause-②: no + + A translated object leaf that a translator wrote by hand is kept as written + when its English source changes later, so some leaves went on saying what the + old source said. On a ja-JP, es-ES or zh-CN console the `sys_webhook` record + page told an admin that `definition_json` carries the full headers / auth / + retry / payload configuration, where the English help says credentials are not + stored there: the signing secret and the custom headers live in the encrypted + `signing_secret` and `headers_secret` fields. + + Eighteen leaves (six paths, in all three locales) whose meaning contradicted + the current English now match it: + + - `@objectstack/plugin-webhooks`: the `sys_webhook.definition_json` help (no + credentials in the JSON) and the `sys_webhook` description (dispatched by the + webhook auto-enqueuer onto the shared HTTP outbox, not executed by an HTTP + connector plugin; declared through `defineStack({ webhooks })` too); + - `@objectstack/plugin-audit`: the `sys_activity.environment_id` label and help + (Environment, not Project), and the `sys_audit_log.user_id` label (User: the + object's separate `actor` field is the actor); + - `@objectstack/plugin-security`: the `sys_position` description (positions + distribute capability, not definitions for RBAC access control). + + Leaves whose English source only gained detail, was reworded, or was + title-cased (the `@objectstack/plugin-approvals` status and action options) + are unchanged. Values only: no key is added or removed, and no provenance + table changes. +- e47355b: Auth, webhook and outbound-delivery refusals, warnings and field help no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these three packages show to operators, administrators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/plugin-auth`: an unrecognised audience posture is refused because it must not fall through to a more permissive posture than the one intended; the `ObjectQL` adapter's case-insensitive warning says the `$ieq` operator is deliberately deferred until there is demonstrated pull for it; the `internal`-column refusal says the column is withheld from every ordinary read and recovered only through the engine's accessor; the 2FA re-enrollment errors say a re-enrolled TOTP secret may be live at sign-in without having been confirmed; the walled-owner boot warning says a declared owner is stamped verified only when an operator-provisioned path creates the account; the OTP send-budget lines name the budget without a number. + - `@objectstack/plugin-webhooks`: the parked-event record says the event is recorded rather than delivered unsigned (or without its authored headers) and rather than discarded without a trace; the redeliver refusals say a delivery that cannot be signed is refused rather than sent unsigned; the zero-trigger warning says the `api` trigger was removed because nothing could fire it; the seed and legacy-migration warnings say a credential is never stored in cleartext instead and that a failed migration leaves it cleartext in `definition_json`. + - `@objectstack/service-messaging`: the `sys_http_delivery` field help for `attempts` (in every shipped locale) says a parked row is not redeliverable because it carries no signature; the `headers_json` and `error` help and the outbox refusals drop their citations; the notification `ack()` refusal says cancelling a pending row is not part of the outbox contract until a live consumer needs it. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [422db78] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-messaging@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 3b295d53a6c..c41fa98abc1 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.", "type": "module", diff --git a/packages/qa/dogfood/CHANGELOG.md b/packages/qa/dogfood/CHANGELOG.md index 0a87a7dd560..4a73eb8a3a5 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,247 @@ # @objectstack/dogfood +## 0.0.46 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [7001918] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [6e3aa75] +- Updated dependencies [ba4648d] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [cbaf04c] +- Updated dependencies [4dfff17] +- Updated dependencies [4d04b6b] +- Updated dependencies [cba417a] +- Updated dependencies [9a4b2bb] +- Updated dependencies [b80ab57] +- Updated dependencies [d282087] +- Updated dependencies [9b384f6] +- Updated dependencies [8acdae9] +- Updated dependencies [91e8fa1] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [10c36cc] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [7184436] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [49d2a24] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [00a92e1] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [6f57888] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [525b813] +- Updated dependencies [05be352] +- Updated dependencies [d67b942] +- Updated dependencies [8d329f0] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [bb2eccf] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [1571aed] +- Updated dependencies [5dbeb7d] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [de8cd58] +- Updated dependencies [5f6b63a] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [2f2fa11] +- Updated dependencies [ae1e950] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [9b81314] +- Updated dependencies [58a77db] +- Updated dependencies [a9d36d5] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [39ab294] +- Updated dependencies [70dae53] +- Updated dependencies [9c8b65a] +- Updated dependencies [665cab3] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [2488b98] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [ef96c9e] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [3a7b6eb] +- Updated dependencies [24c554d] +- Updated dependencies [f0cc16e] +- Updated dependencies [1ecb871] +- Updated dependencies [fbcc05f] +- Updated dependencies [0b12b9e] +- Updated dependencies [55012df] +- Updated dependencies [30c530e] +- Updated dependencies [ce4e205] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [3ddd3d0] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [4727fcb] +- Updated dependencies [d2bc644] +- Updated dependencies [2791138] +- Updated dependencies [cfa9315] +- Updated dependencies [61455de] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [422db78] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/plugin-security@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/metadata@17.6.0 + - @objectstack/plugin-audit@17.6.0 + - @objectstack/service-analytics@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/plugin-approvals@17.6.0 + - @objectstack/plugin-auth@17.6.0 + - @objectstack/plugin-email@17.6.0 + - @objectstack/plugin-sharing@17.6.0 + - @objectstack/service-storage@17.6.0 + - @objectstack/trigger-record-change@17.6.0 + - @objectstack/trigger-schedule@17.6.0 + - @objectstack/service-messaging@17.6.0 + - @objectstack/plugin-webhooks@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/verify@17.6.0 + - @objectstack/connector-mcp@17.6.0 + - @objectstack/mcp@17.6.0 + - @objectstack/example-showcase@0.3.20 + - @objectstack/example-crm@4.0.98 + - @objectstack/example-multi-package@0.0.5 + - @objectstack/connector-openapi@17.6.0 + - @objectstack/connector-rest@17.6.0 + ## 0.0.45 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index c38f4db5168..c2036afb2a0 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.45", + "version": "0.0.46", "private": true, "license": "Apache-2.0", "description": "Dogfood regression gate — hand-written golden tests that boot real example apps through @objectstack/verify's in-process HTTP stack, pinning historical runtime regressions (#2018 timezone bucketing, #1994 cross-owner RLS, #2004 field fidelity) that static checks miss.", diff --git a/packages/qa/downstream-contract/CHANGELOG.md b/packages/qa/downstream-contract/CHANGELOG.md index 4e4235a901d..0e416c4e6a8 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,128 @@ # @objectstack/downstream-contract +## 0.0.44 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 0.0.43 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index 90d63d3e4de..5db56384641 100644 --- a/packages/qa/downstream-contract/package.json +++ b/packages/qa/downstream-contract/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/downstream-contract", - "version": "0.0.43", + "version": "0.0.44", "description": "Frozen third-party consumer fixture — a backward-compatibility gate for @objectstack/spec. Authored the way an external project on a published release authors metadata; if a spec change breaks it, that change is breaking (#2035).", "license": "Apache-2.0", "private": true, diff --git a/packages/qa/http-conformance/CHANGELOG.md b/packages/qa/http-conformance/CHANGELOG.md index 63eba456e84..e320a30de94 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/http-conformance +## 0.1.6 + +### Patch Changes + +- Updated dependencies [05a7547] +- Updated dependencies [3fbf3ca] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [889139c] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [89801cd] +- Updated dependencies [bbcd20c] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [d1633f3] +- Updated dependencies [8368f1c] +- Updated dependencies [58a77db] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [097ef80] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d2bc644] + - @objectstack/core@17.6.0 + ## 0.1.5 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 78b45c23fc4..696d6bd3d4a 100644 --- a/packages/qa/http-conformance/package.json +++ b/packages/qa/http-conformance/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/http-conformance", - "version": "0.1.5", + "version": "0.1.6", "private": true, "license": "Apache-2.0", "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) — a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index b785b874c41..6c065f187ed 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,649 @@ # @objectstack/rest +## 17.6.0 + +### Minor Changes + +- e5c7d07: feat(rest): `GET /api/v1/data/:object/export?template=true` answers an xlsx import template for the object (#18386) + + Clause-②: yes (widening) + + The export door takes one more query parameter, `template`. `template=true` + answers an `.xlsx` workbook with no data rows; `template=false` answers the export. + Without a `template` parameter the export is exactly as before, byte for byte. + + - **Columns.** Every field of the object except + those marked `system` or `readonly`, and `formula`, `summary` and + `autonumber` fields, in the order the object declares them. A `hidden` field + that can be written is a column. The seven columns the platform adds to every + object (`organization_id`, `created_at`, `created_by`, `updated_at`, + `updated_by`, `owner_id`, `owning_business_unit_id`) are never template + columns. A field the caller's field-level security does not let them edit is + left out. If the security service cannot say which fields the caller can edit, + the columns are narrowed by the fields the caller can read instead. The + instructions sheet then says so, and the `X-Export-Template-Projection` + response header reads `readable` instead of `writable` (`none` when no + field-level security applies). An explicit `?fields=` list is used as sent. + - **First sheet.** The header row, with ` *` after each field that is required + and has no default value, and one example row to replace or delete. Select, + radio and boolean columns carry a dropdown. + - **Second sheet.** One row per column: the field's API name, its type, whether + it is required, and the values the import accepts for it. + - **Language.** The sheets are in Chinese for a `zh` request locale + (`?locale=` or `Accept-Language`) and in English otherwise. + + The same two permission checks as the export apply: an object that does not + expose export answers `405`, and a caller without the export permission answers + `403`. `template` with a value other than `true` or `false`, a `format` other + than `xlsx`, or any of `limit`, `page`, `filter`, `search`, `searchFields`, + `orderby` or `header` beside `template=true`, answers `400 VALIDATION_ERROR`. +- eb4b17c: fix(rest): `POST /api/v1/data/:object/import` reads a `date`, `datetime` or `time` cell only in ISO 8601, the export's own `YYYY-MM-DD HH:mm:ss` or a year-first date (`2026/7/15`), on a calendar day that exists, and keeps a `date`'s year at four digits (#20534) + + Clause-②: no (narrowing) + + **BREAKING for callers of the import door.** A text cell for a `date`, + `datetime` or `time` field is now read only in one of these spellings, after + trimming: + + - `YYYY-MM-DD`; + - `YYYY-MM-DDTHH:MM[:SS[.fraction]]`, then `Z`, a `+HH:MM` / `-HH:MM` / + `+HHMM` offset, or nothing (a wall clock, read in the importing user's + business timezone, as before); + - `YYYY-MM-DD HH:MM[:SS[.fraction]]` with no offset, which is what the export + writes for a `datetime` cell; + - a year-first date, `YYYY/M/D` or `YYYY-M-D` (a four-digit year, a one- or + two-digit month and day, the same separator twice), optionally followed by + one space and `H:MM` or `H:MM:SS` with no offset, read exactly as the + export shape is; + - for a `time` field, also a bare `HH:MM` / `HH:MM:SS`. + + The day must exist. Every other cell is that row's `invalid_date` error, with + the importer's existing sentence ("is not a valid date" / "datetime" / + "time"). The reader used to hand such a cell to the JavaScript date parser, + which read it in the SERVER PROCESS's timezone and month-first, and rolled an + impossible day into the next month, so the import reported success and stored + a different value. For each shape, change the cell FROM the refused spelling + TO an admitted one: + + - **An impossible day.** FROM `2026-02-30`, `2026-02-29`, `2026-04-31` in any + spelling (a `datetime` `2026-02-30` was stored as 2 March, and so was a + `date` written `2026-02-30T10:00:00Z`) TO the day you mean. Nothing is + rolled over. + - **A locale or prose date.** FROM `07/15/2026`, `07/15/2026 10:00`, + `07/08/2026`, `15 July 2026`, `Jul 15 2026 10:00` (stored hours apart on a + New York and a Shanghai server, a `date` a day apart, and `07/08/2026` read + as 8 July) TO `2026-07-15`, `2026-07-15 10:00`, `2026-07-08` or + `2026-08-07`. No timezone and no field order is guessed. Converting a + spreadsheet column to ISO (in Excel, the cell format `yyyy-mm-dd` or + `yyyy-mm-dd hh:mm:ss`) before export is the fix. + - **A year-first date outside its one form.** FROM a mixed separator + (`2026/7-15`) TO `2026/7/15` or `2026-07-15`. FROM a `T` or a zone on the + year-first form (`2026/7/15T9:00`, `2026/7/15 9:00Z`) TO `2026/7/15 9:00` + (a wall clock in the business timezone) or the ISO `2026-07-15T09:00:00Z`. + FROM a fraction of a second (`2026/07/15 10:00:00.123`) TO + `2026-07-15 10:00:00.123`. A two-digit year (`26/7/15`) is refused, as it + was. + - **A zone after a space, or lower-case `t` / `z`.** FROM + `2026-07-15 10:00Z`, `2026-07-15 10:00:00+08:00`, `2026-07-15t10:00:00z` TO + `2026-07-15T10:00Z`, `2026-07-15T10:00:00+08:00`, `2026-07-15T10:00:00Z`, + the spellings the create and update doors take. + - **A zone-naive `24:00`.** FROM `2026-07-15 24:00` or `2026/7/15 24:00` (read + in the server's zone) TO `2026-07-16 00:00` or `2026/7/16 0:00`. + `2026-07-15T24:00:00Z`, which names its instant, reads as before. + - **A number, reduced or expanded forms.** FROM a JSON number such as `2026` + or an Excel serial, `2026`, `2026-07`, `+002026-07-15` TO `2026-01-01`, + `2026-07-01`, `2026-07-15`. + + **Kept: year-first dates.** `2026/7/15`, `2026/07/15`, `2026-7-15`, + `2026/7/15 9:00` and `2026/08/01 06:00:00`, Excel's default short date in + zh-CN and ja-JP, stay admitted. They are now held to the same rules as every + other cell: the day must exist (`2026/2/30` is refused, never rolled into + March), the hour runs 0 to 23, and the day is stored in its padded ISO form + (`2026/7/15` is stored as `2026-07-15`). A year-first date with no clock + given to a `time` field reads as `00:00:00`, as an ISO day does; it used to + read the server's zone (`04:00:00` on a New York server). + + **The year keeps four digits.** A `date` cell for a year from 0001 to 0999 + (`0500-01-01`) used to leave the reader as `500-01-01`, which the write door + refuses, so the import refused a day the create door takes. It is stored as + written now. A bare day read into a `datetime` field in years 0001 to 0099 + (`0050-01-01`) used to be stored in the 1900s (`1950-01-01T00:00:00.000Z`); it + is stored in its own year now. + + **What is not affected.** Every ISO 8601 cell and every export-shape cell on + a real day reads exactly as before, including a zone-naive cell read in the + business timezone and an offset-bearing cell honoured as written. An xlsx + cell that Excel stores as a date is unaffected: it reaches the reader as the + export shape. The dry run answers the same verdicts as the real write. A + refused cell fails only its own row, and the rest of the batch imports as + before. The same narrowing applies to the exported `coerceRow` helper. + + **If you are refused.** The row's result carries `code: 'invalid_date'` and + quotes the cell, so the file can be corrected and re-imported. + + +- 22e584c: fix(rest)!: `POST /api/v1/data/:object/import` reads a `time` cell by `@objectstack/core`'s one `time` rule, the rule the write door asks, so the `10:00:00.250` that `/export` writes for a `time` with milliseconds re-imports as itself instead of failing its row as `invalid_date`, and a cell the write door refuses is refused with the write door's code, `invalid_time` (#20722) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `/import` accepts in a `time` cell. An ISO 8601 instant whose UTC year has no four-digit spelling (`"9999-12-31T23:00:00-02:00"`, which is UTC year 10000) was stored as its UTC time of day (`01:00:00`). It now fails its row with `invalid_time`, as the write door has refused the same value since #20671. A refused `time` cell's row code also moves from `invalid_date` to `invalid_time`, the code the write door gives for the same value. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). + + The import's `time` reader was a private pattern with no fractional part. A `time` stored with milliseconds (`10:00:00.250`, which the write door accepts and `/export` writes as it is stored) failed its row on re-import with `Clock: "10:00:00.250" is not a valid time`, on every backend, so an export did not re-import. The reader now asks the same rule the write door asks of a written `time`: `isUninterpretableTemporalComparand('time', …)` for the verdict and `temporalStorageForm(…, 'time')` for the stored value. A cell is admitted exactly when the write door admits the same value, and stored as the same wall clock. + + Through the route, the process in America/New_York, on memory, SQLite and PostgreSQL 16 (at `Asia/Shanghai`), before and after: + + | `time` cell | before | now | + |:--|:--|:--| + | `10:00:00.250`, `23:59:59.999` (what `/export` writes) | row failed, `invalid_date` | stored as written | + | `10:00:00.5`, `10:00:00.000` | row failed, `invalid_date` | `10:00:00.500`, `10:00:00` | + | `2026-07-15T10:00:00.250Z`, `2026-07-15 10:00:00.250` | stored `10:00:00`, the fraction dropped | `10:00:00.250`, as the write door stores it | + | `9999-12-31T23:00:00-02:00` (an instant in UTC year 10000) | stored `01:00:00` | row failed, `invalid_time`, as the write door refuses it | + | `10:00Z`, `10:00+08:00`, `07/15/2026 10:00`, `25:00` | row failed, `invalid_date` | row failed, `invalid_time` | + | `10:00`, `10:00:00`, `2026-07-15T18:00:00+08:00` | `10:00:00` | unchanged | + | `2026/7/15 9:00` (the year-first form) | `09:00:00` | unchanged | + + A zone-naive `2026-07-15 24:00` in a `time` cell, refused before, is now read as `00:00:00`: core's rule reads it so, and the write door admits it. A `date` or `datetime` cell keeps `invalid_date`, and the row's sentence is unchanged for every kind. A time of day with a `Z` or an offset stays refused: a `time` carries no zone. The year-first date-time (`2026/7/15 9:00`) is still read as its wall clock; it is the one reading the import has that the write door has not. The dry run reports the same verdicts. + + **Who is affected.** A caller of `POST /api/v1/data/:object/import`, including the dry run, whose file carries such an instant in a `time` column now gets that row refused. A client that matched the row report's `code` for a refused `time` cell now reads `invalid_time`. Rows already stored are not rewritten. + + This supersedes the note in the `@objectstack/objectql` entry for #20671 that the server import turns a `time` cell into `HH:MM:SS`: the import now keeps a non-zero fraction (`HH:MM:SS.fff`). +- bafb8c9: fix(rest)!: a public form's lookup picker searches and sorts by the first display field the caller may query, so a picker whose first display field is masked for its caller serves its rows instead of answering 403 (#21062) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this widens what the public lookup picker serves and narrows it in one composition. The narrowing: with a security service that lacks `ISecurityService.getQueryableFields`, or that answers no answer for the object, the picker passes over every display field whose declaration carries a `maskingRule`, for every caller, including a caller the rule is lifted for. So its search and order move to the next display field that declares no rule, and a picker whose display fields all declare a rule is refused `403 PERMISSION_DENIED` without the engine being asked, where it used to be served. The security service this repository ships implements the method, so a deployment using it is not narrowed. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changed.** The public lookup picker (`GET /forms/:slug/lookup/:field`) + matches the visitor's search and orders its rows by one key. That key used to + be the first entry of `publicPicker.displayFields`. It is now the first entry + the caller may query on, as the security service answers it + (`ISecurityService.getQueryableFields`). A field whose masking rule applies to + a caller is served to that caller masked, and the engine refuses to search or + sort on it with `403 PERMISSION_DENIED`. A picker whose first display field + declares such a rule therefore answered `403` to every caller the rule applies + to, on every request. It now serves its rows, sorted and searched on the next + display field the caller may query. The masked field is still returned in each + row, masked, as before. + + **When no display field is queryable** for the caller, the picker answers + `403 PERMISSION_DENIED` with the engine's refusal for those fields, without + running a query. + + **Unchanged.** A picker with no masked display field, and a caller the masking + rule is lifted for, keep the first display field as the key, with the security + service this repository ships. A deployment with no security service keeps the + first display field. + + **What to do.** Nothing. To choose the field a picker searches when its first + display field is masked for some of its callers, list a field those callers may + query among `displayFields`: the first such entry is the one searched and + sorted on. A picker whose only display fields are masked for its callers is + refused, so give it one they may query. + + **What to do after upgrading, if your security service predates `getQueryableFields`.** + Implement `getQueryableFields` on it: it answers which fields a caller may filter, + sort, group or aggregate by, and the picker then keys on the first display field + in that answer. Until it does, give each picker at least one display field that + declares no `maskingRule`, or the picker is refused for every caller. +- 3dc33b2: **BREAKING** — an anonymous public form no longer offers record search. The form field's `publicPicker` block (`view.form.sections[].fields[].publicPicker`: `displayFields`, `maxResults`, `filter`, `object`) is removed, and the anonymous lookup route `GET /api/v1/forms/:slug/lookup/:field` is deleted. A public form's `lookup`, `master_detail` and `user` fields are now always left off its anonymous rendering, whatever the form declares. + + Clause-②: yes (narrowing) + + Retired immediately (ADR-0087 D2), with no alias window: the maintainer's ruling reverses the earlier one that had declared the key. Mainstream web-to-lead forms do not let an anonymous visitor search records either, and no example, template, plugin or first-party UI declared or called the picker. + + ## FROM → TO + + | you wrote (17.5 and earlier) | write instead | + | --- | --- | + | `{ field: 'account', publicPicker: { displayFields: ['name'], maxResults: 10 } }` on a public form | delete the `publicPicker` block — the field is left off the anonymous rendering anyway | + | a public form whose visitors chose from a short, fixed list of records | a `select` field with static `options` listing the choices | + | a public form whose visitors had to pick an existing record | the same form behind sign-in (an internal form), where the lookup field searches with the signed-in user's own access | + | a client calling `GET /api/v1/forms/:slug/lookup/:field` | nothing to call: the path is no longer registered and answers what any unregistered path answers (`404 ENDPOINT_NOT_FOUND`) | + + **The one-line fix:** delete the `publicPicker` block; an anonymous public form no longer offers record search. Use a `select` field with static `options`, or put the form behind sign-in. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`FormFieldInput` types the key `never`), and the parse — `defineView()`, `defineStack({ views })`, `os validate`, `PUT /api/v1/meta/view/:name` — refuses it at `…sections[N].fields[N].publicPicker` with the prescription: + + > `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + **What a REST client sees.** The two error codes only that route produced, `LOOKUP_NOT_PUBLIC` and `LOOKUP_TARGET_MISSING`, leave the error-code ledger with it. `GET /api/v1/forms/:slug` and `POST /api/v1/forms/:slug/submit` are unchanged apart from the unconditional strip above. + + ## The retirement kit + + - **A `retiredKey()` tombstone on the form field**, so the parse carries the prescription instead of a bare unknown-key verdict. The block's own schema and its two types go with it: `FormFieldPublicPickerSchema`, `FormFieldPublicPicker` and `FormFieldPublicPickerParsed` are no longer exported, and `ui/FormFieldPublicPicker` is no longer published as a JSON Schema. + - **The D2 conversion `form-field-public-picker-removed`** (protocol 18, retired from the load path) deletes the key from every form field of every form payload — `sections[]`, `groups[]`, top-level `fields[]` and nested rows. Its D3 record is the semantic entry `form-field-public-picker-retired`, which asks the author how a visitor should now choose. + - **`@objectstack/rest`:** the lookup route and its filter-lowering helper are deleted, and the resolve route's strip of lookup / `master_detail` / `user` fields no longer has an opt-in. + - **`@objectstack/lint`:** the preset-comparand rule no longer reads a picker's `filter` (its claiming reader for that position went with the key). + + ## What an operator with a STORED form sees + + A `sys_metadata` view row saved before this release may still carry the key. Nothing breaks at read: the conversion replays on rehydration and strips it, so the view is served canonical and parses, and the field stays off the anonymous rendering either way. `os migrate meta --stored` lists those rows, and `--apply` rewrites them. + + + +### Patch Changes + +- 6f1f1c1: fix(rest): the import template (`GET /api/v1/data/:object/export?template=true`) puts ` *` on a column only when the import refuses a row that leaves it blank + + Clause-②: no + + - A required field whose option list marks an option `default: true` no longer gets ` *` in the header, and the instructions sheet lists it as not required. A blank cell in that column is imported as the marked option. + - A required field that declares `defaultValue: null` now gets ` *`, unless one of its options is marked `default: true`: the import treats `null` as no default and refuses the blank. A required field whose default is `''`, or `[]` on a multi-valued field, now gets ` *` too: the required check refuses that default. + - A required `system`, `readonly` or `autonumber` field named in `?fields=` no longer gets ` *`: the import does not refuse a blank in it. + - Each dropdown's error title, which is the column header, is cut to 32 characters, the longest title Excel's data-validation dialog takes. +- 67c1b11: fix(rest): `GET /api/v1/data/:object/export` writes a `date` or `datetime` cell with a four-digit year, so an export of a year from 0001 to 0999 re-imports (#20602) + + Clause-②: no + + A `date` of `0500-01-01` exported as `500-01-01`, in CSV, xlsx and JSON alike, + and so did the day of a `datetime` cell whose business-timezone day fell before + year 1000: the instant `1000-01-01T02:00:00.000Z` exported in America/New_York + as `999-12-31 21:03:58`. `POST /api/v1/data/:object/import` reads a four-digit + year only, so re-importing the platform's own file refused that row as + `invalid_date`. The export now spells every `date` and `datetime` cell's day + with the storage rule the write doors use (`temporalStorageForm` from + `@objectstack/core`): `0500-01-01` and `0999-12-31 21:03:58`, which the import + reads back as the same day and the same instant. + + **What is not affected.** Every cell whose day falls in the years 1000 to 9999 + exports byte for byte as before, in every business timezone and with none. The + clock of a `datetime` cell is unchanged. A `datetime` stored before year 1000, + which the write doors now refuse, exports with a padded year as well, and the + import refuses it as `invalid_date`, as the write doors do. A year outside 0001 + to 9999 stays unpadded, and a `datetime` whose business-timezone day falls in + such a year now spells that year as the storage rule does (`0-12-31`, not the + era year `1-12-31`); the import refuses both spellings, as before. +- 72f8c38: fix(rest): `GET` / `POST /security/explain` answers a refusal the security service classified with that refusal's own status and code, instead of `500 EXPLAIN_FAILED` (#20603) + + Clause-②: no + + The explain service can refuse a request with an ADR-0112 envelope: a `code` and a 4xx `status`. The measured case is a row-level policy that the record matcher cannot evaluate. The service then answers `INVALID_FILTER` / 400, the same answer the find it explains gives for that filter. This happens for a record-grained explanation (`recordId`), for an object-level explanation, and for a `recordId` that no row carries. + + The route's error handler recognised only `PERMISSION_DENIED` (403) and `OBJECT_NOT_FOUND` (404). Every other throw answered `500` with `error.code: 'EXPLAIN_FAILED'`. So through HTTP the explain call reported a server fault, while the find it explains reported the caller's error. A client reading that 500 retries or reports an outage, where the platform means "this policy cannot be evaluated". + + The route now asks the same classification the `/data` door uses. A throw that declares a 4xx `status` (or `statusCode`) and a `code` answers with that status and that code in the nested envelope, `{ success: false, error: { code, message } }`. The message is bounded the way `/data` bounds a refusal's message. + + Unchanged: + + - A throw that is not classified still answers `500 EXPLAIN_FAILED`. That covers a plain `Error`, a declared 5xx, and a `code` with no `status`. + - The `403 PERMISSION_DENIED` and `404 OBJECT_NOT_FOUND` answers. + - A successful explanation's body. +- 165c1d4: fix(rest): an import row names the column the engine refused, and a missing database column is no longer called an unknown field (#20701) + + **`POST /api/v1/data/:object/import` — a failed row names its column.** A row the + engine refuses with `INVALID_FIELD` (a column that names no field of the object) + now carries `field` with that column, on the dry run and on the commit alike. It + used to carry only `code: 'INVALID_FIELD'`, while `POST /api/v1/data/:object` + named the field for the same key. The row reads the error's own `field` when no + field-level finding names one; a field-level finding still wins. A unique + conflict row now names its column too, when the database said which column it + was, as the `409` does. + + **A missing database column says what the database said.** When the database + reports that a table has no column for a field, the `400 INVALID_FIELD` answer + now reads "The database table of object 'X' has no column for field 'f'. If the + object declares 'f', its database schema has drifted from the metadata: run + 'os migrate' to reconcile." It used to read "Unknown field 'f'", which was false + for a field the object declares: the engine refuses an undeclared key itself, + before the database is reached, and keeps its own "Unknown field" wording for + that case. `code`, `status`, `field` and `object` are unchanged. +- d7b9817: fix(rest): an import row for a NOT NULL refusal or a unique conflict answers what the create door answers (#20701) + + **`POST /api/v1/data/:object/import` and the async import job — a NOT NULL + refusal.** When the database refuses a row because a NOT NULL column has no + value (for example a field declared `storage: { notNull: true }` and not + `required`, which the engine's own check lets through), the committed row + now fails with `code: 'required'`, `field` set to the field, and the sentence + `POST /api/v1/data/:object` gives for it ("f is required"). It used to fail with + the database's own code (`SQLITE_CONSTRAINT_NOTNULL` on SQLite) and no `field`. + The create door answers `400 VALIDATION_FAILED` with a `required` finding for + the field; the row reports that finding the way it reports a `required` field + the engine refuses itself, so no database dialect's code reaches the row. + + **A unique conflict.** A committed row that repeats a unique value keeps + `code: 'UNIQUE_VIOLATION'` and its `field`, and now carries the create door's + sentence, "A record with this f already exists", in place of the engine's longer + sentence (which the create door returns as `developerMessage`). + + The async job's rows, read from `GET /api/v1/data/import/jobs/:jobId/results`, + change the same way. The row takes these answers from the same mapper as the + create door, as it already does for a missing database column. No key is added + to the row. The dry run still previews such rows as `ok`, because it checks the + metadata and does not judge `storage.notNull` or uniqueness. +- f80e2a6: fix(rest): an import row for a missing database column answers what the create door answers (#20701) + + **`POST /api/v1/data/:object/import` and the async import job.** When an object + declares a field whose database column is missing (the schema has drifted from + the metadata), a committed row that writes that field now fails with + `code: 'INVALID_FIELD'`, `field` set to the field, and the sentence + `POST /api/v1/data/:object` gives for the same key: "The database table of + object 'X' has no column for field 'f'. If the object declares 'f', its database + schema has drifted from the metadata: run 'os migrate' to reconcile." It used to + fail with the database's own code and text (for example `SQLITE_ERROR` and + `table X has no column named f`) and no `field`. The async job's rows, read from + `GET /api/v1/data/import/jobs/:jobId/results`, change the same way. + + The row now classifies a write error through the same mapper as the create door, + and takes that answer when it is `INVALID_FIELD`. The dry run still cannot see a missing column, + because it checks the metadata and not the table, so it previews such a row as + `ok`. +- 7afdc5c: fix(rest): an org's published edit to a packaged dashboard or view is what the `/meta` item and list reads serve, in every locale, instead of the packaged translation of the string it replaced + + An organization may edit a packaged dashboard or view in place and publish the edit. The metadata protocol's reads returned the edit, and `?layers=true` reported it as effective, but `GET /api/v1/meta/dashboard/:name`, `GET /api/v1/meta/dashboard`, `GET /api/v1/meta/view/:name` and `GET /api/v1/meta/view` served the bundle's translation of the string the package shipped. For example, a widget retitled `Total Users (edited)` on the platform's `system_overview` dashboard was served as `Total Users` to an `en` reader and as `用户总数` to a `zh-CN` reader. + + The translators in `@objectstack/spec/system` already let an edited string win over the bundle when they are handed the item as the package shipped it, and the metadata protocol already answers that item (`getPackagedDashboardBase`, `getPackagedViewBase`). The `/meta` reads handed it over for objects only. They now hand it over for dashboards and views too. The change is in the translation step that both `/meta` transports share, the REST server's routes and the runtime's HTTP dispatcher. A view is looked up by its full `.` name. + + What a reader sees now: + + - An edited string is served as written, in every locale. + - A widget or view the org left alone is still translated. + - Resetting the overlay brings back the shipped string and its translation. + - A dashboard or view with no org edit is served exactly as before. + + This fixes what the metadata reads serve, not yet what the console draws. The console built from objectui `db11afd4967c`, this repository's pin when the change was made, looks a dashboard's widget titles and a view's label up in the bundle again in the browser, so it still draws the packaged translation over an edit the server now serves: measured, the `system_overview` board shows `Total Users` / `用户总数` and a `zh-CN` view tab shows `进行中`. + + Nothing to migrate: no key, export or route changed. +- f115b1f: REST refusals, warnings and the served OpenAPI text no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + A few strings `@objectstack/rest` sends to callers and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `POST /data/:object/import` with a named mapping that declares a `javascript` transform: the `UNSUPPORTED_TRANSFORM` message now says the import path does not execute it (there is no server-side sandbox), so the import is refused rather than run with that transform skipped. + - `GET /openapi.json`: the built-in section's response description says the section is built from the routes this server actually mounts and that payload schemas are deliberately not invented; the request-body description says the document leaves the route-specific shape undescribed rather than invent one. + - The boot warning for a config that still sets the retired `api.requireAuth` drops its citation; it already says the key was removed and anonymous access to object data is always denied. + - `/discovery`'s `capabilities.transactionalBatch.description` cites ADR-0034 alone. + + Text only: no status, error code, field, route or control flow moves. A client that matches the old message text (for example the tracker-number suffix the `UNSUPPORTED_TRANSFORM` message used to end with) needs the new spelling. +- f6ccca4: fix(objectql,rest): a `date` or `datetime` value refused for its year says so — "must be a date in the years 0001 to 9999" / "must be a datetime whose UTC year falls in the years 1000 to 9999" — instead of "must be a valid date (ISO-8601)", which was false for a value such as `0500-07-15T10:00:00Z` (#20846) + + Clause-②: yes (widening) — one new export on `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`. No value's verdict moves and no wire key moves: the field code stays `invalid_date` and its `constraint` stays `{ type }`. + + `POST` / `PATCH /api/v1/data/:object` and each row of `POST /api/v1/data/:object/import` + refuse a `date` outside the years 0001 to 9999 and a `datetime` whose UTC year falls + outside 1000 to 9999. When the value itself is readable — an ISO 8601 string such as + `0500-07-15T10:00:00Z` or `+010000-01-01`, or a `Date` — the refusal's message now + names the kind's years. An author who read "not valid ISO" rewrote the spelling, and no + spelling of that year is admitted. + + - `@objectstack/spec`: the validation message catalog gains `invalid_date_range` and + `invalid_datetime_range` in `en`, `zh-CN`, `ja-JP` and `es-ES`. They are two more + sentences of the `invalid_date` code, never a wire value. The years are the template + parameters `{{firstYear}}` / `{{lastYear}}`. A deployment that overrides a message + under `validation.field.invalid_date` or `validation.field.invalid_datetime` does not + cover these values. To override their text, define + `validation.field.invalid_date_range` / `validation.field.invalid_datetime_range`. + - `@objectstack/core`: `SUPPORTED_TEMPORAL_YEARS` (`{ date: { first: 1, last: 9999 }, + datetime: { first: 1000, last: 9999 } }`, frozen) is the range + `isOutsideTemporalYearRange` judges by. It is exported so a refusal names the range + from the source the doors use, never a copy of its numbers. + - `@objectstack/objectql` and `@objectstack/rest`: the record validator and the import's + cell reader choose the range sentence for such a value. An import cell with more than + four year digits (`+010000-01-01`) is refused by the import's reader. It used to read + "is not a valid date" and now gets the same range sentence as the write door. + + **What is not affected.** Which values are refused is unchanged, and so is the refusal's + code (`invalid_date`) and `constraint`. A value that is not readable keeps its sentence: + "must be a valid date (ISO-8601)" at the write door, `"…" is not a valid date` at the import. + So does a number, which is never a written `date` or `datetime`. +- 8f78495: fix(rest): the import template (`GET /api/v1/data/:object/export?template=true`) is gated by the import door's permissions, not the export's + + Clause-②: no + + The template carries no records — only the columns the caller may write, one + example row and an instructions sheet — so it answers to whoever may import, and + the export permission (`allowExport`) neither admits nor refuses it: + + - A caller with the create permission on the object gets the template, with or + without `allowExport`. + - A caller without the create permission gets `403 PERMISSION_DENIED`, with or + without `allowExport`. + - An object whose `enable.apiMethods` exposes neither `create` nor `update` + answers `405 OBJECT_API_METHOD_NOT_ALLOWED`, as `POST /api/v1/data/:object/import` + does. An object that exposes `create` without `list` serves the template. + - Without `template=true` the export is unchanged: the same two export checks + and the same bytes. + + To let a role download the template, grant it create on the object. +- 8368f1c: refactor(rest): the import runner, coercion, mapping apply, field-meta map and error classification moved to `@objectstack/core` / `@objectstack/types`; `rest` re-exports them (#20919) + + `runImport`, `coerceRow`, `buildFieldMetaMap` and their types (from the package + index), and `mapDataError` with its sibling classification exports, are now + re-exported from their new homes — byte-identical code, the same names, the same + behaviour at both import routes and at `plugin-auth`'s identity import. Nothing to + change for consumers. +- e161ad3: fix(rest): the `hint` on a NOT NULL refusal leads with the remedy for a column that requires a value, and names schema drift only as a condition, so an author who declared `storage: { notNull: true }` is no longer sent to `os migrate` (#20963) + + Clause-②: no + + A field that declares `storage: { notNull: true }` without `required` is a column + the database keeps NOT NULL on purpose (ADR-0113). A write with no value for it, + through `POST /api/v1/data/:object` or `PATCH /api/v1/data/:object/:id`, is + refused by the database and answers `400 VALIDATION_FAILED` with a `required` + finding for the field. That answer was right. Its `hint` said the field is + optional in the metadata and "the physical schema has drifted from metadata", + and told the caller to run `os migrate`, which changes nothing for a column + declared NOT NULL. + + The `hint` now reads: "The database column for 'FIELD' requires a value: provide + it, or declare the field `required` in the object metadata. If the object + declares neither `required` nor `storage: { notNull: true }` for 'FIELD', the + physical schema has drifted from metadata instead: run 'os migrate' to reconcile + (or reset the dev database)." The first sentence holds for every NOT NULL + refusal. The second names the drift case under the condition that makes it drift. + + **What is not affected.** The status, `code`, `error`, `fields` and `object` of + the answer are unchanged, and no key is added. The import row is untouched: it + does not carry a `hint`. Nothing reads the field map to tell the two cases + apart, so the `hint` stays advice for the author to read against the object's + declaration. +- 514001a: fix(rest,runtime): the published-snapshot read of a flow name a managed package ships answers the package's flow, as the layered read does (#21002) + + Clause-②: yes (widening) + + `flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Since the previous half of #21002, the layered read, `GET /api/v1/meta/flow/:name/layers`, reports the package's flow as the effective layer for a name a managed package ships, and a stored flow of that name as a separate layer that does not take effect. The published-snapshot read, `GET /api/v1/meta/:type/:name/published`, and its runtime-dispatcher twin read that same layered answer, but served its stored layer whenever one was present. So for such a name they still answered `200` with the stored flow, not the package's. + + Both published-snapshot doors now serve the layered read's effective layer when that read put the package's flow over a stored flow, which is the package's flow. They ask the metadata protocol's own check for that decision rather than repeating it. In every other case they answer exactly as before: a flow name no managed package ships, and every other metadata type, `object` included, still answer the stored layer when one is present, and an item with no stored layer still falls through to the code/package snapshot. The stored flow is not deleted, rewritten or refused. + + **The widening.** `@objectstack/metadata-protocol` makes one existing method public: `ObjectStackProtocolImplementation.isShippedFlowName(type, name)`. It answers whether `name` is a flow name a managed package ships. It was private to the class, so a door in another package could not ask it any other way. Its answer is unchanged, and the layered read, the by-name read and the flow list keep calling it. +- 7a606a9: fix(rest,runtime): reading `datasource` and `external_catalog` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability each type's own door already requires (#21087) + + Clause-②: no + + - A `GET` or `HEAD` of `/api/v1/meta/datasource` or `/api/v1/meta/external_catalog` (and their plural spellings) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`GET /api/v1/datasources`, `GET /api/v1/datasources/:name`) and the federation read door (`GET /api/v1/datasources/:name/external/tables`) already require for the same data. Every read route under the type is judged alike: the list, the item read and each of its query switches, `/published`, `/layers`, `/history`, `/audit`, `/diff` and `/references`. `/history`, `/audit` and `/diff` still also require an authoring capability, as before. + - A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. The answer is the same whether or not the named item exists, and nothing is read from the metadata store first. + - Holders of `manage_platform_settings` are served exactly as before. Platform administrators hold it through `admin_full_access`. Every other metadata type, and every write route, is unchanged. + - Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by. + - If you read either type with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or read through a caller that already has it. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- 454bbb6: fix(rest,runtime): writing `datasource` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability the datasource admin door already requires (#21124) + + Clause-②: no + + - A write of a `datasource` definition through `/api/v1/meta` (and its plural spelling) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`POST /api/v1/datasources`, `PATCH` and `DELETE /api/v1/datasources/:name`) already requires for the same create, update and remove. Every write verb is judged alike: the save (`PUT /meta/datasource/:name`, a draft save included), the reset (`DELETE`), `/publish` and `/rollback`. + - A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. Nothing is written. The answer is the same whether or not the named item exists. + - The write doors' own authoring admission is unchanged and still applies, so a datasource write needs `manage_platform_settings` and `manage_metadata` both. Platform administrators hold both through `admin_full_access`. Every other metadata type, and every read route, is unchanged. `external_catalog` writes are unchanged: that type's own write door requires `manage_metadata`. + - Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by. + - If you write datasource definitions through `/api/v1/meta` with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or write through the datasource admin door with a caller that already holds it. +- 04b202e: Provenance comments in `@objectstack/rest` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no route, error code, refusal text, type, export or runtime behaviour + changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [697845d] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/observability@17.6.0 + - @objectstack/service-package@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 53f9b41a565..93e187fa09a 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack REST API Server - automatic REST endpoint generation from protocol", "type": "module", diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 438a6808027..89b20b88d0a 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,558 @@ # @objectstack/runtime +## 17.6.0 + +### Minor Changes + +- cb4c31d: fix(runtime)!: the /automation create and update doors save the flow as a tenant row, so what they answer 200 for survives a restart, and the removal door deletes that row too (#20862) + + Clause-②: no (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. `POST /api/v1/automation` and `PUT /api/v1/automation/:name` now refuse a definition the metadata store refuses, which they used to register and answer `200` for. `DELETE /api/v1/automation/:name` now relays a store's refusal to delete the flow's row. + + **What changed.** The create and update doors registered a flow in the automation engine and wrote no metadata row. The next boot binds flows from the stored metadata, so a flow created through `POST /automation` was gone after a restart, and an update through `PUT /automation/:name` to a flow stored through `/meta` lost to the stored definition. Both doors now save the definition through the metadata protocol's own `saveMetaItem`: the save `PUT /api/v1/meta/flow/:name` uses, and the one the clone door (`POST /automation/:name/clone`) already used. The row is env-wide and live (`active`), so the flow reads back on `/meta` and survives a cold boot. The three doors share one path. + + - **Engine first, store second.** The engine's registration is still the first check. Its refusal is answered as before (`400 VALIDATION_FAILED`), and nothing is saved. + - **A save the store refuses is relayed with its own code and status, and leaves no registration behind.** A create is withdrawn from the engine. An update puts back the definition the engine held, so a refused update does not take the flow down. + - **`DELETE /automation/:name` deletes the tenant row too**, through `deleteMetaItem`, so a flow created through the door does not come back at the next boot. The engine's own removal refusal (`DELETE_RESTRICTED` / `409`) is still raised before the store is touched. A name with no stored row is removed as before. A delete the store refuses puts the definition back and relays the refusal. + - **Unchanged:** the locked-base refusal on a packaged flow's name (`403 NOT_OVERRIDABLE`) and the refusal of a definition claiming a package's provenance (`422 INVALID_METADATA`) still answer first. A composition with no metadata protocol keeps the engine-only registration and removal it always had. + + **Newly refused, because the metadata store refuses them** (measured on the showcase): + + - A flow name with a leading underscore. `FlowSchema` admits it and the metadata item-name grammar does not, so the door answers `400 INVALID_REQUEST`. Rename the flow to a name that starts with a letter. + - A definition a gating runtime publish rule refuses, such as a default edge that also carries a condition (`flow-default-edge-with-condition`). The door answers `422 INVALID_METADATA` with the rule's finding. Fix the definition as the finding says. + + Such a flow could never be stored, so before this change it ran only until the next restart. +- f20f669: fix(cli): `os migrate plan` / `apply` no longer run the app's `onEnable` or a host plugin's post-declaration hooks during their boot + + Clause-②: yes (widening) + + The two schema commands boot the host's stack to read what it declares. That boot ran the + config's `onEnable`, and every `kernel:bootstrapped` / `kernel:listening` hook a host plugin + registered from `init()`. A hook that reads a table the plan does not declare then failed on + every plan. On `examples/app-crm`, whose `onEnable` binds positions to permission sets, each + plan printed six `[sql-driver] DATABASE_ERROR` lines and six `position binding lookup failed` + warnings, on a database `apply` had just migrated as well as on an absent one. + + The boot now composes host code for its declarations only: + + - `AppPlugin` takes a new `skipOnEnable` option. When it is set, `start()` does not run the + bundle's `onEnable`, logs that it withheld it, and reports it through `onEnableWithheld`. The + migrate commands set it on the app they compose from `objectstack.config.ts`. + - A host plugin's `init()` gets a context that does not register `kernel:bootstrapped` or + `kernel:listening` hooks. The kernel contract defines those phases as work after registration + ends: reconcile/backfill, and opening listeners. `kernel:ready` hooks still run, and the + write guard still refuses their row writes. `kernel:shutdown` hooks and data hooks register + as before. + - The plan's notes, and the `--json` payload's `composition.notes`, carry one line naming what + was not run. + + The plan itself is unchanged: the same tables, the same pending DDL, the same drift. `apply` + still flushes the DDL the operator confirms and still runs the coverage pass. The platform's own + plugins are untouched, so the value-shape gate announcement still prints. + + `@objectstack/runtime` widens its public surface, additively: `AppPlugin`, exported from the + package root, gains the optional constructor option `skipOnEnable` (default `false`) and the + read-only getter `onEnableWithheld`. A composition that does not pass the option gets exactly + the behaviour it had, `onEnable` included. +- 2bddb19: fix(runtime)!: the analytics dispatcher faces refuse an unauthenticated caller with `401 UNAUTHENTICATED`, like every other data-serving door (#21061) + + Clause-②: no (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. The three analytics faces the dispatcher mounts under `/api/v1/analytics` (cube read, SQL echo, meta) now answer a caller without a session `401 UNAUTHENTICATED`, in the dispatcher's wrapped envelope (`{ success: false, error: { code: 'UNAUTHENTICATED', message, httpStatus: 401 } }`). They answered that caller as a guest before. + + **What changed.** The analytics domain handler opens with the shared anonymous-deny decision (`shouldDenyAnonymous`, ADR-0056 D2), the same floor the `/data`, `/meta`, `/actions`, `/automation` and `/packages` doors stand on, and the one the REST analytics dataset door already applied. The floor is the handler's first statement: + + - it runs before the analytics service is looked up, so an unauthenticated caller gets `401` whether or not the analytics capability is installed, never the `404` an empty slot answers; + - it runs before the request body is validated, so a malformed body from an unauthenticated caller is `401`, never the `400 VALIDATION_FAILED` the entry check answers. + + **What is not affected.** A signed-in caller, an API-key caller and an internal system context are served exactly as before: the same `200`, the same `400` for a malformed body, the same `404` when no analytics service is installed. Object admission and the row scope behind the service are unchanged by this release. + + **If an analytics call now answers `401`,** it was made without a session: send it with the signed-in user's session or bearer token, or with an API key. + +### Patch Changes + +- 5a23096: Warnings, refusals and hints that cited a tracker number now say what was decided + + Clause-②: no + + Several runtime strings an author or operator reads sent the reader to an issue-tracker number for + the reason behind them. Each now states that reason in the sentence itself: + + - `@objectstack/objectql`: the two data-event warnings. A write that names no single record publishes + no per-record event rather than one with an empty `recordId`; a predicate (`multi: true`) write + publishes its own `data.records.*` event carrying the affected-row count and nothing else, so a + driver result that is not a count publishes no bulk event either. + - `@objectstack/service-automation`: the warning for a pausing node type that never declares + `resumeAuthority`, the generic-route resume refusal (its log line and its error text), and the + refusal of a suspension from a type that declares `supportsPause: false`. An undeclared + `resumeAuthority` resolves to `'service'` (fail-closed), so the generic resume route refuses those + pauses; guessing `'any'` is how a raw resume once walked past an approval decision no service had + recorded. + - `@objectstack/runtime`: the endpoint step's `NOT_IMPLEMENTED` message and its two hints (the + composed runtime always threads the policy context and the execution wiring, because execution is + reachable only past the policy chain), and the endpoint mapping refusals (the publish gate rejects + the same shapes, so a declaration that reaches the runtime check was stored without passing it). + + Text only: no error code, field name, status or behaviour changes. +- c96beb2: fix(security): a flow's inbound-hook secret is withheld from every served flow definition, and a read → edit → republish round trip keeps it (#20552) + + Clause-②: yes (widening) + + **The widening.** `@objectstack/metadata-protocol` gains one public method, + `ObjectStackProtocolImplementation.getMetaItemsForExecution`. It returns the stored + bodies without the serving decorations, for in-process binders that execute what they + read. No door that answers a caller may use it. + + An `api` flow's start node carries its inbound hook's HMAC secret (`config.secret`, + ADR-0041), the one credential that hook has. Every read that served the flow's + definition served the secret with it, to any authenticated caller. It is now + withheld from what is SERVED, and from nothing the engine executes. + + **What no longer carries the secret.** The automation domain's flow-definition read + and the flow its `POST` / `PUT` / clone doors answer with; and on the metadata plane, + every read of a flow — item, list, layered, draft preview, published snapshot, diff, + audit — plus a package export. The key is removed, not masked: a mask is a non-blank + string the registration gate would accept as the secret. + + **Consequence for a reader.** A client that read the secret back from a definition + no longer can. A package exported from one deployment and imported into another + arrives without it, and its `api` flows are refused at registration until a secret is + set on the start node again. + + **The round trip.** A save that carries the projected form — no `secret` where the + read served none — keeps the stored secret, on both authoring surfaces (the metadata + plane's save door and the automation domain's `PUT` / `POST`). Only an explicit value + replaces it, so a rotation is written as before. The start node is matched by its + `id`, not its position, so an edit that reorders `nodes` keeps it too. The first save of an item that has no stored row yet, such as a code-authored flow or datasource, takes the value from the code layer the read served, for every type with a registered redactor. + + - `@objectstack/service-automation` owns the projection (`redactFlowCredentials`) and + registers it as the `flow` read-path redactor at plugin `init`. The engine keeps + binding with the stored secret: it now reads flows from the protocol's execution + face, because the served face no longer holds the credential its hooks verify + against. + - `@objectstack/metadata-protocol` gains `getMetaItemsForExecution` on + `ObjectStackProtocolImplementation` — the same flattened list `getMetaItems` + serves, without the serving decorations (no `_diagnostics`, no credential + redaction). It is for in-process engines that execute what they read; every door + that answers a caller keeps serving `getMetaItems`. `carryForwardRedactedValues` + now follows a redacted path through an array by the element's `id`. + - `@objectstack/metadata`'s `getPublished` applies the type's registered read-path + redactor to the body it returns. It was the one metadata read exit that served a + stored body without it. +- 3f45b6c: fix(security): every credential a flow definition holds is withheld from what is served, at every depth, and an edit round trip keeps each one where it belongs (#20590) + + Clause-②: no + + **What is now withheld.** Beside an `api` flow's inbound-hook secret (the start node's + `config.secret`), every served flow definition now also withholds an `http` node's + outbound signing secret (`config.signingSecret`), and both are withheld wherever the + node sits: at the top level, or inside a `loop` body, a `parallel` branch, or a + `try_catch` region. The engine still executes the stored values. + + **Removing a signing secret.** A definition saved back without the key keeps the + stored secret, because an absent key is what every read serves. To remove it, save + the key as the empty string (`signingSecret: ''`): the durable callout is then + delivered unsigned, and the empty value is served as written, so the next round trip + keeps it cleared. + + **Changing a node's kind.** An edit that keeps a node's `id` and changes its kind no + longer carries that node's stored credential onto it. The credential belonged to the + old kind; a start node that needs a secret asks for one again at registration. + + **Moving a node.** A node moved into or out of a `loop` body, a `parallel` branch or a + `try_catch` region keeps its stored credential across the round trip, as long as its + `id` and kind are unchanged and it is the only node, at the top level or in any region, + that carries that `id`. An edge or a config value with the same `id` does not count. + + **The `/meta` list read on a dispatcher host.** When the metadata protocol's list read + fails, the list answers that failure (`503 SERVICE_UNAVAILABLE` for a store outage, or + the protocol's own refusal) instead of serving the metadata service's stored list, + which applies no credential redaction. A host whose protocol has no list verb keeps + its metadata-service fallback. +- 96e7244: fix(runtime): `POST /api/v1/automation/:name/clone` is served over HTTP + + Clause-②: no + + Cloning a flow under a new machine name (ADR-0126 §7.1) is how an admin customizes a packaged + flow whose base is locked. The runtime implemented the clone, but the dispatcher never mounted + its route, so every clone answered `404 ENDPOINT_NOT_FOUND` before the request reached it: from + the API, and from the Clone dialog on Setup's packaged-automation page, for every caller and + every body. + + The route is now mounted beside `POST /automation/:name/toggle`, at `/api/v1/automation/:name/clone` + and, when environment scoping is enabled, at `/api/v1/environments/:environmentId/automation/:name/clone`. + It answers what the clone implementation already answered: `200 { flow, notice }` for a legal + clone, `400` for a missing or illegal `name` or `label`, `404` for an unknown source flow, + `409 RESOURCE_CONFLICT` for a name already in use, `401` for an anonymous caller and `403` for a + caller without `manage_metadata`. No request or response shape changed. +- 4b45afa: fix(runtime): the `/automation` write doors refuse a packaged flow, as the metadata door does (#20679) + + Clause-②: yes (widening) + + A flow that a code package ships has a locked base (ADR-0126 §2): changing or removing it in place is refused. `PUT /api/v1/meta/flow/:name` already refused it. The two `/automation` definition doors did not: an administrator holding `manage_metadata` could rewrite a packaged flow in the live engine with `PUT /api/v1/automation/:name` or with `POST /api/v1/automation` under its name (a create onto an existing name overwrites it), or remove it with `DELETE /api/v1/automation/:name`. + + All three now answer a packaged flow with the same code and status the metadata door gives (`403` `NOT_OVERRIDABLE`), and with the same sentence wherever the metadata protocol's own package door answers. The refusal comes before the engine is called, so nothing is registered or removed. On `DELETE`, it also comes before the engine's own `DELETE_RESTRICTED` / `409` for a packaged subflow that packaged callers still reach. + + What is not refused: + + - A flow that no code package ships, including a flow created with `POST /api/v1/automation` or authored through the metadata door. It is updated and removed as before. + - `POST /api/v1/automation/:name/clone`, which copies a packaged flow under a new name. This is the supported way to customize one (ADR-0126 §7.1). + - `POST /api/v1/automation/:name/toggle`, the switch that turns a packaged flow on or off (ADR-0126 §7.2). + - A deployment that sets `OS_METADATA_WRITABLE=flow`. It opens both doors, as the refusal message says. + + **The widening.** `@objectstack/metadata-protocol` gains one public method, `ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name, operation })`. It returns the refusal the metadata door would give for writing (`'save'`) or removing (`'delete'`) an existing item because a code package ships it, or `null` when that door would not refuse on this ground. `saveMetaItem` and `deleteMetaItem` call the same code, so the two doors cannot disagree. Their own refusals are unchanged. +- c8111a5: fix(runtime): the clone door's notice names the clone's own off-switch, its status (#20726) + + `POST /api/v1/automation/:name/clone` answers a `notice` saying the clone is armed. It told the admin to switch the clone off through `POST /api/v1/automation/NAME/toggle`. A clone carries no package envelope, so it is a flow authored in the deployment, and that switch refuses it: the switch turns packaged flows on and off only. The notice now names the clone's own switch: send its complete definition with `status: 'obsolete'` to `PUT /api/v1/automation/NAME`. It also says that the toggle switches packaged flows only and refuses the clone, whatever flow the clone was copied from. The response shape is unchanged; only the notice text moves. +- 7c5a311: Runtime refusals, boot errors and warnings no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Strings `@objectstack/runtime` shows to callers, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - The enablement refusal (`POST /actions/_activation/:object/:action`) adds that the switch is not scoped to the caller's organization, which is why `manage_metadata` gates it. + - The doubled post-success navigation warning (`[action-contract]`) says the contract refuses a pair of destinations rather than ranking them, and that "declared `onSuccess` wins" is the console renderer's interim precedence, not a contract. + - The legacy database notice says `dev`, `start` and `migrate` now share one default database file. + - The `BodyRunner` warning for a `log` capability with no logger says the capability writes only to the factory's logger, never to `console`. + - The seed tenancy handoff warning says what a failure leaves behind: seed and API writes on separate autonumber counters until the next boot's migration repairs it. + - The auth forwarder's sanitised-500 log line says the client's message was withheld unconditionally and that this line is where the original error is read. + - The `StandaloneStack` guard for a driver kind with no dispatch arm says falling through to SQLite would hand the caller an engine they never selected. + - The `StandaloneStack` refusals for an unsupported or URL-less database driver, the declarative-endpoint hints, the `cacheTtlSeconds` warning and the two other `BodyRunner` warnings drop their citations; each already said what it refuses and why. + + Text only: no status, error code, field, route, export or control flow moves. A log filter or test that matched the old text (for example a `See #NNNN` suffix) needs the new spelling. +- 76bd58f: fix(automation): which flows are packaged is the package loader's fact, never the flow definition's own, and every flow written through an authoring door is authored in the deployment (#20761) + + Clause-②: yes (widening) + + A flow counts as packaged only when a managed package's loader registered it (ADR-0126 §2, ADR-0131 D6). Before this change, a flow definition written through an authoring door could carry a code package's provenance, and the automation engine then treated that flow as the package's. + + - **The automation engine reads the loader's set.** The ADR-0126 §7.3 subflow guards, the arming gate, the activation switch and the package an activation row names now come from the packages the loader registered. The provenance a flow definition carries is kept for display only. `AutomationEngine` gains `setPackagedFlowSource(reader)` and `packagedFlowOwner(name)`, and the package exports the `PackagedFlowSource` type. `AutomationServicePlugin` attaches the reader for you: it asks the metadata protocol when the engine needs the answer. An engine with no reader attached treats no flow as packaged. + - **One authoring rule for flows.** `ObjectStackProtocolImplementation` gains two methods. `packagedArtifactOwner({ type, name })` names the package whose loader registered an item. `tenantAuthoredWriteRefusal({ type, name, item, packageId? })` is the rule every flow write door asks: the automation create, update and clone doors, and the metadata door's flow write. + - A write to a name a package ships is refused as a locked base. The answer is `packagedBaseRefusal`'s own (`403 NOT_OVERRIDABLE`), so sending a shipped flow's definition back is refused. + - A definition that claims a code package's provenance for a name no package ships is refused with `422 INVALID_METADATA`, and nothing is written. Before, the automation doors kept the claim and the metadata door removed it without saying so. + - A customer flow's definition sent back as it was read is accepted as before. That includes a stored flow bound to one of your own packages, whose read carries that binding. + - `packagedBaseRefusal` also takes an optional `packageId`, the base a save names. + - **The metadata door's other types are unchanged.** Only flows are judged by this rule. Migrating stored rows and duplicating a package are not affected either. + - **A clone is saved.** `POST /automation/:name/clone` now writes its copy as a stored flow of the deployment, through the metadata protocol's save, with no package provenance. The copy reads back on the metadata door and is still there after a restart. Before, it lived only in the running engine and was gone after a restart. If the save fails, the clone is withdrawn and the failure is returned. + + **If a write of yours is now refused with `422 INVALID_METADATA`:** remove the package provenance from the flow definition and send it again. To customize a packaged flow, clone it under a new name. +- 514001a: fix(rest,runtime): the published-snapshot read of a flow name a managed package ships answers the package's flow, as the layered read does (#21002) + + Clause-②: yes (widening) + + `flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. Since the previous half of #21002, the layered read, `GET /api/v1/meta/flow/:name/layers`, reports the package's flow as the effective layer for a name a managed package ships, and a stored flow of that name as a separate layer that does not take effect. The published-snapshot read, `GET /api/v1/meta/:type/:name/published`, and its runtime-dispatcher twin read that same layered answer, but served its stored layer whenever one was present. So for such a name they still answered `200` with the stored flow, not the package's. + + Both published-snapshot doors now serve the layered read's effective layer when that read put the package's flow over a stored flow, which is the package's flow. They ask the metadata protocol's own check for that decision rather than repeating it. In every other case they answer exactly as before: a flow name no managed package ships, and every other metadata type, `object` included, still answer the stored layer when one is present, and an item with no stored layer still falls through to the code/package snapshot. The stored flow is not deleted, rewritten or refused. + + **The widening.** `@objectstack/metadata-protocol` makes one existing method public: `ObjectStackProtocolImplementation.isShippedFlowName(type, name)`. It answers whether `name` is a flow name a managed package ships. It was private to the class, so a door in another package could not ask it any other way. Its answer is unchanged, and the layered read, the by-name read and the flow list keep calling it. +- 70dae53: feat(spec): discovery reports which optional `/auth` route families are mounted, starting with the better-auth admin family (`authFamilies.admin`) (#21046) + + Clause-②: yes + + **New key.** `DiscoverySchema` declares an optional `authFamilies` block, `{ admin: boolean }`. `admin` says whether the better-auth admin family (`{routes.auth}/admin/*`: `list-users`, `set-role`, `update-user`, `ban-user`, …) is mounted on this deployment. On a deployment that does not enable the admin plugin those routes answer a plain `404`, the same as a mistyped path, so a caller checks `authFamilies.admin` before building a URL into the family. `@objectstack/spec/api` also exports the block's schema (`AuthFamiliesSchema`, type `AuthFamilies`) and its reader, `readAuthFamilies(authService)`. + + **Same answer as `/auth/config`.** The value is the auth service's own `getPublicConfig().features.admin`, the object `GET /api/v1/auth/config` serves. Both discovery producers read it through `readAuthFamilies`: `getDiscovery()` in `@objectstack/metadata-protocol` (served by `@objectstack/rest` at `GET /api/v1/discovery`) and `getDiscoveryInfo()` in `@objectstack/runtime` (served at `GET /.well-known/objectstack`). Neither re-derives whether the admin plugin is on, so on one boot the two documents and `/auth/config` agree. On a stock boot `authFamilies.admin` is `false`. With the admin plugin on (`plugins.admin: true`, or SCIM, which forces it on) it is `true`. + + **When the key is absent.** A producer that cannot read the answer emits no `authFamilies`, rather than a guessed `false`. That happens when no `auth` service is registered (then `routes.auth` is absent too), when the registered service has no `getPublicConfig()`, or when that call throws (`/auth/config` answers `500 AUTH_CONFIG_ERROR` in that state). Treat an absent block as "not known to be mounted". + + **What did not change.** No existing key, route or status moved. The unmounted admin routes still answer a plain `404`. +- 7a606a9: fix(rest,runtime): reading `datasource` and `external_catalog` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability each type's own door already requires (#21087) + + Clause-②: no + + - A `GET` or `HEAD` of `/api/v1/meta/datasource` or `/api/v1/meta/external_catalog` (and their plural spellings) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`GET /api/v1/datasources`, `GET /api/v1/datasources/:name`) and the federation read door (`GET /api/v1/datasources/:name/external/tables`) already require for the same data. Every read route under the type is judged alike: the list, the item read and each of its query switches, `/published`, `/layers`, `/history`, `/audit`, `/diff` and `/references`. `/history`, `/audit` and `/diff` still also require an authoring capability, as before. + - A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. The answer is the same whether or not the named item exists, and nothing is read from the metadata store first. + - Holders of `manage_platform_settings` are served exactly as before. Platform administrators hold it through `admin_full_access`. Every other metadata type, and every write route, is unchanged. + - Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by. + - If you read either type with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or read through a caller that already has it. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- 454bbb6: fix(rest,runtime): writing `datasource` metadata through `/api/v1/meta` requires `manage_platform_settings`, the capability the datasource admin door already requires (#21124) + + Clause-②: no + + - A write of a `datasource` definition through `/api/v1/meta` (and its plural spelling) is now admitted only for a caller who holds `manage_platform_settings`. That is the capability the datasource admin door (`POST /api/v1/datasources`, `PATCH` and `DELETE /api/v1/datasources/:name`) already requires for the same create, update and remove. Every write verb is judged alike: the save (`PUT /meta/datasource/:name`, a draft save included), the reset (`DELETE`), `/publish` and `/rollback`. + - A caller without the capability gets `403` with `error.code` `PERMISSION_DENIED`, and a message that names the capability. Nothing is written. The answer is the same whether or not the named item exists. + - The write doors' own authoring admission is unchanged and still applies, so a datasource write needs `manage_platform_settings` and `manage_metadata` both. Platform administrators hold both through `admin_full_access`. Every other metadata type, and every read route, is unchanged. `external_catalog` writes are unchanged: that type's own write door requires `manage_metadata`. + - Both transports answer the same way: `RestServer`, and the runtime dispatcher's `/meta` domain that a host mounting only the `/api/v1/*` catch-all is served by. + - If you write datasource definitions through `/api/v1/meta` with a caller that holds only an authoring capability (`manage_metadata`, `studio.access` or `setup.access`), grant `manage_platform_settings` to that caller, or write through the datasource admin door with a caller that already holds it. +- a186aea: Provenance comments in `@objectstack/runtime` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no route, error code, refusal text, type, export or runtime behaviour + changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [6f1f1c1] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [b531c7b] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [7001918] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [df67985] +- Updated dependencies [42d78b9] +- Updated dependencies [3572916] +- Updated dependencies [fe463b4] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [eb4b17c] +- Updated dependencies [24d521e] +- Updated dependencies [19fc8d6] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [6e3aa75] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [a7d9768] +- Updated dependencies [4bf4e7e] +- Updated dependencies [4d04b6b] +- Updated dependencies [9a4b2bb] +- Updated dependencies [0e9ad74] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [67c1b11] +- Updated dependencies [72f8c38] +- Updated dependencies [cd901d7] +- Updated dependencies [31ed067] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [7184436] +- Updated dependencies [ed54768] +- Updated dependencies [d3f88fa] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [165c1d4] +- Updated dependencies [d7b9817] +- Updated dependencies [f80e2a6] +- Updated dependencies [22e584c] +- Updated dependencies [c8111a5] +- Updated dependencies [7afdc5c] +- Updated dependencies [9ad6544] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [f115b1f] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [76bd58f] +- Updated dependencies [810d42b] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [4d0b9cd] +- Updated dependencies [cf0346e] +- Updated dependencies [8fec76a] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [95fed33] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [b1aee33] +- Updated dependencies [05be352] +- Updated dependencies [250dec8] +- Updated dependencies [d67b942] +- Updated dependencies [f8178ff] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [8f78495] +- Updated dependencies [a3dc817] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [0c5a71b] +- Updated dependencies [75519e1] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [de8cd58] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [25f2e64] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [e161ad3] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [58a77db] +- Updated dependencies [a9d36d5] +- Updated dependencies [b3d7a70] +- Updated dependencies [94990a2] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [c6954d6] +- Updated dependencies [d34aa58] +- Updated dependencies [bafb8c9] +- Updated dependencies [9c8b65a] +- Updated dependencies [665cab3] +- Updated dependencies [665cab3] +- Updated dependencies [45ce12a] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [cfad7de] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [ef96c9e] +- Updated dependencies [e35c40a] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [c6b6889] +- Updated dependencies [ebdb6f2] +- Updated dependencies [55012df] +- Updated dependencies [862f12c] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [be5a83c] +- Updated dependencies [d2bc644] +- Updated dependencies [7923c8e] +- Updated dependencies [95b91cc] +- Updated dependencies [cfa9315] +- Updated dependencies [61455de] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [04b202e] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/rest@17.6.0 + - @objectstack/plugin-security@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/driver-sql@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/metadata@17.6.0 + - @objectstack/metadata-protocol@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/driver-turso@17.6.0 + - @objectstack/driver-memory@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/observability@17.6.0 + - @objectstack/plugin-auth@17.6.0 + - @objectstack/service-datasource@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/driver-sqlite-wasm@17.6.0 + - @objectstack/service-cluster@17.6.0 + - @objectstack/service-i18n@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index ff7e6bd0935..bf00ac6182d 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack Core Runtime & Query Engine", "type": "module", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index 088dfcd161e..3c06dd4dc83 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,34 @@ # @objectstack/sdui-parser +## 17.6.0 + +### Minor Changes + +- a093ce3: The SDUI manifest now marks the html tier's intrinsic tags `tier: 'html'`, and this copy of the parser carries that marker the way the renderer's copy does. + + objectui's copy of `packages/sdui-parser` gained the marker when its registry started declaring the intrinsic HTML tags a `kind:'html'` page may author (`h1`–`h6`, `p`, `a`, `code`, `span`, `table`, the sectioning tags and the rest of the roster; never `div`). This copy still declared only `'public' | 'internal'` and dropped the key. The repo-root `sdui.manifest.json` is serialised through this copy, and `@objectstack/console` ships it in its `dist`, so the published manifest listed those tags with no marker. A reader could not tell them from curated blocks. The port is byte-faithful to objectui at the console pin `dd3f7e1be356`: + + - `RegistryConfigLike.tier` accepts `'html'`, the stamp objectui's `getPublicConfigs()` puts on the roster in its projection. + - `ManifestComponent.tier?: 'html'` is new. `manifestFromConfigs` writes exactly `'html'` or omits the key, so every other entry serialises byte-identically to before. + - `generateBlockList` counts only curated blocks in its title and lists the html tier in a section of its own. + + **What moved in the published manifest:** `"tier": "html"` on 48 entries, and nothing else. It still has the same 107 components in the same order, and no other field on any entry changed. + + **What did not move:** `compile()` and `validateTree()` read the manifest as a whitelist of keys and never read `tier`. So no page's verdict changes. Measured on the three shipped `kind:'html'` pages of `examples/app-showcase`: the full `compile()` result is identical against the old and the new manifest. +- 5bed1f6: `@objectstack/sdui-parser` now reads one base-prop list, ported from objectui's `SDUI_BASE_PROPS` at the console pin `db11afd4967c` (objectui#11008, #11044). Both `validateTree` and the generated JSX types (`generateDts`'s `SduiBaseProps`) are driven by it. + + - On every node, whatever the component declares: `bind`, `hidden`, `visibleWhen`, `hiddenOn`, `testId` are newly accepted. They no longer draw `unknown-prop`, and the generated types accept them as attributes. + - Only on a type whose registration declares no input of that name: `name`, `label`, `description`, `placeholder`, `data`, `ariaLabel` are newly accepted. A type that declares one keeps its declared type check and its declared attribute type; its generated interface `Omit`s that key from `SduiBaseProps`. + + Effect for consumers: `os validate` stops warning `unknown-prop` on those keys, and a `.tsx` page that authors them now type-checks against `generateDts` output where it was a TypeScript error before. Measured on the tracked `sdui.manifest.json` (107 components), no component declares any of the five every-node keys, and every declared where-undeclared key is checked as before, so no diagnostic of error severity is removed for that manifest. The wider type surface is why this is a minor, not a patch. +- b8191f7: fix(sdui-parser)!: the interim `inert-quick-add` diagnostic is retired, mirroring objectui `6f864cf62` (objectui#8285) + + Clause-②: no (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. The save-time parser stops emitting the interim `inert-quick-add` warning for an authored `quickAdd` on ``. The prop walk's own `unknown-prop` warning replaces it, so severity is unchanged and no page that saves today stops saving. The package's barrel no longer exports `checkKanbanQuickAdd`, `INERT_QUICK_ADD`, `QUICK_ADD_HOST_TYPES` or `QUICK_ADD_KEY`. A caller that matched on the `inert-quick-add` code should match `unknown-prop` on the `quickAdd` key instead, and an importer of those four names should delete the import: nothing replaces them. On the authored side, delete the `quickAdd` key: the spec refuses it by name. + ## 17.5.0 ### Minor Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 4314bdc9eff..bdd4e03f9bd 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "main": "dist/index.js", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index cf802cb13cf..c656b532cd7 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,963 @@ # Changelog — @objectstack/service-analytics +## 17.6.0 + +### Minor Changes + +- c8dd8dd: An authored analytics cube's measure `format` and time-dimension `granularities` now take effect on the analytics query doors, the way a compiled dataset's always have (#20282). + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one. The raw-SQL path declines every bucketed query, so such a query now runs on the engine aggregate path, which answers `400 INVALID_FIELD` for every member it cannot evaluate: a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression); and, on a cube whose members resolve through its `joins`, a measure or a `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a `dateRange` window, so grouping by a one-granularity time dimension over a joined object is refused too), a dimension that traverses more than one relationship, and an `avg` or `count_distinct` measure beside any dimension over a joined object. The raw-SQL path answers every one of these, with one group per distinct timestamp; each is now refused, exactly as it already was when the caller stated that granularity by hand. On a host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both), no strategy remains for a bucketed query, so every newly bucketed query, a plain `count` included, now answers "No strategy can handle query" instead of grouping raw timestamps. The remedy: run such a query without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key); on a raw-SQL-only host, add the engine aggregate bridge. It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect. + + Until this change both keys were read on the compiled-dataset path only. One cube shape has three producers — cubes authored with `defineCube()` / `defineStack({ analyticsCubes })`, cubes the dataset compiler mints, and cubes inferred for an ad-hoc query — and only a compiled dataset's cube reached the two readers: + + - **`measures..format`** reached a caller as `fields[].format` only because the dataset door copies it from the DATASET measure. An authored cube has no dataset, so `POST /api/v1/analytics/query` described its measure columns with `name` and `type` alone. Now every measure column a query names carries the `format` its cube measure declares, whichever strategy answered, and a column that declares none carries no `format` key at all. `GET /api/v1/analytics/meta` is unchanged: its projection stays `name`, `type` and `title`, and a client reads `format` off the query result's `fields[]`, as the Data API page already says. The value is relayed verbatim; the vocabulary `fields[].format` documents is a numeral pattern such as `"$0,0.00"` or `"0.0%"`. + - **`dimensions..granularities`** was the default bucket only for a compiled dataset, which the dataset executor filled in before querying. An authored cube's time dimension grouped raw timestamps whatever it declared. Now `query()` and the `generateSql()` dry run read it the same way, through the one rule both paths share: a single-entry list is the dimension's default bucket for a query that groups by it; a granularity the query states always wins, and one the list does not name is not refused (the dataset path compares against no list either); a list of two or more states no default; and a `timeDimensions` entry that carries only a `dateRange` for a dimension the query does not group stays a filter. + + What to expect after upgrading: + + - **A cube measure that declares `format`** now carries it on `POST /api/v1/analytics/query` results. A client that formats amounts from `fields[].format` starts formatting that column. + - **A cube time dimension that declares one granularity** (`granularities: ['month']`) is now bucketed by it when a query groups by it without stating one: one row per month where there was one row per timestamp. Name another granularity in the query's `timeDimensions` to bucket differently. + - **A cube time dimension that declares several, or none**, behaves exactly as before. + - **Compiled datasets** (`POST /api/v1/analytics/dataset/query`) answer exactly as before: the value read off their cube is the one the dataset door already used. + + In `@objectstack/spec`, `MetricSchema.format` and `DimensionSchema.granularities` now carry descriptions that state what the analytics service does with them (the metric's example values move from the names "currency" / "percent" to numeral patterns, the vocabulary the `fields[].format` slot documents), and the liveness ledger rows `analytics_cube.measures.format` and `analytics_cube.dimensions.granularities` move from `dead` to `live`, citing the new readers. +- 03cdb9a: `GET /api/v1/analytics/meta` now publishes an analytics cube's `description`, each measure's and dimension's `description`, and each measure's `format`, when the cube definition declares them (#20282). + + Clause-②: yes (widening) + + - `CubeMeta` (`@objectstack/spec/contracts`) gains an optional `description` on the cube and on each measure and dimension, and an optional `format` on each measure. `AnalyticsMetadataResponseSchema` declares the same members. A definition that declares none of them is published exactly as before. + - `AnalyticsService.getMeta` copies what the definition declares and fills in nothing. A cube compiled from a dataset carries each dataset measure's `format` and no `description`. + - The liveness ledger rows `analytics_cube.description`, `measures.description` and `dimensions.description` move from `dead` to `live`. + + This supersedes one sentence of this release's note on an authored cube's measure `format` and `granularities`: it says `GET /api/v1/analytics/meta` is unchanged and keeps `name`, `type` and `title`. With this change `/meta` also publishes each measure's declared `format`. A client that formats a result column still reads `format` off the query result's `fields[]`. +- 00a92e1: fix(service-analytics)!: a cube or dataset dimension on a structured-JSON field is refused with `INVALID_FIELD` / 400 at the analytics door, before any SQL is built + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the analytics query doors accept as a dimension. A cube dimension, or a dataset dimension, whose column is a declared field of the structured-JSON class (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) is refused before either strategy builds a statement, when it groups the result: a `dimensions` entry, or a `timeDimensions` entry with a `granularity`. The column is judged where it is declared: on the cube's object, or, for a dotted path such as a dataset dimension over `account.hq`, on the object the cube's declared join for that path names. It holds on `POST /api/v1/analytics/query`, on its dry run `POST /api/v1/analytics/sql`, and on `POST /api/v1/analytics/dataset/query`, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What an author sees now.** `400 INVALID_FIELD`, naming the member as the request wrote it (the cube dimension, or the dataset dimension), the column it groups by, the object and the column's declared type, saying the query was not run, and naming the route: group by a field that stores one scalar value, storing the part of the document you group on in a field of its own. The thrown error carries `member`, `param` (`dimensions` or `timeDimensions`), `cube`, `field` and `object`. + + **Why a refusal.** A JSON document is no group key the SQL dialects share. Measured through `POST /api/v1/analytics/query` over three rows with a different document each, on the service `AnalyticsServicePlugin` composes over a real engine: SQLite answered 200 with one group per serialized document, and PostgreSQL 16 answered 500 `DATABASE_ERROR`. A dataset dimension over a joined object's `json` field answered the same two ways through `POST /api/v1/analytics/dataset/query`. The native-SQL strategy compiled the `GROUP BY` itself, so the engine's own refusal of a structured-JSON `groupBy` never saw the query; the engine-aggregate strategy did reach that refusal, but named the engine's `groupBy[0]` position rather than the member the caller wrote. The class is `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, the one the engine's refusal reads. No producer groups by such a field: no cube or dataset dimension in the example apps names one. + + **Who is affected.** A dashboard, report or caller that grouped an analytics query by a structured-JSON field on SQLite and read one group per serialized document as real groups. On PostgreSQL the same query was already a 500. + + **Unchanged.** A dimension on any other type; a `timeDimensions` entry with no `granularity`, which bounds a range and groups nothing; measures (this door judges only the members that group); a dotted dimension path the cube declares no join for, whose object is not a declaration; a member naming a column the object does not have, which keeps its existing `INVALID_FIELD` answer first; and a host that wires no `sourceFieldMeta`, where the column's type cannot be read. +- 793fb83: The analytics seams now run the one shared filter lowering (`lowerFilterCondition`, `@objectstack/spec/data`) that ADR-0053 D-D1, as amended, places at every seam that runs the shared comparand doors: after the doors and after filter-token resolution, so each face compiles one lowered condition — the `$between` split, the whole-day upper bound on a bare `YYYY-MM-DD`, the last supported day, and the NULL-polarity guards. + + Clause-②: yes + + **The read scope (`compileScopedFilterToSql`).** The scope is lowered at the compiler's entry, right after its placeholders resolve. It reads each column's declared type from the `declaredValueShape` option both of its consumers already pass, so the whole-day rule rewrites a declared `datetime` column and nothing else; with no declarations handed in, no column is read as `datetime`. Corrected answers, each now the rows `SqlDriver.find` returns for the same filter: + + - a bare-day `$lte` on a declared `datetime` column kept only the rows before that day and dropped the day itself. Rows at 10:00Z on 07-27, 07-28 and 07-29 under `{ signed_at: { $lte: '2026-07-28' } }` answered 07-27 alone; they now answer 07-27 and 07-28, compiled as `< '2026-07-29'`. This is the NativeSQL statement's read scope and the `/analytics/sql` echo's. + - a bare-day `$between` on a declared `datetime` column answered no row for a one-day range, and now answers that day's rows. + - a `{today}` (or any date-macro) upper bound is widened as the day it resolves to. + + An RLS `using` bound already reached the read scope lowered by the RLS compile seam, and answers as before. A declared `date` column compiles byte-identical to before. + + **The analytics `where` and draft-preview door.** The condition the door admits is lowered before either face reads it. The `where` → tree face (both strategies) reads each member's declared column type through the host's declared-type hook: a bare-day `$lte` on a `datetime` member now reaches the engine as `$lt` the next day, and the `/analytics/sql` echo prints that half-open bound — the statement the engine runs, where it used to print `<=` the named day. Rows are unchanged on every strategy. A nested-relation filter (`{ account: { region: 'NA' } }`) is spelled as the dotted member it has always compiled to before the lowering reads it, so a guard it adds under `$not` names that member. + + The draft preview (`queryDataset` with `previewDrafts`) now evaluates `$null` — the one operator the lowering emits that it did not — so a drafted chart filtered on `{ field: { $null: true } }` is answered instead of refused `INVALID_FILTER` / 400; `$exists` and `$empty` stay refused. Corrected answers: a row with no value now satisfies `$ne`, `$nin` and the negation of an equality even when the comparand is the text `"null"` or `"undefined"`, which this face used to compare as text against the missing value — the answer every data driver gives. Its bare-day bounds answer as before. + + Compiled SQL for `$ne`, `$nin`, `$notContains` and a `$not` operand now carries the lowering's NULL guard around each face's own copy of it: the same rows, a longer statement, until those copies are deleted. +- 8d329f0: fix(service-analytics)!: the nested-relation filter `{ relation: { field: value } }` gets the engine's answer on every analytics face — the related object read as the caller, capped + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics query doors answer for the nested-relation filter form — a plain object with no `$` key beneath a field, `{ owner: { region: 'NA' } }` — on the native-SQL path, and widens it everywhere else. It holds on `POST /api/v1/analytics/query`, on `POST /api/v1/analytics/dataset/query`, and on their dry run `POST /api/v1/analytics/sql`, on every SQL driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What an author sees now.** The same answer `find()` gives for the same filter. The data engine reads the related object with the condition as the caller — that object's row scope and field permissions apply — and matches the relation against the ids it returns: `$in` on a single-valued relation, any member on a multi-valued one. It is the one rule, in the engine; the analytics layer holds no copy of it. + + - A condition on a field of the related object the caller cannot read is refused with `403 PERMISSION_DENIED`, naming the field — never answered. + - A condition matching more than 1,000 related records is refused with `400 INVALID_FILTER`, naming the two-step route — never run over a cut-off list. + - At a measure's own `filter` the form is refused with `400 INVALID_FILTER`, as the engine refuses it at an aggregation's own `filter`: put the condition in the query's `where`. + - `POST /api/v1/analytics/sql` refuses a `where` carrying the form with `400 INVALID_FILTER`: no statement it could print reproduces a read of the related object as the caller. The query itself is answered by `POST /api/v1/analytics/query`. + + **Why.** Measured on the base before the field-level gate (#20917) and the relationship-path admission (#20933) landed, over one fixture with the real security layer (a related field the caller may not read, a related row scope, 1,001 matching related records). The native-SQL strategy flattened the form to a dotted member and joined the related table: through a dataset that `include`d the relationship it answered rows for a condition on a field the caller cannot read, answered a match past the engine's cap, and counted a measure filter carrying the form; without the declared join it named a table that does not exist (500), and a multi-valued relation was refused. The engine-aggregate strategy refused the form as a cross-object filter (400). The engine serves the form since the nested-relation filter landed in `where`. + + **How.** The native-SQL strategy declines a query in which the form appears in the `where`, the dataset's own `filter` or a requested measure's `filter`, so the query runs on the engine-aggregate path, which hands the form to the engine as written. + + **A read scope carrying the form.** Unchanged in outcome: where a read scope is compiled to SQL (`compileScopedFilterToSql`, on the native-SQL path and in both SQL echoes) it is still refused fail-closed with `500 READ_SCOPE_COMPILE_FAILED`, the policy withheld — that compile holds no data engine to read the related object with. Its words now name the route that serves the form. On the engine-aggregate path the scope reaches the engine as written, and the engine serves it as the caller, as before. + + **Who is affected.** A dashboard, dataset or caller that wrote the nested form in an analytics filter on a SQL driver and read the joined answer: a condition on a related field the caller may not read, a match past 1,000 related records, a measure filter carrying the form, or a query that needs the native-SQL strategy for another part (a cross-object measure, a multi-hop dimension), which the engine-aggregate path refuses in its own words. + + **Unchanged.** The dotted cube member (`{ 'owner.region': 'NA' }`), a traversal through the cube's declared join; an empty object beneath a field (`{ owner: {} }`), still refused as a field constraint with no operator; every filter without the form. +- bb2eccf: fix(service-analytics)!: a grouped dimension on a multi-value field and a `count_distinct` measure over a JSON-stored field are refused with `INVALID_FIELD` / 400 at the analytics door, before any SQL is built; a dataset `count_distinct` measure over a field declared `multiple: true` is refused at compile time + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the analytics query doors accept, in two positions, and what the dataset compiler accepts, in one. It holds on `POST /api/v1/analytics/query`, on its dry run `POST /api/v1/analytics/sql` and on `POST /api/v1/analytics/dataset/query`, on every driver and on both strategies. It ships as `minor` under the launch-window convention for accept-set narrowings. + + - A cube or dataset dimension whose column is a **multi-value** field, when it groups the result (a `dimensions` entry, or a `timeDimensions` entry with a `granularity`): an inherently multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `radio`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. The same types without the flag are served. + - A `measures` entry that resolves to a `count_distinct` measure whose column is **JSON-stored**: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently multi option type, or a multi-capable field declared `multiple: true`. An authored cube measure, a suffix-inferred one (`tags_count_distinct`) and a compiled dataset's are judged alike; a `count` measure is not judged. + - A dataset measure that pairs `count_distinct` with a base-object field declared `multiple: true` is refused `400 DATASET_INVALID` when the dataset compiles, at registration and on the request door, beside the type row that already refused `tags`. + + The column is judged where it is declared: on the cube's object, or, for a dotted path, on the object the cube's declared join names. + + **What an author sees now.** `400 INVALID_FIELD`, naming the member as the request wrote it, the column, the object, the declaration (`select with multiple: true`), saying the query was not run, and naming the route. For a multi-value field the route is a record query on the declaring object filtered by one member with `$contains`, one query per member. For a structured-JSON field it is to store the scalar part in a field of its own. The thrown error carries `member`, `param` (`dimensions`, `timeDimensions` or `measures`), `cube`, `field` and `object`. The dataset compile refusal names the measure, the field and its declaration with `multiple: true`. + + **Why a refusal.** The engine's aggregate door already refuses both shapes, and the native-SQL strategy compiled its own statement and never reached it. Measured through this service as `AnalyticsServicePlugin` composes it over a real engine: a dimension on a `tags`, `multiselect` or `multiple: true` select answered 200 with one group per serialized array on SQLite and 500 `DATABASE_ERROR` on PostgreSQL 16; an inferred `count_distinct` over a `json`, `tags` or `multiple: true` select field answered 2, 3 and 2 on SQLite and 500 on PostgreSQL; a dataset `count_distinct` over the `multiple: true` select registered, then answered 2 on SQLite and 500 on PostgreSQL. The engine-aggregate strategy answered 400 for every one of these, under the engine's position (`groupBy[0]`, `aggregations[0].field`) rather than the member the caller wrote. The predicates are `@objectstack/spec/data`'s, the ones the engine's doors read: `isMultiValueField`, and the aggregate × field-type table's `count_distinct` row. + + **Your fix (a host calling `compileDataset` directly).** `DatasetCompileOptions` no longer has `declaredFieldType`. Pass `declaredValueShape` instead: the same read of the field's metadata, answering `{ type, multiple }` (the type, and `multiple === true`) rather than the type alone, or `undefined` when nothing answers. A host that passes neither compiles exactly as before, with no aggregate × field-type refusal at all. `AnalyticsService` and `AnalyticsServicePlugin` wire it themselves from `sourceFieldMeta`. + + **Who is affected.** A dashboard, report or caller that grouped by a multi-value field, or counted a JSON-stored field distinct, through the native-SQL strategy on SQLite and read the serialized arrays or the text-compared count as real answers. On PostgreSQL the same queries were already a 500. A dataset that pairs `count_distinct` with a `multiple: true` field no longer registers. + + **Unchanged.** A dimension or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; a member naming a column the object does not have, which keeps its existing `INVALID_FIELD` answer first; a dotted path the cube declares no join for; a measure whose `sql` is an expression; and a host that wires no `sourceFieldMeta`, where the declaration cannot be read. +- 1571aed: fix(service-analytics)!: every analytics face answers the engine's field-level read refusal, whichever strategy serves the cube: a field the caller may not read is judged before either strategy runs (#20917) + + Clause-②: yes (narrowing) + + + + **BREAKING for analytics queries that read a field the caller may not read: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube.** + + **What changed.** `POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql` + and `POST /api/v1/analytics/dataset/query` now judge every field a query reads + against the caller's field-level read permissions before a strategy is chosen: + dimensions, measures, time dimensions, filter members, order keys, members + joined through a relationship, and a dataset's own and its requested measures' + filters. A member of an authored cube is judged by the field it resolves to, + not by its name in the cube. A field the caller may not read answers + `403 PERMISSION_DENIED`, in the words the engine uses for the same field. The + native-SQL strategy, the one a SQL driver serves first, answered such queries; + the ObjectQL strategy already refused them on `POST /api/v1/analytics/query` + and `POST /api/v1/analytics/dataset/query`, as the data API did, but printed + the statement on `POST /api/v1/analytics/sql`. + + **What is not affected.** A query that reads only fields the caller may read + answers as before. A system context, and a caller with no permission sets, are + unaffected, as on the data API. A host read scope (row-level policy) may still + name fields the caller cannot read. A deployment with no security service applies + no field-level check, as on the data API. A member of an authored cube whose `sql` + is an expression is not attributed to a field. + + **New hook.** `AnalyticsServiceConfig.getReadableFields(object, context)` supplies + the reader. `AnalyticsServicePlugin` wires it to the `security` service's + `getReadableFields`; a host that constructs `AnalyticsService` itself passes its + own, and without one no field-level check applies. + + **If a widget stopped answering for some users,** it reads a field those users + may not read. Grant that field's read permission to the users who need it, or + build the widget on fields they can read. +- 5dbeb7d: fix(service-analytics): on the engine-aggregate path, a `$not`, `$notContains` or null test over a multi-valued lookup now gets the engine's rows instead of `400 INVALID_FILTER` (#20918) + + Clause-②: yes + + **What changed.** `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query`, when served by the engine-aggregate strategy (a query with a granularity, or a host with no raw SQL), used to refuse these filters on a SQL driver when the field is a multi-valued lookup (or any other JSON-stored multi-value field). The engine's `find()` and the native-SQL strategy answered them: + + - `{ $not: { owners: { $contains: 'u1' } } }`, and any `$not` whose operand tests such a field; + - `{ owners: { $notContains: 'u1' } }`; + - `{ owners: { $null: false } }`, `{ owners: { $exists: true } }`, `{ owners: { $null: true } }`, and the same tests in a dataset measure's own `filter`. + + Each now answers the rows the native-SQL strategy answers. Where `engine.find()` serves the same filter, those are its rows too. + + **Why.** The analytics `where` door adds a NULL test beside each leaf of a `$not` operand, so that a row holding no value is still answered by the negation. It adds a NULL alternative to the negative-polarity operators too. The engine-aggregate strategy passed both tests to the engine as `{ $ne: null }` and the bare `{ field: null }`. `driver-sql` refuses both spellings over a JSON column, so the whole filter was refused. They now reach the engine as `{ $null: false }` and `{ $null: true }`. The engine's own filter lowering writes the same tests in those spellings for every driver, and `driver-sql` applies them on a JSON column. + + **Unchanged.** Every filter on a single-valued field gets the same rows as before. The native-SQL strategy and the `POST /api/v1/analytics/sql` echo are unchanged: they compile their own SQL. A read scope is unchanged too. + + **One difference from the engine remains.** `{ owners: { $ne: null } }` and the bare `{ owners: null }` are null tests at the analytics door. Both strategies now answer them, the native strategy as before. `engine.find()` refuses them, because `driver-sql` reads `$ne` and the bare equality as value comparisons on a JSON column. `{ $null: false }` / `{ $null: true }` is the spelling both read the same way. +- 5f6b63a: fix(service-analytics)!: an object an analytics query reads through a relationship path is admitted and row-scoped exactly as a declared join to it is, on both strategies (#20933) + + Clause-②: no (narrowing) + + + + **BREAKING for analytics queries that read a related object through a relationship path the cube does not declare: on a SQL deployment, and on `POST /api/v1/analytics/sql` whichever strategy serves the cube.** + + **What changed.** The analytics door admits and row-scopes one object set + before either strategy runs. It held the cube's base object and the joins the + cube declares (`joins`, or a dataset's `include`). An object reached through a + relationship path the cube does not declare was not in it, although both + strategies read that object: a dotted member of an inferred cube, an authored + member whose `sql` walks a relationship the cube's `joins` does not list, or a + dotted member the query names itself. Every such object is now in the set, so + `POST /api/v1/analytics/query`, `POST /api/v1/analytics/sql` and + `POST /api/v1/analytics/dataset/query` treat it exactly as a declared join: + + - a related object the caller may not read answers `403 PERMISSION_DENIED`, + naming that object, before any statement runs; + - the caller's row scope on the related object is applied, so related rows + outside it are not read. On the native-SQL strategy a base row whose related + record is outside the scope drops out of the answer, as it already did for a + declared join; the ObjectQL strategy still groups such rows as restricted; + - a related-object scope the native-SQL strategy cannot compile routes the + query to the ObjectQL strategy, as it already did for a declared join. + + Each hop of a multi-hop path is judged on its own object, resolved the way the + field-level gate resolves it: the join the cube keys by the path, or else the + relationship name itself. + + **What is not affected.** A query through a related object the caller may read + answers as before, within the caller's row scope. A system context, and a + caller with no permission sets, are unaffected, as on the data API. A + deployment with no security service applies no object-level check, as on the + data API. + + **Refusals that change form.** On the ObjectQL strategy a related object the + caller may not read was already refused on `POST /api/v1/analytics/query` and + `POST /api/v1/analytics/dataset/query`, though `POST /api/v1/analytics/sql` + printed the statement; on those two doors it now answers the analytics door's + refusal rather than the engine's, the same one a declared join gets. A filter, + a time window or a two-hop path through such an object moves from + `400 INVALID_FIELD` to that `403`. A relationship path whose relationship name + is not itself an object name was never served by either strategy; for a caller + the object-level check applies to, it now answers `403 PERMISSION_DENIED` + naming that relationship. + + **If a widget stopped answering for some users,** it reads a related object + those users may not read. Grant read access on that object to the users who + need it, or build the widget on objects they can read. +- 83480c6: fix(service-analytics)!: a field the caller is served masked is refused as a group key, an aggregate input, a filter or a sort key on every analytics face, whichever strategy serves the cube (#20935) + + Clause-②: yes (narrowing) + + + + **BREAKING for analytics queries on a SQL deployment that group, aggregate, filter or sort by a field the caller may only see masked.** + + **What changed.** The field-level gate on `POST /api/v1/analytics/query`, + `POST /api/v1/analytics/sql` and `POST /api/v1/analytics/dataset/query` judged + each member by the caller's readable fields. A field whose `maskingRule` applies + to the caller is readable (its values are served masked), so the gate admitted + it, and the native-SQL strategy then grouped or filtered by the stored value. + The gate now also asks which fields the caller may query on, and refuses a + member naming a masked field with `403 PERMISSION_DENIED`, in the words the + engine uses for the same field. The ObjectQL strategy and the data API already + refused these queries. + + **What is not affected.** A caller who holds the capability that lifts a + field's masking rule queries the field as before. A system context is + unaffected. A query that names no masked field answers as before. + + **New hook.** `AnalyticsServiceConfig.getQueryableFields(object, context)` + supplies the answer. `AnalyticsServicePlugin` wires it to the `security` + service's `getQueryableFields`. When that service predates the method, or + answers "no answer", the plugin treats every field that declares a + `maskingRule` as not queryable, for every caller. A host that + constructs `AnalyticsService` itself with `getReadableFields` and without + `getQueryableFields` is warned once at construction. + + **If a widget stopped answering for some users,** it groups or filters by a + field those users see masked. Give the users who need it the capability the + field's `requiredPermissions` names, or build the widget on fields they can query. +- 9b81314: fix(service-analytics)!: a relationship-path hop the cube declares no join for reads the object its lookup field declares, so an inferred cube's dotted path through a lookup named differently from its target is answered + + Clause-②: yes (narrowing) + + + + **BREAKING**: this widens what the analytics query doors answer for a dotted relationship path the cube declares no join for — an inferred cube's dotted member (`owner.region`), or an authored member whose `sql` walks a relationship its `joins` does not list — and narrows it in one case, named below. It holds on `POST /api/v1/analytics/query` and on its dry run `POST /api/v1/analytics/sql`, on both strategies and every SQL driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What an author sees now.** Each hop of the path reads the object its lookup field declares as its target, the way a join the cube declares already did. With a lookup `owner` that references a person object: + + - the caller may read the person object: `dimensions: ['owner.region']` is answered with the person rows' regions on both strategies, and `where: { 'owner.region': 'NA' }` is answered on the native-SQL strategy with what the nested form `{ owner: { region: 'NA' } }` answers. The engine-aggregate strategy keeps refusing a filter on a related value with its own `400 INVALID_FIELD`, as it does through a declared join; + - the caller may not read the person object: `403 PERMISSION_DENIED` naming the person object, before any statement runs; + - the field-level gate judges `region` on the person object, and the caller's row scope on the person object is applied where the related value is read (the join on the native-SQL strategy, the related read on the engine-aggregate one). + + A lookup to the cube's own object (a self-reference such as `parent`) is read the same way. A lookup named after its target answers exactly as before. + + **Why.** An inferred cube declares no join, so a hop fell back to an object named after the lookup field. For a lookup named differently from its target that is no object: a caller who may read both objects was refused `403` "reading "owner" is not permitted", and a caller the object check passes reached a statement over a table named `owner` (`500`). + + **The narrowing.** A lookup whose name is ALSO the name of another object — a field `account` referencing `crm_account` while an object `account` exists — used to be read from that other object: joined by the ids of the records the field points to, admitted and scoped as that other object. It now reads its declared target. So that path answers from the target's rows, and a caller who may not read the target is refused `403 PERMISSION_DENIED` naming it, where the query used to be answered. + + **Unchanged.** A cube that declares a join for the path keeps reading the join's object. A host that wires no `relationshipResolver` (`AnalyticsServicePlugin` always wires it, from the data engine's object schema), or a relationship field it cannot answer for, keeps reading the object named after the field. A dataset's `include` compiles to declared joins, so a path it declares is unchanged. +- 58a77db: fix(service-analytics)!: the analytics read scope and the native `where` answer `$contains` / `$notContains` on a multi-valued or JSON-stored field by membership, with the one construct `driver-sql` emits, now exported from `@objectstack/core` (#20987) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics doors answer for one class of read. A row policy (the read scope the analytics plugin compiles from the security service, or a host's own `getReadScope`) whose `$contains` or `$notContains` names a field declared multi-valued (`multiple: true` on a multi-capable type, or a multi-option type) or JSON-stored now selects the rows holding the comparand as an ELEMENT of the stored list. It used to select every row whose stored JSON text contained the comparand as a substring, so on SQLite a policy could admit rows outside it, and on PostgreSQL every query under such a policy answered `500` (MySQL was not measured). An analytics count under such a policy now equals what the same caller reads through the data door. On a datasource whose SQL dialect the analytics host cannot name, such a policy now refuses the query (`READ_SCOPE_COMPILE_FAILED` / `500`) instead of falling back to the substring reading. It ships as `minor` under the launch-window convention. + + **The `where`.** `POST /api/v1/analytics/query`, the dataset door and `/analytics/sql` on the native strategy render the same membership test for a `$contains` / `$notContains` in a query's `where` (or a dataset's `runtimeFilter`) on such a field: on PostgreSQL the query answers rows where it answered `500`, and on SQLite the count stops over-counting (`$contains`) and under-counting (`$notContains`). On a datasource whose dialect the host cannot name, the operator on such a field is refused `INVALID_FILTER` / `400`. The ObjectQL strategy already answered membership and is unchanged. + + **Unchanged.** On a scalar text field `$contains` stays the substring test, on every face. `$notContains` keeps its NULL rule: a row with no value satisfies it. A host that wires no field metadata keeps the substring reading, because it cannot tell a JSON column from a text one; the analytics plugin wires it from the data engine. + + **New export.** `@objectstack/core` exports `jsonMembershipPredicate(dialect, emitters, value)` and `jsonMembershipCandidates(value)`, with the `JsonMembershipDialect` and `JsonMembershipEmitters` types: the per-dialect membership construct (#17590) moved from `@objectstack/driver-sql`, where it was module-private, and made placeholder-agnostic. `@objectstack/driver-sql` imports it and emits byte-identical statements and bindings. + + **What to do after upgrading.** Nothing, unless a policy or a dashboard filter relied on the substring reading of a multi-valued or JSON-stored field: such a filter now selects members only, as the data door always did. A host whose analytics `sqlDialect` hook answers nothing for a SQL datasource should answer `'sqlite'`, `'postgres'` or `'mysql'`, or the operator on such a field is refused. +- 39ab294: fix(service-analytics)!: the cube door asks the aggregate × field-type table for every measure, so a configured or suffix-inferred cube measure whose aggregate the table refuses for its column's declared type answers `INVALID_FIELD` / 400 on every driver and both strategies, and a `min` / `max` over a temporal column is described `time` in `fields[]` + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on every driver and on both strategies. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + FROM → TO, for a `measures` entry that resolves to a cube measure over a column of the cube's own object (an authored cube measure, or a suffix-inferred one such as `note_max`): + + - `min` / `max` over a type outside the numeric, temporal and boolean classes (the string family such as `text`, `email` and `url`; `select`, `radio`, `lookup`, `user`; `autonumber`; the JSON-stored, file and `formula` types): FROM, on the native-SQL strategy, `200` with the column's own value (a string such as `"y"`) under `fields[] { type: 'number' }`, on SQLite and PostgreSQL alike; on the ObjectQL strategy the engine's door already answered `400 INVALID_FIELD` after the strategy began. TO `400 INVALID_FIELD` before either strategy reads anything. + - `sum` / `avg` over a type outside the numeric and boolean classes (`sum` also refuses `percent`): FROM `200` with a plausible `0` on SQLite and `500 DATABASE_ERROR` on PostgreSQL (the ObjectQL strategy refused `avg` at the engine and passed `sum` to the driver, which answered the same `0` / `500`). TO `400 INVALID_FIELD`. + - `min` / `max` over a `date`, `datetime` or `time` column: FROM `fields[] { type: 'number' }` beside the instant. TO `fields[] { type: 'time' }`, the `DimensionType` word a temporal dimension column already carries, by the same rule the dataset door applies (`measureResultType`). + + **What an author sees now.** `400 INVALID_FIELD`, naming the measure as the request wrote it, the cube, the column, the object and its declared type, saying the query was not run, and naming the types the aggregate accepts, read off `AGGREGATE_FIELD_TYPE_COMPATIBILITY`. The thrown error carries `member`, `param` (`measures`), `cube`, `field` and `object`. + + **Why a refusal.** The dataset door (`POST /api/v1/analytics/dataset/query`) refuses every one of these pairs at compile by the same table (`DATASET_INVALID`), and the engine's aggregate door refuses most of them on the ObjectQL strategy; the native-SQL strategy compiled its own statement and asked nothing. Measured through the real dispatcher route on SQLite and PostgreSQL 16: a configured cube's `max` over a `text` column answered `"y"` under a column described `number` on the native strategy and `400` on the ObjectQL strategy, and `sum` over the same column answered `0` on SQLite and `500` on PostgreSQL. One cube, one query, an answer chosen by the driver. + + **What to write instead.** Aggregate a field of a type the aggregate accepts. A question that was counting in disguise is `count` (or `count_distinct` over a scalar-stored field). A first or last record by a text value is a sort on a list, not an aggregate. A quantity stored as text belongs in a numeric field of its own, aggregated there. + + **Who is affected.** A dashboard, report or caller that asked `min` / `max` / `sum` / `avg` of such a column through `/analytics/query` on the native-SQL strategy and read the answer as a real one. No example app and no shipped cube authors such a pair. A reader that branched on `fields[].type === 'number'` for a temporal `min` / `max` column now sees `time`. + + **Unchanged.** Every pair the table accepts, a `max` over a `boolean` column included (its column keeps `number`: the rule declines the boolean class); `count` over any column; `count_distinct`, which keeps its own door and words; a measure over a relationship path (`account.name`), which this door does not judge; a column the host's field metadata cannot resolve, or a type outside `FieldType`; a measure whose `sql` is `*`; an expression metric type (`number` / `string` / `boolean`); and a host that wires no `sourceFieldMeta`, where the declaration cannot be read. The dataset door keeps its own `DATASET_INVALID` answer at compile. +- cb45469: fix(service-analytics)!: the analytics native-SQL strategy declines an object an engine middleware is registered for, so the engine serves it and that object's read gates apply; the engine answers which objects carry one (`IObjectQLEngine.hasObjectMiddleware`) (#21080) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics doors serve for one class of query. It ships as `minor` under the launch-window convention for narrowings. + + **What changes.** On a SQL driver, `NativeSQLStrategy` compiled a query to SQL and ran it through the driver's raw-SQL seam, so no engine operation ran and no engine middleware did. It applied the security service's object admission and read filter and nothing else, so the read gates that live in the engine as per-object middlewares did not apply there: a caller admitted to such an object at object level read grouped results and counts over every row, rows about parent records that caller cannot read included. It now declines a query that reads (as its base object, a declared join, or through a relationship path) an object the data engine holds a middleware registered for. The ObjectQL strategy serves it through the engine with the caller's context, so the engine's middlewares run, and the analytics answer for that caller equals the data door's. On the stock composition the objects that move off the native path are `sys_comment`, `sys_activity` and `sys_attachment` (read gates), `sys_approval_request` (the snapshot redaction), and `sys_user_position` and `sys_permission_set` (write-side middlewares, which move as a side effect: a middleware does not declare its operation). No shipped dataset or dashboard reads any of them. + + **What is newly refused.** A query on such an object that the ObjectQL strategy cannot serve is refused with that strategy's existing `400`, where the native strategy used to serve it: for example a dimension reached through a relationship path combined with a measure that cannot be recombined across it (`avg`, `count_distinct`). Correctness wins over the fast path for a gated object. + + **It fails closed.** `AnalyticsServicePlugin` asks the data engine. An engine without `hasObjectMiddleware`, or no engine, cannot say, and the strategy declines then too: every query on such a host is served by the ObjectQL strategy, and the plugin says so once at `warn`. A host that constructs `AnalyticsService` with `executeRawSql` and without the new `hasObjectMiddleware` config member keeps the native path for every object and is told so once at construction. + + **New, additive.** `IObjectQLEngine.hasObjectMiddleware?(objectName): boolean` (`@objectstack/spec`), `ObjectQL.hasObjectMiddleware(objectName)` (`@objectstack/objectql`): whether a `registerMiddleware(fn, { object })` names the object; a global registration (no `object`, or `'*'`) is keyed to none and is not counted. `AnalyticsServiceConfig.hasObjectMiddleware` (`@objectstack/service-analytics`), which the plugin fills from the data engine. + + **Unchanged.** Objects no middleware names keep the native path. The middleware chain, `registerMiddleware` and every gate are unchanged. + + **What to do after upgrading.** Nothing on the stock composition. A host whose `"data"` service is not ObjectQL should implement `hasObjectMiddleware` to keep the native path for ungated objects. A host that builds `AnalyticsService` itself with `executeRawSql` should pass `hasObjectMiddleware` from its engine. +- 336e191: fix(security)!: stored metadata bodies are projected or refused at the audit, analytics, realtime and data-door filter/sort exits too + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what three doors accept or serve for the two stored-metadata tables — the generic data door refuses a filter or sort on the body column, the analytics door refuses it as a dimension / measure / filter / sort member, and the realtime event and the audit/activity copy now carry the body as its type's read projection instead of the stored bytes. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** + + - **Audit / activity copy (`@objectstack/plugin-audit`).** The audit writer copies a `sys_metadata` / `sys_metadata_history` row into `sys_audit_log.new_value` / `old_value` and `sys_activity.metadata`. That copy now projects the body through the shared redactor, so stored credential material is withheld from the second store too. A new `os migrate audit-metadata-bodies` command rewrites the copies already at rest (dry run by default, `--apply` to write, idempotent). + - **Analytics (`@objectstack/service-analytics`).** A query naming the stored body column of these objects as a dimension, measure, filter or sort is refused with `400 INVALID_FIELD`, before any strategy runs — the posture analytics already takes for a member it will not evaluate. + - **Realtime (`@objectstack/objectql`).** A `data.record.*` event projects its `after` / `changes` body through the same redactor, so a subscriber to these objects' events receives no stored credential. + - **Data door filter / sort (`@objectstack/metadata-protocol`).** A filter or sort on the body column is refused with `400 INVALID_FIELD`, the same family and shape as the existing groupBy refusal. + + **What stays answerable.** Every scalar column of these objects — `type`, `name`, `scope`, `state`, timestamps — is still grouped, filtered, sorted, counted and served; only the body column is affected. Every other object is unchanged. +- 3a7b6eb: fix(service-analytics)!: a cube measure whose `sql` is a relationship path (`account.name`) is judged by the aggregate × field-type table, described in `fields[]` and presented on the native-SQL strategy by the declaration on the object the path's last hop reaches, as a measure over the cube's own column already was + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept on the native-SQL strategy, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + The column a relationship path names is located by the one hop resolver both strategies join and read it through: the cube's declared join at that path, else the relationship field's declared `reference`. + + FROM → TO, for a `measures` entry that resolves to a cube measure over a relationship path (an authored cube measure, or a compiled dataset's measure over an `include`d relationship): + + - `min` / `max` / `sum` / `avg` over a related field of a type the table refuses for that aggregate (the string family such as `text`, `select`, `lookup`; the JSON-stored, file and `formula` types; and the rest the table lists): FROM, on the native-SQL strategy, `200` with the related column's own value (a string such as `"zeta"`) under `fields[] { type: 'number' }` on SQLite and PostgreSQL, and for `sum` a plausible `0` on SQLite and `500 DATABASE_ERROR` on PostgreSQL; the ObjectQL strategy refused it as a cross-object measure. TO `400 INVALID_FIELD` on both strategies, before either reads anything — the refusal a base-object column of the same type already got. + - `min` / `max` over a related numeric field on PostgreSQL: FROM the exact-decimal string (`"250.000000000000000000000000000000"`) under `fields[] number`. TO the number `250`. + - `min` / `max` over a related `date`, `datetime` or `time` field: FROM `fields[] { type: 'number' }` beside the instant. TO `fields[] { type: 'time' }`. + + **What an author sees now.** `400 INVALID_FIELD`, naming the measure as the request wrote it, the cube, the path, the related object and the type it declares, saying the query was not run, and naming the types the aggregate accepts. The thrown error carries `member`, `param` (`measures`), `cube`, `field` (the path, `account.name`) and `object` (the related object that declares the column). + + **What to write instead.** Aggregate a related field of a type the aggregate accepts, or `count` the rows. A first or last related record by a text value is a sort on a list, not an aggregate. + + **Who is affected.** A dashboard, report or caller that asked `min` / `max` / `sum` / `avg` of such a related column through the native-SQL strategy and read the answer as a real one. No example app and no shipped cube or dataset authors such a pair. A dataset whose measure aggregates such a related field is now refused when its query runs (`INVALID_FIELD`), where its compile check, which reads the base object's declaration, still lets it through. + + **Unchanged.** Every pair the table accepts; a measure over the cube's own column; `count`, and `count_distinct`, which keeps its own door; a related column the host's field metadata cannot describe; an expression `sql` or `*`; a host that wires no `sourceFieldMeta`; and the ObjectQL strategy's refusal of a related-field measure the table accepts (`max` over a related `number`), a capability limit of the engine aggregate — run that query on a native-SQL driver. +- ce4e205: fix(service-analytics)!: a dataset's own `field` text that is not a column reference is refused at the analytics dataset door, inline or saved + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the analytics dataset door accepts. A dataset whose dimension or measure `field` is not a column reference is now refused with `403 PERMISSION_DENIED` instead of being evaluated — an inline dataset and a saved dataset queried by name alike, since both reach the same door. No shipped dataset carries a non-column `field`. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What changes.** The service compiles a dataset into a cube whose members read as declared, so a dimension or measure whose `field` was a raw expression resolved to a declared cube member and was left to the field-level read gate, which stands down with no security service and on an object its reader answers `undefined` for; in those tiers the expression reached the native statement as written. The dataset's own `field` text is now judged at the dataset door, before compile and before any strategy runs, through the field-read gate's existing judge (`PERMISSION_DENIED` / 403, naming the member and never the expression text), for every caller, admin included, and with or without a security service. There is no new error code and no new admission module. + + **What stays answerable.** Every dataset whose fields are columns or relationship paths is unchanged, inline or saved. A saved dataset whose `field` is an expression is refused the same way as an inline one; refusing such a `field` when it is authored belongs to the dataset schema's own retirement of expression fields, not to this door. The dataset's own filter, the selection's runtime filter and cube-query members are lowered into the compiled query and already judged on the query path, so they are unchanged. +- 4727fcb: fix(service-analytics)!: a grouped dimension or a `count_distinct` measure over a JSON-stored column reached through a relationship path the cube declares no join for is refused with `INVALID_FIELD` / 400 at the analytics door, as the same member over a declared join already was + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + The column of a dotted path is now located by the one hop resolver both strategies join and read it through: the cube's declared join at that path, else the relationship field's declared `reference`, else the relationship's own name for a host that cannot answer. Before, the door read the cube's declared joins alone and stood down on a path the cube declares no join for. This reverses one clause of the earlier entries for this door in the same release, which listed such a path as unchanged. + + **Before and after**, measured on a configured cube over an object whose lookup the cube declares no join for — `owner`, declaring `reference` a person object — and a member over that lookup. An ad-hoc query's inferred cube declares no join at all, and a dotted dimension on it (`owner.prefs`) now gets the same refusal: + + - A `dimensions` entry, or a `timeDimensions` entry with a `granularity`, over a structured-JSON field (`owner.prefs`, `json`) or a multi-value field (`owner.labels`, `tags`; or a field declared `multiple: true`). Before, on the native-SQL strategy: `200` with one group per serialized value on SQLite and `500 DATABASE_ERROR` on PostgreSQL; the ObjectQL strategy answered `400 INVALID_FIELD` from the engine under its own position (`groupBy[1]`), a name the request never wrote. Now: `400 INVALID_FIELD` from this door on both strategies, before either reads anything. + - A `count_distinct` measure over the same columns. Before, on the native-SQL strategy: `200` with a count of serialized values on SQLite and `500` on PostgreSQL; the ObjectQL strategy refused it as a cross-object measure. Now: the same `400 INVALID_FIELD` from this door. + + **What an author sees now.** The refusal the same member over a declared join already got: `400 INVALID_FIELD`, naming the member as the request wrote it, the cube, the path, the object the lookup declares as its target and the column's declared type, saying the query was not run, and naming the route. The thrown error carries `member`, `param` (`dimensions`, `timeDimensions` or `measures`), `cube`, `field` (the path, `owner.prefs`) and `object` (the target object). + + **What to write instead.** Group by, or count distinct, a related field that stores one scalar value. For a multi-value field, run a record query on the target object filtered by one member with `$contains`, one query per member. + + **Who is affected.** A dashboard, report or caller that grouped or counted distinct such a related column through a lookup the cube declares no join for, on SQLite, and read the serialized values as real groups or a real count. On PostgreSQL the same queries were already a 500. No example app and no shipped cube or dataset authors such a member. + + **Unchanged.** Every member over a declared join; a scalar related column (`owner.email`), which is served on both strategies; a related column whose object the host's field metadata does not describe; an expression `sql`; a host that wires no `sourceFieldMeta`; and a dataset dimension over an `include`d relationship, whose join the dataset compiler declares. + +### Patch Changes + +- 92fe081: **BREAKING** — the inner `name` on an analytics cube's measures and dimensions (`MetricSchema.name`, `DimensionSchema.name`) is now refused at parse: nothing ever read it. The record key a member is declared under IS its name — the analytics API publishes it as `.` and a query names it that way. Delete the inner `name`; to rename a member, rename its key. + + Clause-②: no (narrowing) + + `measures` and `dimensions` are records, and the key was always the member's identity: `GET /api/v1/analytics/meta` publishes every member as `${cube.name}.${key}` (in `@objectstack/service-analytics` and in `@objectstack/driver-memory`), and both SQL strategies and the in-memory driver resolve a member by indexing the bag with that key. Measured before removal, with a lit control: zero reads of a member's inner `name` in non-test source, against four reads of the neighbouring `measure.label` / `dimension.label` in the same two `getMeta` projections. So the inner `name` was a REQUIRED second copy of the identity that nothing read — and one that disagreed with its key was silently ignored (this repository's own in-memory driver fixtures authored `totalAmount: { name: 'total_amount', … }` and queried `orders.totalAmount`). + + **Removed rather than enforced** (ADR-0049 enforce-or-remove; the triage verdict on the card, by the maintainer's criterion for declared-but-unenforced families): Cube.dev and LookML key a member by its declared name, with no second inner name that can disagree — and here the record key already delivered it. + + ## FROM → TO + + | you wrote (17.4 and earlier) | write instead | + | --- | --- | + | `measures: { total_amount: { name: 'total_amount', label: 'Total', type: 'sum', sql: 'amount' } }` | `measures: { total_amount: { label: 'Total', type: 'sum', sql: 'amount' } }` | + | `dimensions: { status: { name: 'status', label: 'Status', type: 'string', sql: 'status' } }` | `dimensions: { status: { label: 'Status', type: 'string', sql: 'status' } }` | + | an inner `name` that DIFFERS from its key, e.g. `totalAmount: { name: 'total_amount', … }` | nothing changes at runtime — `orders.totalAmount` was already the name every query used. Delete the inner `name`, or, if `total_amount` is the name you meant, re-key the member and update every query, dashboard and report that names `orders.totalAmount` | + + **The one-line fix:** delete `name` from every metric and dimension; the key it is declared under is its name. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`Metric` / `Dimension` type the key `never`), and the parse — `defineCube()`, `defineStack({ analyticsCubes })`, `PUT /api/v1/meta/analytics_cube/:name` — refuses it at `measures..name` / `dimensions..name` with the prescription: + + > `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. … Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. + + ## The retirement kit + + - **`retiredKey()` tombstones, not a bare deletion** — even though both member shapes are `strictObject`s (the `action.aria` posture). A bare delete would still be loud, but only as a generic unrecognized-key report that cannot carry the prescription; the tombstone types the key `never` for `tsc` and raises the upgrade text at parse. The keys therefore stay in the walked shape: both liveness rows stay `dead` with a `REMOVED` note, and the authorable-surface baseline marks `data/Metric:name` and `data/Dimension:name` `[RETIRED]`. + - **The D2 conversion `cube-member-inner-name-removed`** (protocol 18, retired from the load path) deletes the inner `name` from every metric and dimension of every `analyticsCubes[]` entry. It is owed because the key was REQUIRED, so every stored or built cube carries it. It strips a disagreeing value too — the key already won everywhere, so no query or discovery answer changes — and its notice prints both spellings (`from: name "total_amount"`, `to: (removed; the record key "totalAmount" is the name)`). Its D3 record is the semantic entry `cube-member-inner-name-retired`, which asks the author of a disagreeing name which spelling they meant. + - **The producers stop writing it** (`@objectstack/service-analytics`): the dataset compiler (`compileDataset`), `CubeRegistry.inferFromObject` and the ad-hoc query mint no longer put an inner `name` on the members of the cubes they build. The members are filed under the same keys as before, so `/analytics/meta`, `/analytics/query` and `/analytics/sql` answer exactly as they did. The package README's cube example is corrected. + - **The `measures` / `dimensions` descriptions now say it**: "keyed by metric name: the record key IS the metric's name, published and queried as `.`". + + ## Reach, measured + + - This repository, non-test: the showcase cube (`examples/app-showcase`, 8 members), the published `objectstack-ui` skill's `defineCube` example (6), the `service-analytics` README (3), and the three internal cube mints above — every one wrote the inner `name` EQUAL to its key, and all are corrected here. Test fixtures: about 300 member literals and map-built members across 84 test and fixture files in eight packages, all EQUAL to their key except 21 in `driver-memory`, which disagreed (camelCase key, snake_case inner name) and were already queried by key. + - Out-of-repo authors: NOT MEASURED. + + ## What an operator with a STORED cube sees + + A `sys_metadata` `analytics_cube` row or a built artifact written before this release carries the inner `name` on every member. Nothing breaks at read: the conversion replays on rehydration and at the artifact door and strips it, so the cube is served canonical and parses. `os migrate meta --stored --apply` rewrites the rows. + + +- f1e921a: feat(spec)!: `$empty` joins `FILTER_OPERATORS`, and the view operators `is_empty` / `is_not_empty` lower to it (#20446) + + A stored 「is empty」 / 「is not empty」 — `['field', 'is_empty', …]`, `isempty`, `is_not_empty`, `isnotempty`, in a view rule, a sharing rule or any filter array — now lowers to `{ field: { $empty: true | false } }` instead of `$null`. `$empty` is answered by the field's DECLARED type: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. So an 「is empty」 rule on a text field now also finds `''`, and on a multi-value field also finds `[]`, which the `$null` lowering missed. `is_not_empty` is its exact complement. `$empty` is in `FILTER_OPERATORS` (and `ALL_OPERATORS`) now, and `canonicalAstOperator` folds the empty pair onto `is_empty` / `is_not_empty` rather than onto `is_null` / `is_not_null`. On `@objectstack/driver-memory`, a QueryAST comparison node (`{ type: 'comparison', operator: 'is_empty' }`) is answered by the same declared-type arm. + + **BREAKING**: two things accepted before are refused now, each loudly and with its fix. + + - **A `{ $empty: … }` object written as a field value** (a `where` pasted into an insert or update payload) is refused with `VALIDATION_FAILED` (`invalid_type`, "$empty is a filter operator, not a value"). Before, a text-like field stored it as data. + FROM `update('task', { title: { $empty: true } })` → TO write the value itself (`{ title: '' }`, `{ title: null }`); a filter belongs in `where`. + - **`is_empty` / `is_not_empty` where no face holds the column's declared type** is refused with `INVALID_FILTER` / 400 (`READ_SCOPE_COMPILE_FAILED` / 500 on an analytics read scope). The `$null` lowering answered these. The compositions: + - the built-in `id`, which no object declares. FROM `['id', 'is_empty', true]` → TO `['id', 'is_null', true]` / `is_not_null`; + - a federated (external) object on a driver that does not implement `registerExternalObject` (driver-memory, driver-mongodb). The boot already reports such an object as NOT bound to its remote table, naming it, and its reads answered from a table named after the object. FROM `is_empty` on such an object → TO bind it on a driver that implements federation (driver-sql and its heirs, driver-turso); + - an `AnalyticsService` constructed without `sourceFieldMeta`. FROM such a host → TO pass `sourceFieldMeta` (the package README shows it), or filter with `is_null` / `is_not_null`; + - a multi-value column on a SQL dialect `driver-sql` does not model (a knex client other than SQLite, PostgreSQL or MySQL). FROM `['tags', 'is_empty', true]` there → TO `['tags', 'is_null', true]` / `is_not_null`. + + Stored sharing rules and views that use 「is empty」 are not rewritten; they are re-read under the new meaning. Production rules that use 「is empty」 on a text or multi-value field were not measured; each finds more rows (the `''` / `[]` ones) from this release. + + Clause-②: yes (narrowing) + + +- b785c3b: fix: `sum` / `avg` answer the same double on every face the platform owns, added with one compensated fold that `@objectstack/core` now exports as `compensatedSum` (#20544) + + Clause-②: yes + + **New export.** `@objectstack/core` exports `compensatedSum(nums)`: the sum of + `nums`, added in order with Kahan-Babuska-Neumaier compensation, which is the + summation SQLite (3.43 and later) uses for its own `sum` and `avg`. It moved + here from `@objectstack/objectql`'s rows path (`in-memory-aggregation.ts`), + which now imports it instead of keeping a private copy. + + **What changed.** Three folds still added a group's values naively, and now call + the same function: + + - `@objectstack/driver-memory`'s `aggregate()` and `find()` with aggregations, + the path `engine.aggregate` takes on an in-memory datasource; + - `@objectstack/driver-memory`'s analytics face (`MemoryAnalyticsService`), + whose `sum` / `avg` measures are now a `$group` `$accumulator` in place of + mingo's `$sum` / `$avg`; + - `@objectstack/service-analytics`' draft preview. + + Over a `number` column holding `0.1`, `0.2` and `0.3`, each of them answered + `0.6000000000000001` / `0.20000000000000004`. They now answer `0.6` / + `0.19999999999999998`, as SQLite and the engine's rows path do. Over + `1e16, 1, -1e16` they answered `0` and now answer `1`. On driver-memory, + `engine.aggregate` gave two answers depending on its path: `having { s: { $eq: + 0.6 } }` kept the group on the rows path and dropped it on the native path. It + now keeps it on both. + + **What did not move.** Two addends, integers whose running total stays within + 2^53, and a non-finite total give the same answer as before. Which values count + as addends did not change either: booleans as 1 / 0, and nulls and non-numeric + strings left out, as each face already had it. `count`, `min` and `max` are + untouched. The analytics face's pipeline dump (`result.sql`) now renders the + accumulator's functions by name, so a `sum` measure and an `avg` measure still + dump differently. + + **Residual.** PostgreSQL and MySQL add their doubles natively without + compensation, and the platform does not wrap that arithmetic. So over three or + more fractions their native path can still differ from these faces in the last + place. An exact `$eq` on a fractional sum compares doubles; compare with a range. +- d282087: Provenance comments in `service-analytics` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- a6866da: fix(core): a date or time in the years 0001..0099 is read as written, not as 1900..1999, wherever a UTC instant is built from year / month / day / time parts + + `Date.UTC(year, …)` and `new Date(year, …)` read a year from 0 to 99 as 1900 + year. Core built its instants from parts that way, so every day of the years 0001..0099 (inside the supported range 0001..9999) landed in the 1900s at the sites below, with no error. + + - `@objectstack/core`: **new export** `wallClockToUtcMs(parts)`, the epoch milliseconds of a `WallClockParts` read as UTC. It is `Date.UTC` without the two-digit-year remap: `month` is 1-12, omitted time components are 0, and every component rolls over past its end as `Date.UTC` rolls it (`month: 13` is next January, `day: 0` the previous month's last day, `hour: 24` the next midnight). A `NaN` component gives `NaN`. Every site below now builds through it: + - `zonedWallClockToUtcMs` and `zonedDateStartToUtcMs`, the wall clock and the zone-offset read. The offset read also takes the zone's era, so a wall clock early on 0001-01-01 in a zone west of UTC, whose offset probe lands in year 0, reads right. + - `bucketKeyToCalendarRange` (`0050` spans 0050-01-01..0051-01-01, not 1950..1951; `0050-01-01` as a `day` key is no longer `null`) and `bucketDateKey`'s ISO week (0050-01-01 is in week 52 of 0049, not of 1949). + - The date macros: `{1976_years_ago}` resolves to `0050-09-30`, not `1950-09-30`. A macro that lands in 0001..0999 is now spelled with a four-digit year, as the `date` storage form spells it (`0055-06-15`, not `55-06-15`, which names no day). + - `@objectstack/service-analytics`: the preview evaluator's `week` key and the `compareTo` bucket alignment build their days through `wallClockToUtcMs`. + - `@objectstack/trigger-schedule`: a time-relative window's day bounds build through `wallClockToUtcMs`. + + What an author sees: `POST /api/v1/data/:object/import` stores the `datetime` cell `0050-01-01 10:00` as `0050-01-01T10:00:00.000Z`, and in `Asia/Shanghai` as `0050-01-01T01:54:17.000Z` (the zone's local mean time for that year). Before, it stored `1950-01-01T10:00:00.000Z` and `1950-01-01T02:00:00.000Z` and reported the row `ok`. Measured through the import route and read back through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16; the `2026-07-15 10:00` control is stored the same before and after. Every year from 0100 on builds exactly as before. +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- 10c36cc: fix(service-analytics): every dataset answer names its base object as `object` (#20644) + + Clause-②: no + + **What was wrong.** `queryDataset` set `object`, the dataset's base object, only + while it built drill-through metadata, which it builds only when a drillable + dimension is selected and at least one row came back. A dimension-less (KPI) + answer, a zero-row answer and the degraded answer for an unavailable backing + object carried no `object`, and neither did a draft preview (`previewDrafts`), + grouped or not. `POST /api/v1/analytics/dataset/query` relays the service answer + as it is, so a consumer that refreshes on that object's record changes had + nothing to subscribe to for those answers. + + **What changed.** Every `queryDataset` answer carries `object`, the dataset's + `object` by machine name, whatever dimensions are selected and whether or not + rows came back, as `AnalyticsResult.object` in `@objectstack/spec` declares. A + grouped answer is unchanged: `object` sits beside the same drill-through keys + as before. A cube `query` answer still carries no `object`. +- 856321f: A date-bucket key spells its year with four digits at every granularity, as the SQL drivers' bucket expressions do, so the in-memory and pushed-down paths key a day in 0001..0999 alike and a drill-down from such a key finds its range. + + A `date` value names a year from 0001 to 9999, so these keys are reachable through a `date` field and through a stored `datetime` row. For 0050-06-15, `strftime('%Y-%m')` on SQLite and `to_char(…, 'YYYY-MM')` on PostgreSQL answer `0050-06`, while `bucketDateKey` answered `50-06`: the same `groupBy` keyed the same rows differently depending on which path ran it. + + - **`@objectstack/core` `bucketDateKey`** pads the year to four digits: `0050`, `0050-Q2`, `0050-06`, `0050-06-15`, and the ISO week key `0050-W24` (early January 0050 is `0049-W52`, its ISO week-year). The engine's in-memory `groupBy` and the memory cube face delegate to it, so both now answer the drivers' key. A year from 1000 to 9999 is spelled as before. + - **`@objectstack/core` `bucketKeyToCalendarRange`** reads exactly what `bucketDateKey` writes. Its week arm checked a key against the unpadded label, so a padded key such as `0050-W01` answered `null`; it now answers `{ start: '0050-01-03', end: '0050-01-10' }`. An unpadded key (`50-06`, `49-W52`) is not a bucket key and still answers `null`. + - **`@objectstack/service-analytics`** mints the `compareTo` alignment key through `bucketDateKey` instead of spelling it locally, so a comparison row in 0001..0999 merges onto its bucket (`0050-06`) instead of being appended under `50-06`. +- 975b248: fix(objectql,spec)!: a `groupBy` on a multi-value field and a `count_distinct` on a JSON-stored field are refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver, and the aggregate × field-type table stops accepting `count_distinct` over the JSON-stored types + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, in two positions, on every driver and for every caller that reaches the engine (the REST query door, a flow or hook, and the analytics strategy that lowers a cube query onto `engine.aggregate`). Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + - A `groupBy` entry that names a **multi-value** field: an inherently-multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. Both entry spellings are judged, the field name and the `{ field }` object. + - A `count_distinct` aggregation over a **JSON-stored** field: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently-multi option type, or a multi-capable field declared `multiple: true`. + + **BREAKING** (`@objectstack/spec`): `AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct` no longer lists the ten JSON-stored types (the structured-JSON seven and `multiselect`, `checkboxes`, `tags`), so `isAggregateCompatibleWithFieldType('count_distinct', type)` answers `false` for them. Every reader of the table refuses those pairs now: the dataset-measure lint rule (`measure-aggregate-field-type-refused`, run by `os validate` and at a runtime dataset save), the analytics dataset compile leg (`400 DATASET_INVALID`), and the engine door above. The `count` row is unchanged. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, `groupBy[0].field`, or `aggregations[0].field`), the field and its declaration, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps. For a multi-value field the route is to filter by one member: `where` with `$contains` on the field, one query per member. For a structured-JSON field it is to store the part you count in a field of its own, or to count rows with `count`. The thrown error carries `field`, `fields`, `object` and `param` (`groupBy` or `aggregations`). + + **Why a refusal.** Every SQL driver stores these values in a JSON column, and the drivers share no meaning for one as a group key or a distinct key. Measured through `POST /api/v1/data/:object/query` over three rows: grouping by any of the eight multi-value declarations answered one group per array on the in-memory driver, one group per serialized array on SQLite, and 500 `DATABASE_ERROR` on PostgreSQL 16. `count_distinct` over any structured-JSON or multi-value field answered 3 on the in-memory driver (equal values counted apart), 2 on SQLite (serialized text compared), and 500 on PostgreSQL (no equality operator for `json`). No example app and no published stack groups by a multi-value field or counts one distinct, so no meaning is defined for either here. + + **What to write instead.** A dataset measure or a query that counted a JSON-stored field distinct: use `count` over it, or store the scalar part you meant to count in a field of its own and `count_distinct` that field. A grouping by a multi-value field: filter by each member with `$contains` and count. + + **Who is affected.** A caller that grouped by a multi-value field, or counted a JSON-stored field distinct, on the in-memory driver or on SQLite and read the answer as a real one; on PostgreSQL both were already a 500. A dataset whose measure pairs `count_distinct` with a JSON-stored field is refused by the lint rule and the compile leg. + + **Unchanged.** (Two shapes the structured-JSON `groupBy` entry of this same release lists as unchanged are narrowed here: a `multiple: true` select as a group key, and `count_distinct` over a structured-JSON field. This entry is the later word on both.) A `groupBy` or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; the `having`, filter and sort positions; and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. + + `@objectstack/lint`: the dataset-measure refusal's hint no longer says `count_distinct` accepts every type. + + `@objectstack/service-analytics`: the dataset compile leg's refusal of a `count_distinct` measure over a JSON-stored field says why it diverges (the drivers compare the values for equality three ways) and prescribes `count`, or a scalar field for the part being counted; its other refusals no longer say `count_distinct` accepts every type. +- 525b813: A dataset's date dimension reads the bucket key `@objectstack/core`'s `bucketDateKey` writes, with its year in four digits, and a draft preview keys a row the way the same dataset does once published. + + - **Dimension labels (`queryDataset`).** A date dimension's grouped key is labelled as written. The year key `0050` was labelled `1970` (read as epoch seconds, because the year check admitted only 1000..9999), and a month or day key lost its padding (`0050-06` became `50-06`, `0050-06-15` became `50-06-15`). A raw date value is relabelled with the year in four digits too. A year from 1000 to 9999 is labelled as before. + - **Draft preview (`queryDataset` with `previewDrafts`).** Drafted seed rows are keyed by `bucketDateKey` itself, the key the published path's grouping writes. For 0050-06-15 the preview answered `50`, `50-Q2`, `50-06` and `50-06-15`; it now answers `0050`, `0050-Q2`, `0050-06` and `0050-06-15`. A `week` bucket is now the ISO week label (`2026-W25`), no longer the Monday's date (`2026-06-15`), so a weekly `compareTo` in the preview merges each comparison row onto its week, as the published path does. An epoch-milliseconds value is bucketed by its instant (it was the empty bucket), and a `Date` in 0001..0999 by its own year (a `Date` in 0050 keyed `1950`). +- d1633f3: fix: the analytics native-SQL path answers a measure its response declares `number` as a number on every dialect, presented by the one rule `driver-sql`'s `aggregate()` applies, which `@objectstack/core` now exports as `AGGREGATE_ANSWER_KIND` and `presentAsNumber` (#20889) + + Clause-②: yes (widening) + + **New exports.** `@objectstack/core` exports two names, moved here unchanged + from `@objectstack/driver-sql`, which now imports them instead of keeping them + private: + + - `AGGREGATE_ANSWER_KIND`: what each declared aggregate function answers. + `count`, `count_distinct`, `sum` and `avg` answer `'number'`; `min` and `max` + answer `'column'`, a value of the aggregated column. + - `presentAsNumber(value)`: the `'number'` presentation. A string `Number()` + reads as a number becomes that number. Any other value is returned as given: + a number, `null`, a boolean, empty or blank text, or text that reads as NaN. + + **What changed.** On PostgreSQL, `POST /api/v1/analytics/query` and + `POST /api/v1/analytics/dataset/query` answered through `NativeSQLStrategy` + returned count, count_distinct, sum, avg, and min / max over a numeric column + as strings, such as `count: "2"` and + `sum: "500.000000000000000000000000000000"`, while `fields[]` declared + `number`. A dataset's `row_count` did the same, and a measure-scoped count + mixed `"1"` with the number `0` in one column. SQLite answered numbers. The + strategy now presents each measure column by its declared aggregate function, + through the same table and presenter as `SqlDriver.aggregate()`. `min` / `max` + are presented only when their column is declared numeric, so `max` over a text + column, every dimension, and expression measures keep the value the database + returned. + + **Precision.** The answer is one JS number, the policy `driver-sql`'s + `aggregate()` already applies. A total that needs more digits than a double + holds, such as `9007199254740993`, answers the nearest double + (`9007199254740992`), which is also what SQLite and the engine path answer. + + **What did not move.** `@objectstack/driver-sql`'s behaviour is unchanged: its + `aggregate()` reads the same table, and its read presenter calls the same + function. The answers on SQLite are byte-identical. The arithmetic of the + analytics native statement did not change either. On PostgreSQL its `sum` and + `avg` still add exact decimals, so `0.1 + 0.2` answers `0.3` where the engine + path answers `0.30000000000000004`. +- 5d5e679: feat(spec)!: an analytics cube member's `sql` is a column reference — a SQL expression there is refused at parse, and a derived value is declared on an ADR-0021 dataset (#20943) + + Clause-②: yes (narrowing) + + **BREAKING** — shipped as `minor` under the launch-window convention + (`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by + this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, + never by the level). + + `MetricSchema.sql` and `DimensionSchema.sql` — the `sql` of every member in an + analytics cube's `measures` and `dimensions` — admit a column reference only: a + field of the cube's object (`amount`), a relationship path of bare identifiers + ending in one (`account.amount`, `account.owner.region`), or `'*'` for a count. + Any other value — a `CASE WHEN …`, an aggregate or a ratio of aggregates, a quoted + or `$`-prefixed spelling, an empty string — is refused at parse with a + prescription. This is ADR-0021's "zero raw SQL / zero raw expressions" carried + from the dataset layer to the cube members it compiles to (maintainer ruling D on + the card): an expression names no single field, so no platform check can judge + which fields it reads, and the two analytics strategies never agreed on it — the + raw-SQL path ran it verbatim and the ObjectQL path refused it. The rule is a + `pattern` in the published JSON Schema too, so a document validated against + `json-schema/**` is judged as the parse judges it. + + ## FROM → TO + + A derived value moves to an ADR-0021 dataset over the same object. A conditional + count or sum is a dataset measure with its own structured `filter`; a ratio, sum, + difference or product of measures is `derived: { op, of: [...] }` over measures + named in the same dataset. + + ``` + FROM defineCube({ name: 'delivery', sql: 'task', measures: { + done_rate: { label: 'Done Rate (%)', type: 'number', + sql: "SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)" }, + } }) + -> parsed; the expression ran verbatim on one strategy and was refused on the other + TO -> ZodError at measures.done_rate.sql (invalid_format): `measures..sql` is a + column reference: a field of the cube's object (`amount`), a relationship path ending + in one (`account.amount`), or `'*'` for a count. A SQL expression there was retired … + + defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', + dimensions: [/* … */], + measures: [ + { name: 'task_count', aggregate: 'count' }, + { name: 'done_count', aggregate: 'count', filter: { status: 'done' } }, + { name: 'done_rate', derived: { op: 'ratio', of: ['done_count', 'task_count'] }, format: '0.0%' }, + ] }) + ``` + + **Mind the scale.** A `derived` ratio is a 0–1 fraction. An expression that + multiplied by 100 returned percentage points; pair the ratio with a `%` numeral + pattern (the server marks a ratio column's percent scale as a fraction) and + re-check any consumer that read the old number raw. + + **A dimension that bucketed a column with a CASE expression** has no expression + form in the cube layer or the dataset layer: group by the column itself, or keep + the bucket as a field of the object and name that field. + + **The one-line fix:** parse each cube; every refusal at `…sql` is one member to + move — replace it with the column it aggregates, or move the derived value to a + dataset measure as above, and point the dashboards, reports and queries that named + `.` at the dataset measure. + + **What an author who still writes it sees.** `CubeSchema`, `defineCube()`, + `defineStack({ analyticsCubes })` (`STACK_SCHEMA_INVALID` / 422) and the + `analytics_cube` write door refuse the member at its `sql` path with the + prescription. `tsc` does not: the key's type is still `string`. + + ## The retirement kit + + - **Schema.** `MetricSchema.sql` / `DimensionSchema.sql` carry the pattern and + their prescriptions (`data/analytics.zod.ts`). A column reference parses + byte-identically to before. The retired metric `filters` guidance and the + analytics query's `filters` guidance no longer offer "fold the condition into + the metric's own `sql` expression" as a live channel; the `metric-filters-removed` + conversion summary and its D3 entry and step-18 rationale fragment say the same. + - **ADR-0087.** The D3 entry `cube-member-sql-expression-retired`, with its + step-18 rationale fragment. No D2 conversion — an expression has no mechanical + rewrite into a dataset — and no `RETIRED_KEYS_BY_MAJOR` row: no key left the + shape, so the authorable-surface, api-surface and JSON-schema manifest + ratchets are unchanged. + - **Liveness.** The `analytics_cube` ledger rows `measures.sql` and + `dimensions.sql` stay `live`, re-verified, with the narrowing recorded. + - **Docs.** The `data/analytics` reference page is regenerated. + - **Example.** The showcase cube's `done_rate` expression member moves to the + `showcase_task_metrics` dataset as `done_count` (a count filtered on + `status: 'done'`) and `done_rate` (`ratio` over `done_count` and `task_count`, + format `0.0%`). + - **`@objectstack/service-analytics`** (README only): its query-body section no + longer tells a reader to fold a per-metric condition into the metric's own + `sql` expression. The runtime is unchanged: its expression branches remain for + a cube that reaches the service without meeting the parse, and their deletion + is a separate change. + + ## Reach, measured + + - This repository: one authored expression member (the showcase `done_rate`), + moved here. Test fixtures in `@objectstack/service-analytics` that build + expression members WITHOUT the parse keep exercising the runtime's expression + branches, unchanged. + - Out-of-repo authored cubes: NOT MEASURED. + + +- ae1e950: fix(service-analytics): the analytics field-level read gate refuses a cube member whose `sql` names no field, instead of letting the query run (#20965) + + Clause-②: no + + **What changed.** Where the analytics field-level read gate judges a cube's + object (a security service is registered and gives a field answer for that + object), a query that names a cube member whose `sql` is neither a column + reference (a field of the cube's object, or a relationship path ending in one) + nor `'*'` is now refused `403 PERMISSION_DENIED` on + `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql`, before either + strategy runs. Whatever + the caller may read, the member is refused. That covers an expression member + of a cube that reached the service without the spec's parse (the cube + registry never parses: `analyticsCubes` and `AnalyticsServicePlugin({ cubes })` + arrive as written), a declared member with no `sql` string, and a member the + query names itself that is not a column reference. The gate used to stand down + on such a member, because it names no field, and the native-SQL strategy then + compiled it into its statement as written: a read of fields no permission + verdict was reached for. The refusal names the member and the object, and + never the member's `sql`. + + **What is not affected.** A member that is a column reference is judged by the + field it resolves to, as before. A `count` over `'*'` names no field and is + served. A deployment with no security service, and an object the security + service gives no field answer for, apply no field-level check, as before. The + spec's parse already refuses an expression member, so a cube that parses is + unaffected. + + **If a widget stopped answering,** its cube carries an expression member from + before the parse refused one. Re-author the member as a column reference, or + declare the derived value on an ADR-0021 dataset: a conditional count or sum + is a dataset measure with its own `filter`, and a ratio of measures is + `derived: { op: 'ratio', of: [...] }`. +- 097ef80: fix: the analytics native-SQL path aggregates with the engine's own aggregate policies, so one query answers one number whichever strategy serves it: `sum` / `avg` accumulate in double, a PostgreSQL boolean aggregand is cast, and an all-NULL `sum` answers `0`. The operand policies move from `@objectstack/driver-sql` to `@objectstack/core` (#21042) + + Clause-②: yes (widening) + + **New exports.** `@objectstack/core` exports the aggregate operand policies, moved here from `@objectstack/driver-sql`, where they were module-private. The driver now imports them and emits byte-identical statements. + + - `AGGREGATE_ACCUMULATION`: what each declared aggregate function accumulates in on PostgreSQL and MySQL. `avg` accumulates in double; `sum` accumulates in double over a fractional column; the counts, `min` and `max` take the column as stored. + - `aggregandColumnClass(shape)`: the one column-class predicate those policies read, over a column's declared `{ type, multiple }`. It answers `'fractional'`, `'integral'`, `'boolean'`, or `undefined` for every other column, a multi-valued one included. The type `AggregandColumnClass` names the three classes. + - `POSTGRES_BOOLEAN_AGGREGAND_CAST`: the functions whose boolean aggregand is cast to `int` on PostgreSQL. These are `sum`, `avg`, `min` and `max`; the two counts are never cast. + - `doubleAccumulationOperand(operand, dialect)`: the column's text, parsed as a double, spelled for `'postgres'` or `'mysql'`. + - `aggregandOperandSql(func, columnClass, dialect, operand)`: the operand an aggregate wraps, with the cast inside the double operand. The type `AggregandSqlDialect` names its dialects (`'sqlite'`, `'postgres'`, `'mysql'`, `'unknown'`). + + **What changed.** `POST /api/v1/analytics/query` and `POST /api/v1/analytics/dataset/query` served by `NativeSQLStrategy` (the default on a SQL driver) skipped three policies `SqlDriver.aggregate()` applies. So the ObjectQL strategy and `engine.aggregate` answered differently for the same query. Measured on SQLite and PostgreSQL 16.13: + + - On PostgreSQL, `sum` / `avg` over an exact-decimal column, and `avg` over an integer one, added exact decimals. For example, `0.1 + 0.2` answered `0.3` and `11 / 9` answered `1.222222222222222`, where the engine answers `0.30000000000000004` and `1.2222222222222223`. The native statement now accumulates in double, as the driver does. + - On PostgreSQL, `sum` / `avg` / `min` / `max` over a boolean field answered `500` (`function sum(boolean) does not exist`). The native statement now casts the boolean aggregand to `int`, as the driver does, and answers the numbers the engine answers. + - On every dialect, a group whose aggregand is NULL in every row, and a measure-scoped `sum` that admits no row, answered `sum` `null` at the cube door. The strategy now folds a `null` answer to `emptyGroupValueFor` (`@objectstack/spec`) for every measure, so that `sum` answers `0`. `avg`, `min` and `max` over nothing stay `null`. The dataset door already answered `0`. + + This is no narrowing: each answer moves to the value the platform already declared for the same query. + + **What did not move.** `@objectstack/driver-sql`'s statements and answers are unchanged: a move-proof test compiles each aggregate function over each column class on SQLite, PostgreSQL and MySQL, and the statements equal the ones captured before the move. SQLite's native statement is unchanged, because neither operand policy applies there. A host that relays no field declarations to the analytics service, or names no SQL dialect, gets today's native arithmetic. +- 0b12b9e: Clause-②: no + + Security: refuse a caller-named analytics member that is neither a declared cube member nor a column reference at the query door, in every tier — including a deployment with no security service and an object the field-level read gate does not judge — so caller-supplied member text can no longer reach a native statement unjudged. The refusal reuses the existing field-read gate's envelope (`PERMISSION_DENIED` / 403); no new error code, and the declared-cube paths are unchanged. +- 2791138: fix(service-analytics): on the native-SQL strategy, a base-table column is qualified with its table whenever the statement joins a related object, not only when the cube declares a join + + Clause-②: no + + A cube that declares no join still joins a lookup's declared `reference` when a query names a relationship path through it (`owner.email`). The native-SQL strategy qualified base-table columns only for a cube that declares a join, so it wrote them bare beside the joined object. When that object declares a column of the same name, the database refused the statement as ambiguous, and `POST /api/v1/analytics/query` answered `500 DATABASE_ERROR` on SQLite and on PostgreSQL. The ObjectQL strategy answered `200` for the same query. + + **Before and after**, measured on a configured cube over a `deal` object that declares no join, whose lookup `owner` points at a person object that also declares `note`, `amount`, `closed_on` and `id`: + + - Dimensions `note` and `owner.email`, with or without a `where` on `note` and an `order` by it: `500` → `200`, one group per (deal note, owner email). + - A `sum` over `amount`, a `timeDimensions` window on `closed_on`, or a `where` on `id`, each grouped by `owner.email`: `500` → `200`. + - An ad-hoc query over the object, whose inferred cube never declares a join: the same. + + The strategy now reads what the statement actually joins, from the one relationship-path resolver, and qualifies every base column in the select list, the grouping, the filters, the measures and the time windows. A statement that joins nothing keeps bare columns. That is now also true on a cube that declares a join when the query uses none of it: the statement it shows on `POST /api/v1/analytics/sql` reads `note` where it read `"deal"."note"`, and the answer is the same. + + **Unchanged.** The ObjectQL strategy; every query on a cube that declares the join it uses; every statement that joins nothing on a cube that declares no join; the refusals. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 0b5fde6c7f6..68d9716a9e0 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Analytics Service for ObjectStack — implements IAnalyticsService with multi-driver strategy pattern (NativeSQL, ObjectQL, InMemory)", "type": "module", diff --git a/packages/services/service-automation/CHANGELOG.md b/packages/services/service-automation/CHANGELOG.md index baab3192049..e45e48f56b3 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,536 @@ # @objectstack/service-automation +## 17.6.0 + +### Minor Changes + +- 5363e2d: feat(spec,service-automation): a flow run's result carries the flow's authored label as `flowLabel` (#20318) + + Clause-②: yes (widening) + + **The widening.** `AutomationResult` (`@objectstack/spec/contracts`) gains one + optional member, `flowLabel?: string`, and `TriggerFlowResponseSchema` + (`@objectstack/spec/api`) mirrors it on `data`. The automation engine sets it to + the flow definition's `label`, copied verbatim, the same way it copies + `successMessage` and `errorMessage`. Nothing is removed or renamed, and no + existing member changes meaning. + + **Why.** A flow runner names the flow it is running, in its header and in its + completion toast, and translates that name against the `flows..label` + translation key, falling back to the authored label. The runner only held the + flow's API name, so there was no authored label to fall back to. The console's + reader of the translation key is a separate change. + + **Which results carry it.** + + - **Set** on every result of an evaluation of a registered flow: `status: 'paused'` + (first attempt, retry attempt, a resume that pauses again), a terminal success + (including the two skip exits), `'failed'` (including an exhausted retry budget), + `'stranded'`, `'refused'`, and a resumed parent whose delegated child failed. + - **Absent** on every refusal that carries a `code` (the run never dispatched, or a + resume never continued it) and when the flow is not registered. + - **Subflow chains** answer with the label of the run the caller addressed, which + is the parent. The child that supplied the screen does not lend its label. + - **Never the API name.** `FlowSchema` requires `label`, so the value is always + what the author wrote, an empty string included. + + **At the wire.** Both runner doors relay the result verbatim on a `200`, so + `data.flowLabel` arrives on `POST /api/v1/automation/:name/trigger` (a paused or + finished launch) and on `POST /api/v1/automation/:name/runs/:runId/resume` (a + further pause or the completion). A `400 FLOW_FAILED` answer is unchanged: its + `error.details` keep their fixed set (`errorMessage`, `summary` and, on resume, + the stranded verdict), with no `flowLabel`. + + **For a consumer.** A client that parses the trigger response with + `TriggerFlowResponseSchema` now keeps `data.flowLabel`, where an undeclared key + would have been stripped. A caller that deep-compares a whole `AutomationResult` + from `execute()` or `resume()` sees one more key on the results listed above. +- 36d043b: fix(service-automation)!: disabling a packaged subflow completes once its packaged callers are switched off and hold no parked run, and the refusal names the parked runs and the cancel door (#20678) + + Clause-②: yes (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. `toggleFlow(name, enabled)` on the automation service, and so `POST /api/v1/automation/:name/toggle`, now accepts some disables it used to refuse (the widening) and refuses one corner of enables it used to accept (the narrowing). + + **The disable direction (the widening).** Disabling a packaged flow that a packaged flow calls as a subflow (the `flowName` of a `subflow` or `map` node) was refused while any such caller existed, even one already switched off. So the refusal's own remedy, "disable the calling flow first", could never complete. A caller now guards the disable only while it can still reach the subflow node: + + - **An enabled caller** guards, as before. The refusal names it, and the step is to disable it first. + - **A disabled caller** (switched off in the activation ledger, or disabled by its definition's `status`) guards only while it holds a **parked run**: a run paused at a wait, an approval, a screen, or at a `map` node between items. Switching a flow off stops its new runs only, and a parked run still resumes into its subflow node. The refusal names each parked run id and the operator cancel door, `POST /api/v1/automation/:name/runs/:runId/cancel` (ADR-0044). Cancel those runs, or let them finish, and the disable completes. + - **A disabled caller with no parked run** no longer guards, so "disable the caller, then the callee" completes. + + Parked runs are read from both the in-process runs and the durable suspended-run store, including runs a previous process parked. If the durable store cannot be listed at that moment, the disable fails with the store's own error and nothing is written; it is never read as "no parked run". The refusal keeps `DELETE_RESTRICTED` / `409` and its `subflowCallers` list, which now names exactly the callers that guard. + + **The enable direction (the narrowing).** A subflow in a cycle of ledger-switched-off flows with the flow being enabled was skipped whole, even when its definition's `status` also disabled it. So the enable was accepted onto a subflow that stays disabled, and the publish remedy was never named. Such a subflow is now named, with both reasons and both steps (publish it with status `active`, then enable it). The cycle exemption covers the activation switch only, because no enable order changes a status. +- 679f95e: fix(service-automation)!: re-enabling a packaged flow is refused while a packaged subflow it calls is disabled, and the refusal names a remedy that subflow's state admits (#20678) + + Clause-②: no (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. `toggleFlow(name, true)` on the automation service, and so `POST /api/v1/automation/:name/toggle` with `{"enabled": true}`, now refuses an enable it used to accept. + + **What changed.** A packaged flow switched off in the activation ledger could be switched back on while a packaged flow it calls (the `flowName` of a `subflow` node, or of a `map` node) was itself disabled. The enable was accepted, and every run of the flow then failed at that node on the child's `FLOW_DISABLED` refusal. That enable is now refused with `RESOURCE_CONFLICT` / `409`, before anything is written: the ledger row still reads off, the trigger stays unbound, and runs are still refused. The message names each disabled subflow and what holds it off, and the remedy follows from that: + + - **Switched off in the activation ledger**: enable that subflow first, then this flow. + - **Disabled by its own definition's `status`** (`obsolete` or `invalid`): the activation switch never changes a status, so enabling the subflow through it would change nothing. Publish the subflow with status `active` (for a package that is read-only in this environment, that takes a package version that ships it active), then enable this flow. + + **Not refused:** + + - Enabling a flow that is already enabled. Nothing is re-armed. + - A subflow the customer authored. A flow the customer authored is not this switch's to enable at all: the activation switch switches packaged flows only, and it refuses a customer-authored flow for that reason before this guard is asked (see the entry "the toggle door refuses a flow no package ships, naming its status switch"). + - A subflow in a cycle of switched-off flows with the flow being enabled, including a flow that calls itself. Each flow in such a cycle would refuse the others, so no order could complete. A subflow in such a cycle whose definition's `status` also disables it is still named, with its publish remedy: no enable order changes a status. + + The disable direction of the same guard is described in its own entry, "disabling a packaged subflow completes once its packaged callers are switched off and hold no parked run". +- 0d9349f: fix(service-automation)!: a packaged flow is never armed onto a disabled packaged subflow on any door, removing a packaged subflow its packaged callers can still reach is refused, and the disable refusal reads a caller's parked runs completely (#20725) + + Clause-②: yes (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. ADR-0126 §7.3 refuses one state: a packaged flow armed while a packaged flow it calls (the `flowName` of a `subflow` or `map` node) is disabled, so that the caller fails at that node on the child's refusal. The activation switch (`POST /api/v1/automation/:name/toggle`) already refused it in both directions. It now holds on every other door too. + + **Arming declines it; registration is never refused.** Creating, republishing, upgrading or hot-reloading a flow, a cold boot, and a trigger registering at `kernel:ready` all arm through one gate. That gate now leaves a packaged flow **unarmed** while a packaged subflow it calls is disabled, by the activation ledger or by its definition's `status` (`obsolete` / `invalid`). The flow still registers, so a boot or an upgrade never fails on an installation's choice: + + - `GET /api/v1/automation/_status` reports it `enabled: true, bound: false`, with a `reason` naming each disabled subflow and the step that re-arms it. The `kernel:bootstrapped` binding audit prints the same reason. + - The engine logs one warning naming each subflow and its remedy (enable it, or publish it with status `active`). + - It is armed the moment its subflow is enabled, through the switch or by republishing the subflow `active`. A flow held back by two subflows is armed when both are on. + - An armed caller is unarmed when its subflow is republished `obsolete` or `invalid`, or when the activation ledger read at boot switches that subflow off. + - In a cycle of flows switched off in the activation ledger, enabling the first one is still accepted, but it stays unarmed until the flow it calls is enabled; then both are armed. + - A flow the customer authored, or a subflow the customer authored, is not judged. + + **Removing a packaged subflow is refused while a packaged caller can still reach it.** `AutomationEngine.unregisterFlow`, and so `DELETE /api/v1/automation/:name`, now refuses with `DELETE_RESTRICTED` / `409` and `subflowCallers`, the same refusal the switch gives on disable. Nothing is removed. The message names each caller and the steps: + + - **An enabled caller** guards: disable it first. + - **A switched-off caller** guards while the subflow itself is still enabled. The removal door cannot read whether that caller still holds a parked run, so it names the switch instead: switch the subflow off (that refusal names each parked run to cancel), then remove it. + - Once the subflow is switched off and every caller is switched off, the removal completes. + + A flow an artifact reload no longer ships (a package upgrade or uninstall, a Studio package publish, a dev reload) is removed through the new `AutomationEngine.withdrawFlow`, which does not take this refusal: the package decided the removal, and the next cold boot would not register the flow either. + + **The disable refusal reads a caller's parked runs completely.** Disabling a packaged subflow under a switched-off caller that holds a parked run was decided from the durable store's deployment-wide list of paused runs, which reads at most 1000 rows. A caller whose run lay beyond them read as holding none, and the disable was accepted. The refusal now asks the store for the named callers' runs, all of them: `SuspendedRunStore` gains an optional `listByFlow(flowNames)` (complete by contract, or an error), and `ObjectStoreSuspendedRunStore.listByFlow` reads the `(flow_name, status)` index page by page to its end. A store without it is read through `list()`. The deployment-wide listing (`listSuspendedRunsDurable`) is unchanged. +- c8111a5: fix(service-automation)!: the toggle door refuses a flow no package ships, naming its status switch (#20726) + + Clause-②: no (narrowing) + + + + **BREAKING**: shipped as `minor` under the launch-window convention. `toggleFlow(name, enabled)` on the automation service, and so `POST /api/v1/automation/:name/toggle` and `client.automation.toggle`, now switches packaged flows only: a flow a code package ships. + + **What was wrong.** The switch records an installation's choice in the packaged-metadata activation ledger (`sys_metadata_activation`, ADR-0126 §7.2), whose rows name the package that ships the flow. For a flow authored in the deployment it wrote a row anyway: + + - A flow with no package id, such as one created through `POST /api/v1/automation` or the clone door, was refused with 400 `VALIDATION_FAILED` "Package is required", naming a field the caller never sent. + - A flow carrying the runtime-row package sentinel or an app package id was accepted, and a ledger row was written for it. That gave it a second off-switch beside its own `status`. + - With no ledger attached, the flip was accepted in process only. + + **What changed.** A flow without package provenance is now refused with `RESOURCE_CONFLICT` / `409`, in both directions and with or without a ledger. The refusal comes before anything is written or changed. The message says the switch turns packaged flows on and off. It names the flow's own switch: its definition's `status`, published with the complete definition through `PUT /api/v1/automation/:name`. `obsolete` switches it off and `active` arms it. The switch never rewrites a definition itself. Packaged flows toggle exactly as before. + + **Migration.** To switch a customer-authored flow off, stop sending `POST /api/v1/automation/NAME/toggle` with `{"enabled": false}`. Instead, send `PUT /api/v1/automation/NAME` with the flow's complete definition and `status: 'obsolete'`, and `status: 'active'` to arm it again. In the SDK, `client.automation.toggle(name, false)` becomes `client.automation.update(name, { ...definition, status: 'obsolete' })`. + + **A customer flow that a ledger row already holds off.** If this switch turned a customer flow off before this release, its ledger row still holds the flow off after the upgrade, and no `status` clears that row. The refusal says so and names the step that completes: clone the flow under a new name through `POST /api/v1/automation/NAME/clone`, which arms the copy, then remove the old one. +- 76bd58f: fix(automation): which flows are packaged is the package loader's fact, never the flow definition's own, and every flow written through an authoring door is authored in the deployment (#20761) + + Clause-②: yes (widening) + + A flow counts as packaged only when a managed package's loader registered it (ADR-0126 §2, ADR-0131 D6). Before this change, a flow definition written through an authoring door could carry a code package's provenance, and the automation engine then treated that flow as the package's. + + - **The automation engine reads the loader's set.** The ADR-0126 §7.3 subflow guards, the arming gate, the activation switch and the package an activation row names now come from the packages the loader registered. The provenance a flow definition carries is kept for display only. `AutomationEngine` gains `setPackagedFlowSource(reader)` and `packagedFlowOwner(name)`, and the package exports the `PackagedFlowSource` type. `AutomationServicePlugin` attaches the reader for you: it asks the metadata protocol when the engine needs the answer. An engine with no reader attached treats no flow as packaged. + - **One authoring rule for flows.** `ObjectStackProtocolImplementation` gains two methods. `packagedArtifactOwner({ type, name })` names the package whose loader registered an item. `tenantAuthoredWriteRefusal({ type, name, item, packageId? })` is the rule every flow write door asks: the automation create, update and clone doors, and the metadata door's flow write. + - A write to a name a package ships is refused as a locked base. The answer is `packagedBaseRefusal`'s own (`403 NOT_OVERRIDABLE`), so sending a shipped flow's definition back is refused. + - A definition that claims a code package's provenance for a name no package ships is refused with `422 INVALID_METADATA`, and nothing is written. Before, the automation doors kept the claim and the metadata door removed it without saying so. + - A customer flow's definition sent back as it was read is accepted as before. That includes a stored flow bound to one of your own packages, whose read carries that binding. + - `packagedBaseRefusal` also takes an optional `packageId`, the base a save names. + - **The metadata door's other types are unchanged.** Only flows are judged by this rule. Migrating stored rows and duplicating a package are not affected either. + - **A clone is saved.** `POST /automation/:name/clone` now writes its copy as a stored flow of the deployment, through the metadata protocol's save, with no package provenance. The copy reads back on the metadata door and is still there after a restart. Before, it lived only in the running engine and was gone after a restart. If the save fails, the clone is withdrawn and the failure is returned. + + **If a write of yours is now refused with `422 INVALID_METADATA`:** remove the package provenance from the flow definition and send it again. To customize a packaged flow, clone it under a new name. +- 27bf358: fix(automation): boot-time flow precedence takes which same-named flow is the packaged one from the package loader's set, not from the flow definitions' own provenance (#20864) + + Clause-②: yes (widening) + + When several flow definitions share one name at startup, the automation plugin arms one of them and shadows the rest. Which contender counts as the packaged one is now the answer of the set of flows a managed package's loader registered. That is the same answer the ADR-0126 §7.3 subflow guards, the arming gate and the activation switch read since #20761. The package provenance a flow definition carries is kept for display only. + + - `resolveFlowPrecedence(items, logger?, packagedFlowOwner?)` and `describeFlowContender(item, packagedFlowOwner?)` take the reader as a new optional last argument, typed `PackagedFlowSource` (the reader `AutomationEngine.setPackagedFlowSource` takes). `AutomationServicePlugin` passes the engine's own `packagedFlowOwner` for you. + - A definition that claims a package's provenance for a name no package loaded ranks as a flow of the deployment. The shadowing record (`getShadowedFlows()`, and the startup warnings) no longer names that package as its source. + - With no reader, no contender is packaged. That is the engine's own answer when no reader is attached. + - Two contenders that both rank as the deployment's keep the order they were listed in. The package id orders packaged contenders only, as before. + - A startup whose registry the package loader and the stored-flow hydration filled arms the same flows as before: those entries already agree with the loader's set. + + **If you call `resolveFlowPrecedence` or `describeFlowContender` yourself:** pass the loader's-set reader as the last argument, for example `(name) => engine.packagedFlowOwner(name)`. Without it no contender ranks as packaged. +- 8368f1c: feat(service-automation): the connector sync executor pulls a `mapping`'s `connectorSource` and writes it through the import runner (#20919) + + `AutomationServicePlugin.pullConnectorSource({ mapping, context })` (and the + exported `pullConnectorSource(deps, opts)`) reads the mapping through the + protocol's `getMetaItem`, resolves `connectorSource.connector` to a declared + (`connectors[]`) `rest` or `openapi` instance, makes ONE call to its read action, + takes the array at `recordsPath` (default `body`), projects it through the + mapping's `fieldMapping` and writes it with `@objectstack/core`'s `runImport` — + the import door's coercion, `mode` / `upsertKey` matching and per-row verdicts, + with the door's defaults for every knob `connectorSource` does not declare. + + - **Watermark, read from the target.** For `connectorSource.watermark`, the + starting point sent as `query[watermark.param]` is the highest value already + stored in the target field a `fieldMapping` entry copies `watermark.field` onto + (transform `none`). Nothing else stores it. + - **One response per pull.** The connector's paging is not followed. + - **Loud refusals,** each a `ConnectorPullError` (`code`, `status`, `reason`) + raised before anything is written: a plugin-registered or unregistered + connector, a degraded instance, a provider other than `rest` / `openapi`, an + undeclared action, `update` / `upsert` with an empty `upsertKey`, a + `javascript` transform, an unmapped `watermark.field`, an `ok: false` answer, + a non-array at `recordsPath` and a non-object record. + - **Nothing schedules a pull** — a `job` will drive it; the caller supplies the + execution context. + + The plugin now records the provider of each declared connector instance it + materializes, which the executor reads. + +### Patch Changes + +- 5a23096: Warnings, refusals and hints that cited a tracker number now say what was decided + + Clause-②: no + + Several runtime strings an author or operator reads sent the reader to an issue-tracker number for + the reason behind them. Each now states that reason in the sentence itself: + + - `@objectstack/objectql`: the two data-event warnings. A write that names no single record publishes + no per-record event rather than one with an empty `recordId`; a predicate (`multi: true`) write + publishes its own `data.records.*` event carrying the affected-row count and nothing else, so a + driver result that is not a count publishes no bulk event either. + - `@objectstack/service-automation`: the warning for a pausing node type that never declares + `resumeAuthority`, the generic-route resume refusal (its log line and its error text), and the + refusal of a suspension from a type that declares `supportsPause: false`. An undeclared + `resumeAuthority` resolves to `'service'` (fail-closed), so the generic resume route refuses those + pauses; guessing `'any'` is how a raw resume once walked past an approval decision no service had + recorded. + - `@objectstack/runtime`: the endpoint step's `NOT_IMPLEMENTED` message and its two hints (the + composed runtime always threads the policy context and the execution wiring, because execution is + reachable only past the policy chain), and the endpoint mapping refusals (the publish gate rejects + the same shapes, so a declaration that reaches the runtime check was stored without passing it). + + Text only: no error code, field name, status or behaviour changes. +- c96beb2: fix(security): a flow's inbound-hook secret is withheld from every served flow definition, and a read → edit → republish round trip keeps it (#20552) + + Clause-②: yes (widening) + + **The widening.** `@objectstack/metadata-protocol` gains one public method, + `ObjectStackProtocolImplementation.getMetaItemsForExecution`. It returns the stored + bodies without the serving decorations, for in-process binders that execute what they + read. No door that answers a caller may use it. + + An `api` flow's start node carries its inbound hook's HMAC secret (`config.secret`, + ADR-0041), the one credential that hook has. Every read that served the flow's + definition served the secret with it, to any authenticated caller. It is now + withheld from what is SERVED, and from nothing the engine executes. + + **What no longer carries the secret.** The automation domain's flow-definition read + and the flow its `POST` / `PUT` / clone doors answer with; and on the metadata plane, + every read of a flow — item, list, layered, draft preview, published snapshot, diff, + audit — plus a package export. The key is removed, not masked: a mask is a non-blank + string the registration gate would accept as the secret. + + **Consequence for a reader.** A client that read the secret back from a definition + no longer can. A package exported from one deployment and imported into another + arrives without it, and its `api` flows are refused at registration until a secret is + set on the start node again. + + **The round trip.** A save that carries the projected form — no `secret` where the + read served none — keeps the stored secret, on both authoring surfaces (the metadata + plane's save door and the automation domain's `PUT` / `POST`). Only an explicit value + replaces it, so a rotation is written as before. The start node is matched by its + `id`, not its position, so an edit that reorders `nodes` keeps it too. The first save of an item that has no stored row yet, such as a code-authored flow or datasource, takes the value from the code layer the read served, for every type with a registered redactor. + + - `@objectstack/service-automation` owns the projection (`redactFlowCredentials`) and + registers it as the `flow` read-path redactor at plugin `init`. The engine keeps + binding with the stored secret: it now reads flows from the protocol's execution + face, because the served face no longer holds the credential its hooks verify + against. + - `@objectstack/metadata-protocol` gains `getMetaItemsForExecution` on + `ObjectStackProtocolImplementation` — the same flattened list `getMetaItems` + serves, without the serving decorations (no `_diagnostics`, no credential + redaction). It is for in-process engines that execute what they read; every door + that answers a caller keeps serving `getMetaItems`. `carryForwardRedactedValues` + now follows a redacted path through an array by the element's `id`. + - `@objectstack/metadata`'s `getPublished` applies the type's registered read-path + redactor to the body it returns. It was the one metadata read exit that served a + stored body without it. +- 3f45b6c: fix(security): every credential a flow definition holds is withheld from what is served, at every depth, and an edit round trip keeps each one where it belongs (#20590) + + Clause-②: no + + **What is now withheld.** Beside an `api` flow's inbound-hook secret (the start node's + `config.secret`), every served flow definition now also withholds an `http` node's + outbound signing secret (`config.signingSecret`), and both are withheld wherever the + node sits: at the top level, or inside a `loop` body, a `parallel` branch, or a + `try_catch` region. The engine still executes the stored values. + + **Removing a signing secret.** A definition saved back without the key keeps the + stored secret, because an absent key is what every read serves. To remove it, save + the key as the empty string (`signingSecret: ''`): the durable callout is then + delivered unsigned, and the empty value is served as written, so the next round trip + keeps it cleared. + + **Changing a node's kind.** An edit that keeps a node's `id` and changes its kind no + longer carries that node's stored credential onto it. The credential belonged to the + old kind; a start node that needs a secret asks for one again at registration. + + **Moving a node.** A node moved into or out of a `loop` body, a `parallel` branch or a + `try_catch` region keeps its stored credential across the round trip, as long as its + `id` and kind are unchanged and it is the only node, at the top level or in any region, + that carries that `id`. An edge or a config value with the same `id` does not count. + + **The `/meta` list read on a dispatcher host.** When the metadata protocol's list read + fails, the list answers that failure (`503 SERVICE_UNAVAILABLE` for a store outage, or + the protocol's own refusal) instead of serving the metadata service's stored list, + which applies no credential redaction. A host whose protocol has no list verb keeps + its metadata-service fallback. +- 14f80e2: The `http` node's designer form says an outbound credential never goes in the node's `url` or `headers`, and where it goes instead (#20590) + + Clause-②: no + + The `http` action descriptor's `configSchema` is what the flow designer's palette and property form read (`GET /api/v1/automation/actions`). It described `url` as "Target URL" and `headers` as "Request headers", with no word about credentials. Both values are stored in the flow definition, and a flow definition is served to every member who can read flows; of the node's config, only `signingSecret` is withheld. The two field descriptions now say this, and name where the credential goes instead: + + - a credential in a header: a `connector_action` on a declarative connector whose `auth.credentialRef` names the secret; + - a key in the query string: a declarative `rest` connector with `api-key` auth, whose `paramName` names the parameter and whose `auth.credentialRef` names the secret; + - a webhook whose path is the secret: a token-authenticated connector instead, such as the `slack` connector with its bot token. No `credentialRef` variant carries a secret in the url path. + + Description text only. No config key is added or removed, nothing more is withheld on read, and there is nothing to migrate. +- 73155fe: Provenance comments in `service-automation` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the record in this repository that decided + the matter (an ADR where one exists, otherwise the commit in this repository's + history), and say in their own words what was decided. Comments only: no type, + schema, export, log or refusal text, or runtime behaviour changes. +- 89801cd: **A flow `http` node's `signingSecret` now signs the request on every arm, with one scheme, and a secret that does not resolve refuses the node instead of letting the request leave unsigned.** + + `signingSecret` is declared as "HMAC-SHA256 secret → X-Objectstack-Signature", with no arm named. Only the durable arm honoured it, because only the messaging outbox signed. The default inline request, and a `durable: true` node on a host with no messaging HTTP outbox (which degrades to that inline request), were sent without the header while the run reported success. + + - `@objectstack/core`: **new exports** `signHttpBody(body, secret)` and `HTTP_SIGNATURE_HEADER`, the outbound HTTP signature scheme: `X-Objectstack-Signature: sha256=`, where a request with no body is signed over the empty string. They were `@objectstack/service-messaging`'s own, and they moved here so a sender with no outbox can sign with the same code. + - `@objectstack/service-messaging`: `signHttpBody` and `HTTP_SIGNATURE_HEADER` are still exported under the same names. They are now re-exports of the `@objectstack/core` bindings, not a second implementation. Delivery rows and the headers the outbox sends are unchanged. + - `@objectstack/service-automation`: the `http` node's inline request carries `X-Objectstack-Signature` whenever `signingSecret` is set. It is computed over the exact body the node sends (its JSON serialization of `config.body`, or the empty string when there is none), so a receiver that verifies with `signHttpBody` over the bytes it received accepts it on every arm. + - A non-empty `signingSecret` that renders to nothing at run time now fails the node with a guard refusal naming `config.signingSecret`, and nothing is sent. This covers a `{token}` with no value in the run, or one that renders the empty string. The refusal is on every arm, including the outbox arm, which used to enqueue such a delivery unsigned. A fault edge does not route it. The fix is to give the run the value the template reads. + - An authored `signingSecret: ''` still sends unsigned on purpose, on every arm. + + Clause-②: yes (widening) — two new exports on `@objectstack/core`'s root. Nothing is removed or renamed on any package. The one newly refused case is a node whose authored secret did not resolve, which the published contract already said signs. +- defc7f7: fix(service-automation): a flow switched off in the activation ledger stays unbound after a restart, and a trigger-fired refusal no longer logs an ERROR claiming a run-history row (#20677) + + Clause-②: no + + **What was wrong.** A packaged flow switched off through the ADR-0126 activation + ledger (`POST /api/v1/automation/:name/toggle` with `enabled: false`) came back + `bound: true` after every cold restart. Its runs were still refused, so the switch + itself held, but its trigger was armed again. At boot the automation service pulls + the flows and applies the ledger, which leaves a switched-off flow unbound. The + trigger plugins register later, at `kernel:ready`, and registering a trigger armed + every matching flow without asking whether it may run. So `GET + /api/v1/automation/_status` reported the flow `enabled: false, bound: true`. Each + matching event also logged `ERROR Trigger-fired run of flow '…' failed`, saying the + failure "is recorded in the flow's run history", while no run row was written. + + **What changed.** + + - The engine checks whether a flow may run in one place: at the step that arms a + trigger. Every arming path goes through it: flow registration (boot pull, + publish, hot reload), trigger registration, and the enable toggle. A flow that + either disable dimension switches off (the activation ledger, or an `obsolete` / + `invalid` status) is never armed, whenever its trigger registers. + - Re-enabling a flow arms it on its trigger as before. Re-enabling the ledger bit + of a flow whose `status` is still `obsolete` or `invalid` no longer arms it, since + every run it fired would be refused. + - A trigger-fired run refused because the flow is disabled (for example, an event + already in flight when the flow was switched off) is logged at `info`, saying + nothing ran and no run-history row records it. It is no longer an `ERROR`. + - The `ERROR` line for any other trigger-fired failure says the failure is + recorded in the run history only for a run that dispatched and failed. A run + refused before it dispatched gets the same line without that claim. + + **What is not affected.** The runtime refusal (`FLOW_DISABLED`) and its message are + unchanged. The enabled flows beside a disabled one arm exactly as before. No export, + option, route or response shape changes. +- b280546: A caller-supplied value for a `formula` field is stripped on every engine write path, in every context, and reported through `droppedFields` / `onFieldsDropped` under a new `reason`, `computed`; and `ObjectQL.validate` runs the write's own field doors, so a dry run built on it predicts what the write will do (#20805). + + Clause-②: yes (narrowing) + + + + **BREAKING**: `ObjectQL.validate` now refuses a row that carries a key the object does not declare, exactly as `insert` and `update` refuse it: the call throws `INVALID_FIELD` / 400 naming the field. It used to answer `valid: true` for that row while the write it previews refused it, so the protocol's `validateData` and the import dry run built on it said "ok" for rows the commit then failed. It ships as `minor` under the launch-window convention; the widening half is the new `reason` arm. + + **A formula value is stripped, never refused.** A `formula` field is computed on every read and no driver has a column for it, so a full read returns the key and a record written back carries it: a form save, a flow's `update_record`, a `GET` then `PUT`. The key used to reach the driver, and the driver decided. On SQL drivers the whole write failed with the driver's own error (`SqliteError` "table … has no column named …", with no `status` and no `field`; the REST door relabelled it `400 INVALID_FIELD` "Unknown field" for a field the object declares). On the in-memory driver the value was stored as a shadow column nothing reads. Now the engine takes the value out before the defaults, the hooks and the other strips, completes the write, and reports one `{ reason: 'computed' }` event per call. That holds on `insert` (one row or a batch), `insertMany`, and `update` by id and by predicate, on every driver, and in every context, `isSystem` included: there is no column for any caller's value to land in. Measured on SQLite and the in-memory driver through `protocol.createData`, `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and `engine.update`: each now answers success with `droppedFields: [{ fields: ['doubled'], reason: 'computed' }]`, the stored row carries no such key, and the read still returns the computed value. + + - **`computed` is not `readonly`.** `isSystem` exempts the static `readonly` strip and does not exempt this one. A `formula` field also declared `readonly: true` is reported once, as `computed`. + - **`strictReadonlyWrites` refuses it.** That option's coverage is derived from what `onFieldsDropped` reports, so a caller that passes it and sends a formula value now gets `ERR_READONLY_FIELD_REJECTED` with a `computed` drop in `drops`, and nothing is written, `isSystem` included. The refusal message names the reason and its remedy; a refusal without a `computed` drop reads exactly as before. + - **Hooks are handed the payload that will be stored.** A `beforeInsert` / `beforeUpdate` hook no longer sees the formula key in `ctx.input.data`; `ctx.submitted` on update still carries the caller's submission as sent. + - **Consumers of `DroppedFieldsEvent['reason']` must handle `computed`.** The contract requires a branch on `reason` to be exhaustive. In this release the strict refusal message (`@objectstack/objectql`) and the flow `create_record` / `update_record` step warning (`@objectstack/service-automation`) word it. + + **What `validate` runs now.** Before judging a row, `ObjectQL.validate` runs the write's own doors, by the same functions the write calls: the declared-field door (the refusal above), the computed-field strip, and the caller-write strips, under the write's `isSystem` gate (on `insert` mode the runtime-owned strip and the static `readonly` strip with its re-default; on `update` mode the static `readonly` strip, where a supplied `id` is the address the write binds and is never judged). What the write would drop is reported through a new optional `onFieldsDropped` listener on `validate`'s options, in the same events the write emits. One consequence for verdicts: a reference field declared static `readonly` is now stripped in the preview as it is on the write, so a validation rule that reads through it answers the same on both. + + **Unchanged.** A `summary` field keeps its column: a caller-supplied roll-up value is still stored as sent and overwritten by the next write of a child record. The REST layer's own handling of a missing column (schema drift) is unchanged. +- 75519e1: fix(automation): for a flow name a managed package ships, every startup step arms the package's flow, and a stored flow of that name is reported as shadowed (#20913) + + Clause-②: no + + A startup arms flows in two steps: the boot pull from the metadata registry, then a second bind at `kernel:ready` from the metadata protocol's flow list. The second step registered every flow the list held, with no precedence at all. So for a name a managed package ships, a stored flow of the same name could be armed after the boot pull had armed the package's flow. The stored definition then ran, while `getShadowedFlows()` and the startup warnings said the package's flow was armed, and named both contenders as the package. + + - Both startup steps, and the re-bind on `metadata:reloaded`, now resolve same-named flows through one precedence decision: `resolveFlowPrecedence`, with the engine's reader over the package loader's set. + - Within a name a managed package ships, the package's flow is armed and a stored flow of that name is shadowed. A managed package's flow is sealed (ADR-0126 §2): it is customized by cloning it under a new name or by switching it off. This replaces the earlier direction, in which a flow authored in the deployment won over the packaged flow of the same name. + - The shadowing record and the startup warnings name the stored flow as a runtime-authored row, not as the package. The collision warning says which rule armed the flow. + - Names no managed package ships are unchanged: flows authored in the deployment keep the order they were listed in, and two packages shipping one name still resolve by package id. + + **If you call `resolveFlowPrecedence` yourself:** within a name the reader says a package ships, the packaged contender now wins over a contender authored in the deployment. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [05be352] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/formula@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index b6fd76151da..5eea5f1c9d6 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Automation Service for ObjectStack — implements IAutomationService with plugin-based DAG flow execution engine", "type": "module", diff --git a/packages/services/service-cache/CHANGELOG.md b/packages/services/service-cache/CHANGELOG.md index 596b9d13ab7..b9ebcfde0c3 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,151 @@ # @objectstack/service-cache +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/observability@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 0810ce7720f..851bf0ef814 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters", "type": "module", diff --git a/packages/services/service-cluster-redis/CHANGELOG.md b/packages/services/service-cluster-redis/CHANGELOG.md index 2e8d462b304..012a9bb1161 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,129 @@ # @objectstack/service-cluster-redis +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/service-cluster@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 15586698c34..3ebf3a468e7 100644 --- a/packages/services/service-cluster-redis/package.json +++ b/packages/services/service-cluster-redis/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster-redis", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Redis cluster driver for ObjectStack — implements IPubSub/ILock/IKV/ICounter against Redis using ioredis.", "type": "module", diff --git a/packages/services/service-cluster/CHANGELOG.md b/packages/services/service-cluster/CHANGELOG.md index fb4b8e13ed3..5d1a2b2232e 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/service-cluster +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index 7937f9ef2c0..d14b3822207 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Cluster Service for ObjectStack — pluggable PubSub/Lock/KV/Counter primitives. Memory driver included; postgres/redis drivers ship separately.", "type": "module", diff --git a/packages/services/service-datasource/CHANGELOG.md b/packages/services/service-datasource/CHANGELOG.md index 74f8094a4d0..35b4d0b013a 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,193 @@ # @objectstack/service-external-datasource +## 17.6.0 + +### Patch Changes + +- 0e9ad74: Provenance comments in `service-datasource` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- c6954d6: fix(service-datasource): `POST` / `PATCH /api/v1/datasources` and `POST /api/v1/datasources/test` judge the datasource record they will persist against `DatasourceSchema`, the contract `os build` and `PUT /api/v1/meta/datasource/:name` already enforce (#21058) + + Clause-②: no + + - Before, these doors checked the driver `config` alone. A record that `DatasourceSchema` refuses was accepted `201`, and `GET /api/v1/meta/datasource/:name` then reported it `valid: false`. The record is now judged as it will be stored, with the `external.credentialsRef` that a supplied `secret` (or the existing binding) carries, before any secret or record is written. A refusal answers `400 DATASOURCE_ADMIN_ERROR` with `DatasourceSchema`'s own message, and nothing is persisted. + - Newly refused, for example: a mongo `config.url` whose userinfo names no user, or a composed mongo `config` with no `username`, beside a `secret`. The bound secret was never used and the datasource connected anonymously. Fix: put the user in the url (`mongodb://user@host/db`) or in `config.username`, or send no `secret`. Also refused: `schemaMode: 'external'` or `'validate-only'` with no `external` block. Fix: send `external: {}` or the federation settings. Also refused: a create with no `config`, or a `pool` key the schema does not declare. + - `POST /api/v1/datasources/test` answers `ok: false` with the same message instead of probing, so a green test no longer comes before a refused save. + - A `PATCH` that changes only `label` and/or `active` is not judged. A row stored before this change can still be renamed or taken out of service. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [df67985] +- Updated dependencies [42d78b9] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [19fc8d6] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [d3f88fa] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [810d42b] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [cf0346e] +- Updated dependencies [8fec76a] +- Updated dependencies [53ed3d1] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [95fed33] +- Updated dependencies [26437ae] +- Updated dependencies [f8178ff] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [a3dc817] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [45ce12a] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [e35c40a] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [c6b6889] +- Updated dependencies [ebdb6f2] +- Updated dependencies [862f12c] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [be5a83c] +- Updated dependencies [d2bc644] +- Updated dependencies [7923c8e] +- Updated dependencies [95b91cc] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/driver-sql@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/driver-turso@17.6.0 + - @objectstack/driver-memory@17.6.0 + - @objectstack/driver-mongodb@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/driver-sqlite-wasm@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 1176cac001b..f4abdc45e71 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.", "type": "module", diff --git a/packages/services/service-i18n/CHANGELOG.md b/packages/services/service-i18n/CHANGELOG.md index 8f90c662bef..a00a478ed02 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,153 @@ # @objectstack/service-i18n +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index d11d94ea11b..0b5632babc9 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "I18n Service for ObjectStack — implements II18nService with file-based locale loading", "type": "module", diff --git a/packages/services/service-job/CHANGELOG.md b/packages/services/service-job/CHANGELOG.md index 6e3542698dd..dcd0cadf18c 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/service-job +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index aa2b64640fa..0104eeced9d 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling", "type": "module", diff --git a/packages/services/service-knowledge/CHANGELOG.md b/packages/services/service-knowledge/CHANGELOG.md index 85231373285..e7dec202cd8 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,156 @@ # @objectstack/service-knowledge +## 17.6.0 + +### Patch Changes + +- e952cff: fix(service-knowledge): the realtime event bridge no longer keeps a `record.created` / `record.updated` / `record.deleted` branch, and its docs name the events ObjectQL really publishes (#20573) + + No producer emits a bare `record.*` event: the ObjectQL engine publishes `data.record.created` / `data.record.updated` / `data.record.deleted` for a single-record write and `data.records.updated` / `data.records.deleted` for a predicate write (`multi: true`), and `@objectstack/spec` already dropped the bare names from `RealtimeEventType`. The `KnowledgeServicePlugin` subscription handler still carried a branch for them, with a `payload.record ?? payload` fallback for a shape nothing sends. That branch is removed. The `data.record.*` sync (record body from `after`, id from `recordId`) and the `data.records.*` stale-index warning are unchanged. + + The `enableEventSync` option's TSDoc and the `handleRecordUpsert` / `handleRecordDelete` docs now name `data.record.created|updated|deleted` instead of `record.*`. + + If a plugin of yours publishes a bare `record.created`, `record.updated` or `record.deleted` event through `IRealtimeService` and relied on the knowledge index following it, publish `data.record.created|updated|deleted` with the `DataEvent` payload instead (the record in `after`, its id in `recordId`). +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 5389bde30af..c5d3c589b08 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Knowledge Service for ObjectStack — orchestrator implementing IKnowledgeService over pluggable IKnowledgeAdapter backends (RAGFlow, LlamaIndex, Dify, in-memory).", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index b7029a0b1db..c24a2a8ae03 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,186 @@ # @objectstack/service-messaging +## 17.6.0 + +### Patch Changes + +- 89801cd: **A flow `http` node's `signingSecret` now signs the request on every arm, with one scheme, and a secret that does not resolve refuses the node instead of letting the request leave unsigned.** + + `signingSecret` is declared as "HMAC-SHA256 secret → X-Objectstack-Signature", with no arm named. Only the durable arm honoured it, because only the messaging outbox signed. The default inline request, and a `durable: true` node on a host with no messaging HTTP outbox (which degrades to that inline request), were sent without the header while the run reported success. + + - `@objectstack/core`: **new exports** `signHttpBody(body, secret)` and `HTTP_SIGNATURE_HEADER`, the outbound HTTP signature scheme: `X-Objectstack-Signature: sha256=`, where a request with no body is signed over the empty string. They were `@objectstack/service-messaging`'s own, and they moved here so a sender with no outbox can sign with the same code. + - `@objectstack/service-messaging`: `signHttpBody` and `HTTP_SIGNATURE_HEADER` are still exported under the same names. They are now re-exports of the `@objectstack/core` bindings, not a second implementation. Delivery rows and the headers the outbox sends are unchanged. + - `@objectstack/service-automation`: the `http` node's inline request carries `X-Objectstack-Signature` whenever `signingSecret` is set. It is computed over the exact body the node sends (its JSON serialization of `config.body`, or the empty string when there is none), so a receiver that verifies with `signHttpBody` over the bytes it received accepts it on every arm. + - A non-empty `signingSecret` that renders to nothing at run time now fails the node with a guard refusal naming `config.signingSecret`, and nothing is sent. This covers a `{token}` with no value in the run, or one that renders the empty string. The refusal is on every arm, including the outbox arm, which used to enqueue such a delivery unsigned. A fault edge does not route it. The fix is to give the run the value the template reads. + - An authored `signingSecret: ''` still sends unsigned on purpose, on every arm. + + Clause-②: yes (widening) — two new exports on `@objectstack/core`'s root. Nothing is removed or renamed on any package. The one newly refused case is a node whose authored secret did not resolve, which the published contract already said signs. +- e47355b: Auth, webhook and outbound-delivery refusals, warnings and field help no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these three packages show to operators, administrators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/plugin-auth`: an unrecognised audience posture is refused because it must not fall through to a more permissive posture than the one intended; the `ObjectQL` adapter's case-insensitive warning says the `$ieq` operator is deliberately deferred until there is demonstrated pull for it; the `internal`-column refusal says the column is withheld from every ordinary read and recovered only through the engine's accessor; the 2FA re-enrollment errors say a re-enrolled TOTP secret may be live at sign-in without having been confirmed; the walled-owner boot warning says a declared owner is stamped verified only when an operator-provisioned path creates the account; the OTP send-budget lines name the budget without a number. + - `@objectstack/plugin-webhooks`: the parked-event record says the event is recorded rather than delivered unsigned (or without its authored headers) and rather than discarded without a trace; the redeliver refusals say a delivery that cannot be signed is refused rather than sent unsigned; the zero-trigger warning says the `api` trigger was removed because nothing could fire it; the seed and legacy-migration warnings say a credential is never stored in cleartext instead and that a failed migration leaves it cleartext in `definition_json`. + - `@objectstack/service-messaging`: the `sys_http_delivery` field help for `attempts` (in every shipped locale) says a parked row is not redeliverable because it carries no signature; the `headers_json` and `error` help and the outbox refusals drop their citations; the notification `ack()` refusal says cancelling a pending row is not part of the outbox contract until a live consumer needs it. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 422db78: Provenance comments in `service-messaging` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, refusal text or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index 6a59b0c0da3..fe72386cef2 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Messaging Service for ObjectStack — outbound notification dispatch (ADR-0012). Ships the MessagingChannel registry, emit() fan-out, and the always-on inbox channel; other channels (email/webhook/push/IM) plug in.", "type": "module", diff --git a/packages/services/service-package/CHANGELOG.md b/packages/services/service-package/CHANGELOG.md index 7febd436897..11c5b816979 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,157 @@ # @objectstack/service-package +## 17.6.0 + +### Patch Changes + +- 697845d: Provenance comments in `service-package` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 67d4a2cd46a..92496297b53 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index 65b5feb4cfc..891d5985db0 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/service-queue +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index 31955bc943a..8c8ea68eef7 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and durable DB-backed (sys_job_queue) adapters", "type": "module", diff --git a/packages/services/service-realtime/CHANGELOG.md b/packages/services/service-realtime/CHANGELOG.md index cfc5b71b044..611119d3e6d 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,164 @@ # @objectstack/service-realtime +## 17.6.0 + +### Patch Changes + +- e73ee2d: docs(service-realtime): the README and `InMemoryRealtimeAdapter` examples name the events the ObjectQL engine actually publishes (#20288) + + The usage example subscribed to and published `record.created`, and the security + posture section said the engine publishes `record.created` / `record.updated`. + The engine publishes `data.record.created` / `data.record.updated` / + `data.record.deleted` (and `data.records.updated` / `data.records.deleted` for a + predicate write), with the spec's `DataEvent` as the envelope's `payload` — the row + is `payload.after`, not the payload itself. Both examples now subscribe to + `data.record.created` and publish that shape. No code changes: the adapter matches + whatever event type a subscription names, exactly as before. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 7ede2264098..89c3b116507 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Realtime Service for ObjectStack — implements IRealtimeService with WebSocket and in-memory pub/sub", "type": "module", diff --git a/packages/services/service-settings/CHANGELOG.md b/packages/services/service-settings/CHANGELOG.md index f6bf47d2fd9..c06367a8c1e 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,178 @@ # @objectstack/service-settings +## 17.6.0 + +### Patch Changes + +- bbe03f4: Provenance comments in `service-settings` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 1073485d179..aa4f8f4eb88 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Settings service for ObjectStack — manifest registry + K/V resolver (OS_* env > Tenant > User > Default) + REST routes. See ADR-0007.", "type": "module", diff --git a/packages/services/service-sms/CHANGELOG.md b/packages/services/service-sms/CHANGELOG.md index 4413e6a7400..a3d4a345f29 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,155 @@ # @objectstack/service-sms +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [4d04b6b] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [55012df] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/plugin-auth@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index 33580fea18b..35c4b46b346 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "main": "dist/index.js", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index 9413728b800..a997eea5c1b 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,193 @@ # @objectstack/service-storage +## 17.6.0 + +### Patch Changes + +- 9b384f6: Provenance comments in `service-storage` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- f0cc16e: fix(service-storage): presigned S3 uploads no longer bake the CRC32 of an empty body into the signed URL (#21147) + + Clause-②: no + + `S3StorageAdapter.getPresignedUpload()` handed the browser a URL carrying + `x-amz-sdk-checksum-algorithm=CRC32&x-amz-checksum-crc32=AAAAAA==`. `AAAAAA==` + is the CRC32 of an empty body: the AWS SDK's default (`WHEN_SUPPORTED`) stamps a + checksum into every `PutObjectCommand` it prepares, and a presign prepares the + command before any byte exists. A store that enforces a query-signed checksum + against the uploaded body refuses every presigned browser upload with a checksum + mismatch, while server-side `upload()` keeps working. Hosted R2 does not enforce + it, which is why this stayed quiet; self-hosted deployments on other + S3-compatible stores may. + + The adapter's `S3Client` now sets `requestChecksumCalculation` and + `responseChecksumValidation` to `WHEN_REQUIRED`. None of the commands the adapter + issues is checksum-required, so the presigned PUT URL carries no `x-amz-checksum-*` + or `x-amz-sdk-checksum-algorithm` parameter, and a presigned GET URL no longer + carries `x-amz-checksum-mode=ENABLED`. + + Two server-side effects of the same setting, measured against a loopback server: + `upload()` and multipart `uploadChunk()` no longer send the client-computed + `x-amz-checksum-crc32` header (the SigV4 `x-amz-content-sha256` payload hash is + unchanged), and `download()` no longer asks the store for, or validates, a + response checksum. Both were added only by the SDK's default change; the older + behaviour is restored. SDKs older than the flexible-checksum release ignore the + two options. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [820d3f4] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/observability@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index e8ac63ae020..56e0265dc22 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index e6eb3069fd5..c5690980783 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,2466 @@ # @objectstack/spec +## 17.6.0 + +### Minor Changes + +- e5c7d07: `ISecurityService` (`@objectstack/spec/contracts`) gains an optional `getWritableFields(object, context)`: the field names field-level security lets the caller write on the object, the write-side twin of `getReadableFields` (#18386). + + Clause-②: yes (widening) + + - It is the exact complement of the fields the write path's field-level-security gate refuses when a payload names them. Neither the object permission nor a field's own rules (`readonly`, `system`, a `formula`, `summary` or `autonumber` type) are part of the answer. + - It fails soft like `getReadableFields`: `undefined` means no answer, `[]` means no field is writable. A system context gets every field. + - It is optional. A consumer checks `typeof svc.getWritableFields === 'function'`. When the method is missing, the consumer may narrow by `getReadableFields` instead, and must say in its response that it did. +- addbbf0: feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518) + + Clause-②: yes (widening) + + - **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused. + - **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source. + - **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. The runtime resolves the reference onto that served field; see the picklist runtime entry of this release. + - **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package. + - **Translation.** `TranslationData` gains `picklists..{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them. + - **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES). + - **Extraction.** `os i18n extract` walks `picklists.NAME.{label, options.VALUE}`, including an extension's options under the list it extends, and `os lint` reports an untranslated option under its own rule, `i18n/missing-picklist`. + - **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column. +- 9905e61: fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level `options` bag is refused by name (#20051) + + **BREAKING** — two narrowings on the `view` write door (`PUT /api/v1/meta/view/:name`, the Studio and MCP save). They ship as `minor` under the repo's launch-window convention for breaking changes. This is stage (iv), the last stage, of ruling 甲 on #20051. The storage half follows the maintainer's ruling on its Q2, letter B (「同意 批次 #256」). + + Clause-②: no (narrowing) + + ## What changes + + **1. What a saved view stores.** `saveMetaItem` used to validate a `view` body and then store the request body as sent, with two normalizations grafted back (filter operator spellings, and form `groups` → `sections`). It now stores the parsed value of every key the body carried, and nothing else: + + - **An undeclared key is dropped.** The members' top-level `.strip()` used to drop it from the parse only, so it lived in the store and nowhere in the contract. Every key the console reads back off a stored row is declared (`VIEW_CONSOLE_ROUND_TRIP_KEYS`), so nothing the console relies on is lost. The keys the console writes that are dropped are the ones mapped rather than declared in that record: a sort row's `id` (the builders mint a fresh one), a top-level `id` (read only when a row has no `name`), and `objectName` (every reader falls back to `object`, which both writers stamp). + - **A declared key keeps its normalized value.** Examples: `notEquals` → `not_equals`, `exportOptions: ['csv']` → `{ formats: ['csv'] }`, and a CEL string → its expression object. + - **A moved key is stored under its canonical spelling.** Examples: `groups` → `sections`, and `visibleOn` → `visibleWhen`. The second was never grafted before, so a form stored with `visibleOn` kept the legacy spelling. + - **A schema default the author did not write is NOT stored.** This is ADR-0087's `storable` rule, the one flows already follow: a stored row never pins the day's default. A console toolbar save (sort, density, hidden fields, column widths, inline edit) stores no `type: 'grid'`. A form stores no `sharing.enabled` and no section `collapsible` / `collapsed` / `columns` it did not write. Storing the whole parse output instead would also have minted rows that fail their own re-save: a column-less toolbar patch carrying the `grid` default is refused as "sets `type` but lists no `columns`". + + The stored row re-parses to exactly what the save accepted, so a GET → PUT of it is judged the same. Every other metadata type keeps its request body, with the two grafts, as before. + + **2. A ViewItem record's top-level `options` bag is refused.** On a record (`{ name, object, viewKind, config }`), the member's top-level strip used to drop `options` from the parse unread while the save stored it. The interface page then rendered it and the object page did not. It is now refused by name with `422 INVALID_METADATA` at `options`, and the message prescribes `config.KIND`. No console write puts the bag on a record. The flattened list overlay's legacy `options` bag is unchanged: it is judged key by key, and objectui pins it. + + ## FROM → TO + + | you wrote | the stored row / the door now | + |:--|:--| + | a view body with a key its member does not declare (`objectName`, a form-only `layout` on a list view, `isPinned` on a form) | the key is not stored: write only declared keys (`object`, not `objectName`) | + | a view body relying on a schema default being written into the row | the row carries only what you wrote; the parse applies the default on every read | + | `sort: [{ id, field, order }]` | stored as `sort: [{ field, order }]` | + | `groups: [...]` / `visibleOn: '…'` on a form | stored as `sections: [...]` / `visibleWhen: { dialect: 'cel', source: '…' }` | + | a ViewItem record with `options: { kanban: {...} }` | `422` at `options`: move it to `config: { kanban: {...} }` (`config.KIND`), or remove it | + + **The one-line fix:** write the declared spelling. A stored view you read back is what the contract accepts, and a record's per-kind blocks live under `config`. + + ## Existing rows + + Stored rows are not migrated and not re-read differently. The maintainer's word on this card is 「20051 不考虑现有的数据」. A row keeps its bytes until its next save, and that save stores it as described above. A record carrying a top-level `options` is refused on its next save and served as stored until then. + + +- 0efbdc3: feat(spec)!: connector-attached sync leaves the connector — `syncConfig` and `fieldMappings` are retired, and a `mapping` gains the `connectorSource` pull binding (#20281) + + **BREAKING** — `connector.syncConfig` (the `DataSyncConfig` block: `strategy`, + `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, + `deleteMode`, `filters`) and `connector.fieldMappings` (the `ConnectorFieldMapping` + list: `source`, `target`, `defaultValue`, `dataType`, `required`, `syncMode`) are + removed from `ConnectorSchema` and `DeclarativeConnectorEntrySchema` — so from + `defineConnector`, `stack.connectors[]`, the `PUT /api/v1/meta/connector/:name` door + and `AutomationEngine.registerConnector`. The `DataSyncConfigSchema`, + `SyncStrategySchema`, `ConnectorConflictResolutionSchema` and + `ConnectorFieldMappingSchema` exports (and their `DataSyncConfig`, `SyncStrategy`, + `ConnectorConflictResolution`, `ConnectorFieldMapping` types and `…Parsed` aliases) + leave `@objectstack/spec/integration` with them. ADR-0049, ruled ENFORCE on the + maintainer's criterion for a declared-but-unenforced family, with the definition + MOVED: every mainstream platform binds a sync to its TARGET, not to the connection. + + Measured before removal: no engine ever ran a connector-attached sync or moved a + value through a connector field mapping — outside the spec package `syncConfig` + appeared only in two comments and `fieldMappings` nowhere, the automation service's + declared-connector item carried neither key, and the def a provider registers is its + own. The `latest_wins` and `soft_delete` defaults read as configured policy and did + nothing; the platform has no soft delete. + + **Added:** `mapping.connectorSource` — the pull + binding on the target side, beside the mapping's existing `targetObject`, + `fieldMapping`, `mode` and `upsertKey`: `connector` (a `rest` or `openapi` + connector instance), `action` (the action that reads the records), optional + `input`, optional `recordsPath`, and an optional `watermark` (`field` on the + record, `param` on the request) for a timestamp-incremental pull. Version 1 is a + one-way pull. It carries no cadence (a `job` sets that), no credential (the + connector instance holds it) and no delete or conflict policy. The connector sync + executor, `@objectstack/service-automation`'s `pullConnectorSource` (#20919), reads + it; nothing schedules a pull until the `job` stage lands. + + ### FROM → TO + + | removed | what to write instead | + | --- | --- | + | `connector.syncConfig` | delete the key. A sync you still want is a `mapping` on its target object, with `connectorSource` naming the connector and its read action, `watermark` for an incremental pull, and a `job` for the cadence. `direction`, `conflictResolution` and `deleteMode` have no counterpart: the pull is one-way and writes through `mode` / `upsertKey`. | + | `connector.fieldMappings` | delete the key, and carry its `source` → `target` pairs into that mapping's `fieldMapping` (a `defaultValue` becomes a `constant` transform). | + | `DataSyncConfigSchema`, `SyncStrategySchema`, `ConnectorConflictResolutionSchema`, `ConnectorFieldMappingSchema` and their types | no replacement — nothing parsed a sync or a connector field mapping into anything that ran. | + + **The one-line fix: delete `syncConfig:` and `fieldMappings:` from every connector.** + `os migrate meta --from 17` lists the mechanical edits for existing sources. + + ⚠️ Runtime behaviour is deliberately **unchanged**: no connector sync ever ran. + What changes is the answer an author gets — both keys are refused at parse with a + prescription naming the target-side binding, and in `tsc` (their input type is + `never`), instead of being saved with no effect. + + ### The retirement kit + + - **Tombstones.** `syncConfig` and `fieldMappings` are `retiredKey()` tombstones on + the private `ConnectorBaseSchema` both published carriers wrap (the schema is not + `.strict()`, so a bare deletion would be a silent strip, ADR-0104). + `RETIRED_KEYS_BY_MAJOR[18]`: `integration/Connector:syncConfig`, + `integration/Connector:fieldMappings`, + `integration/DeclarativeConnectorEntry:syncConfig` and + `integration/DeclarativeConnectorEntry:fieldMappings`. Neither key had a default, + so no retired-default residue is owed. + - **Four defs leave whole** (`RETIRED_DEFS_BY_MAJOR[18]`): `integration/DataSyncConfig`, + `integration/SyncStrategy`, `integration/ConnectorConflictResolution`, + `integration/ConnectorFieldMapping`. The two `RENAMED_DEFS` entries that targeted + them left the rename table. + - **D2 conversion `connector-sync-keys-removed`** (step 18, retired from the load + path): strips both keys from `connectors[]` and from stored `sys_metadata` + connector rows (the rehydration seam replays it), one notice per key, as a + lossless delete. It never writes a `mapping`: a pulled mapping would start writes + that never happened. + - **D3 entry `connector-sync-keys-retired`** carries the family's judgement: which + syncs should now exist as target-side mappings, and what an author who relied on + `export`, `bidirectional`, `soft_delete` or a conflict policy does without them. + - **No deprecation window**, per the project's startup-stage posture. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published, so this is breaking for consumers no telemetry was consulted for. + + Clause-②: yes (narrowing) + + +- c8dd8dd: An authored analytics cube's measure `format` and time-dimension `granularities` now take effect on the analytics query doors, the way a compiled dataset's always have (#20282). + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one. The raw-SQL path declines every bucketed query, so such a query now runs on the engine aggregate path, which answers `400 INVALID_FIELD` for every member it cannot evaluate: a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression); and, on a cube whose members resolve through its `joins`, a measure or a `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a `dateRange` window, so grouping by a one-granularity time dimension over a joined object is refused too), a dimension that traverses more than one relationship, and an `avg` or `count_distinct` measure beside any dimension over a joined object. The raw-SQL path answers every one of these, with one group per distinct timestamp; each is now refused, exactly as it already was when the caller stated that granularity by hand. On a host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both), no strategy remains for a bucketed query, so every newly bucketed query, a plain `count` included, now answers "No strategy can handle query" instead of grouping raw timestamps. The remedy: run such a query without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key); on a raw-SQL-only host, add the engine aggregate bridge. It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect. + + Until this change both keys were read on the compiled-dataset path only. One cube shape has three producers — cubes authored with `defineCube()` / `defineStack({ analyticsCubes })`, cubes the dataset compiler mints, and cubes inferred for an ad-hoc query — and only a compiled dataset's cube reached the two readers: + + - **`measures..format`** reached a caller as `fields[].format` only because the dataset door copies it from the DATASET measure. An authored cube has no dataset, so `POST /api/v1/analytics/query` described its measure columns with `name` and `type` alone. Now every measure column a query names carries the `format` its cube measure declares, whichever strategy answered, and a column that declares none carries no `format` key at all. `GET /api/v1/analytics/meta` is unchanged: its projection stays `name`, `type` and `title`, and a client reads `format` off the query result's `fields[]`, as the Data API page already says. The value is relayed verbatim; the vocabulary `fields[].format` documents is a numeral pattern such as `"$0,0.00"` or `"0.0%"`. + - **`dimensions..granularities`** was the default bucket only for a compiled dataset, which the dataset executor filled in before querying. An authored cube's time dimension grouped raw timestamps whatever it declared. Now `query()` and the `generateSql()` dry run read it the same way, through the one rule both paths share: a single-entry list is the dimension's default bucket for a query that groups by it; a granularity the query states always wins, and one the list does not name is not refused (the dataset path compares against no list either); a list of two or more states no default; and a `timeDimensions` entry that carries only a `dateRange` for a dimension the query does not group stays a filter. + + What to expect after upgrading: + + - **A cube measure that declares `format`** now carries it on `POST /api/v1/analytics/query` results. A client that formats amounts from `fields[].format` starts formatting that column. + - **A cube time dimension that declares one granularity** (`granularities: ['month']`) is now bucketed by it when a query groups by it without stating one: one row per month where there was one row per timestamp. Name another granularity in the query's `timeDimensions` to bucket differently. + - **A cube time dimension that declares several, or none**, behaves exactly as before. + - **Compiled datasets** (`POST /api/v1/analytics/dataset/query`) answer exactly as before: the value read off their cube is the one the dataset door already used. + + In `@objectstack/spec`, `MetricSchema.format` and `DimensionSchema.granularities` now carry descriptions that state what the analytics service does with them (the metric's example values move from the names "currency" / "percent" to numeral patterns, the vocabulary the `fields[].format` slot documents), and the liveness ledger rows `analytics_cube.measures.format` and `analytics_cube.dimensions.granularities` move from `dead` to `live`, citing the new readers. +- 03cdb9a: `GET /api/v1/analytics/meta` now publishes an analytics cube's `description`, each measure's and dimension's `description`, and each measure's `format`, when the cube definition declares them (#20282). + + Clause-②: yes (widening) + + - `CubeMeta` (`@objectstack/spec/contracts`) gains an optional `description` on the cube and on each measure and dimension, and an optional `format` on each measure. `AnalyticsMetadataResponseSchema` declares the same members. A definition that declares none of them is published exactly as before. + - `AnalyticsService.getMeta` copies what the definition declares and fills in nothing. A cube compiled from a dataset carries each dataset measure's `format` and no `description`. + - The liveness ledger rows `analytics_cube.description`, `measures.description` and `dimensions.description` move from `dead` to `live`. + + This supersedes one sentence of this release's note on an authored cube's measure `format` and `granularities`: it says `GET /api/v1/analytics/meta` is unchanged and keeps `name`, `type` and `title`. With this change `/meta` also publishes each measure's declared `format`. A client that formats a result column still reads `format` off the query result's `fields[]`. +- 542670d: feat(spec)!: retire the connector `triggers` array — the `ConnectorTrigger` shape nothing ever registered, polled or received (#20287) + + **BREAKING** — `connector.triggers` (the `ConnectorTrigger` array: `key`, `label`, + `description`, `type: 'polling' | 'webhook'`, `intervalSeconds`) is removed from + `ConnectorSchema` and `DeclarativeConnectorEntrySchema` — so from `defineConnector`, + `stack.connectors[]`, the `PUT /api/v1/meta/connector/:name` door and + `AutomationEngine.registerConnector` — and the `ConnectorTriggerSchema` / + `ConnectorTrigger` exports leave `@objectstack/spec/integration` with it. ADR-0049 + enforce-or-remove, ruled RETIRE on the maintainer's criterion for a + declared-but-unenforced family; ADR-0041 is unchanged: connector-event triggers stay + in its third tier, as their own trigger package, promoted when real projects ask for + them — and then in the mainstream shape (subscribe / unsubscribe lifecycle, + signature verification, a dedupe cursor), which these five keys could not carry. + + Measured before removal: `registerConnector` walks a connector's `actions` only and + stores the rest of the def unread; the engine's trigger registry holds FLOW trigger + kinds (`record_change`, `time_relative`, `schedule`, `api`) and no connector trigger + ever entered it; no polling loop read `intervalSeconds`; no receiver was driven by a + `webhook` trigger; and no connector package, provider or example declared one. A + declared trigger parsed clean and never started a flow. + + ### FROM → TO + + | removed | what to write instead | + | --- | --- | + | `connector.triggers` with a `type: 'polling'` trigger (`intervalSeconds`, or the pre-rename `interval`) | delete the key, and write a `schedule` flow whose `connector_action` node calls the connector's action — at the cadence you meant, in seconds. | + | `connector.triggers` with a `type: 'webhook'` trigger | delete the key, and write an `api` flow that the external sender calls, with a `connector_action` node calling the connector's action. It opens an inbound endpoint that never existed before: an `api` flow is refused without a per-flow secret and every call must carry its signature, so the sender must be able to sign. | + | `ConnectorTriggerSchema`, `ConnectorTrigger` | no replacement — nothing parsed or constructed a connector trigger. | + + **The one-line fix: delete `triggers:` from every connector.** + `os migrate meta --from 17` lists the mechanical edits for existing sources. + + ⚠️ Runtime behaviour is deliberately **unchanged**: no connector trigger ever started + anything. What changes is the answer an author gets — the key is refused at parse + with a prescription naming the two shapes that work, and in `tsc` (its input type is + `never`), instead of being saved with no effect. + + ### The retirement kit + + - **Tombstone.** `triggers` is a `retiredKey()` tombstone on the private + `ConnectorBaseSchema` both published carriers wrap (the schema is not `.strict()`, + so a bare deletion would be a silent strip, ADR-0104). + `RETIRED_KEYS_BY_MAJOR[18]`: `integration/Connector:triggers` and + `integration/DeclarativeConnectorEntry:triggers`. The key had no default, so no + retired-default residue is owed. + - **The provider-bound refusal is gone.** `DeclarativeConnectorEntrySchema` used to + refuse `triggers` on a provider-bound instance, reasoned "the provider derives them + from the upstream at boot" — untrue, since no provider ever derived a trigger. The + tombstone refuses every value on every carrier, so that rule became unreachable and + was deleted rather than re-reasoned; a provider-bound instance now meets the + retirement prescription. + - **The def leaves whole** (`RETIRED_DEFS_BY_MAJOR[18]`: `integration/ConnectorTrigger`). + - **D2 conversion `connector-triggers-removed`** (step 18, retired from the load path): + strips the array from `connectors[]` and from stored `sys_metadata` connector rows + (the rehydration seam replays it), one notice per connector, as a lossless delete. + A trigger is stripped, never turned into a flow. + - **The chain.** In the same step, `connector-health-and-trigger-durations-unit-in-key` + renamed `triggers[].interval` to `intervalSeconds`. That trigger half is absorbed by + this removal, as its breaker half already was by the `health` removal, so with neither + half left the rename conversion is gone from the table and from step 18; an author + holding either spelling ends with no `triggers` at all. The retired-key row + `integration/ConnectorTrigger:interval` stays as the record. + - **D3 entry `connector-triggers-retired`** carries the family's judgement: which + triggers should exist now as flows, the cadence in seconds, and whether an external + sender can sign the calls a signed `api` flow requires. The absorbed rename's own D3 entry + (`connector-resilience-durations-unit-in-key`) is gone with its + conversion. + - **No deprecation window**, per the project's startup-stage posture. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published, so this is breaking for consumers no telemetry was consulted for. + + Clause-②: no (narrowing) + + +- e73ee2d: feat(spec)!: `RealtimeEventType` names the realtime events the runtime emits — `data.record.*` and `data.records.*`; `record.created` / `record.updated` / `record.deleted` / `field.changed` retired (#20288) + + Clause-②: yes (narrowing) + + **BREAKING** — shipped as `minor` under the launch-window convention + (`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by + this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, + never by the level). The enum both gains and loses values: it gains the five + names the runtime emits and loses the four it never emitted. + + `RealtimeEventType` (`@objectstack/spec/api`) types `SubscriptionEvent.type`, so + it is the event vocabulary of `Subscription.events[]` and + `RealtimeConfig.subscriptions[].events[]`, and the generated API reference + published it as such. None of its four values was ever emitted by anything. The + ObjectQL engine publishes `data.record.created` / `data.record.updated` / + `data.record.deleted` for each written record and `data.records.updated` / + `data.records.deleted` for a predicate write (`multi: true`), and it parses every + event through `DataEventSchema` / `BulkDataEventSchema` before publishing. A + subscription written with the names the reference showed could never fire. + The runtime's published event names do not change; the enum moves to them. + + - **Accepted now:** exactly `DataEventType` + `BulkDataEventType` — + `data.record.created`, `data.record.updated`, `data.record.deleted`, + `data.records.updated`, `data.records.deleted`. Metadata change events + (`metadata.{type}.{action}`) are not members: a subscription event is + record-shaped, and metadata events keep their own `MetadataEventType` contract + and `subscribeMetadata` client primitive. + - **Refused now:** `record.created`, `record.updated`, `record.deleted` and + `field.changed`, each with its own prescription. Any other unknown value keeps + zod's own message, which lists the legal names. + + ``` + FROM SubscriptionSchema.parse({ id, transport: 'websocket', events: [{ type: 'record.created', object: 'account' }] }) + -> parsed; nothing ever published 'record.created', so the subscription never fired + TO -> ZodError at events[0].type (invalid_value): `record.created` was removed from + `RealtimeEventType` in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — … Write + `data.record.created` — the same event, spelled the way the engine publishes it. + + FROM { type: 'record.updated' } -> TO { type: 'data.record.updated' } + (add { type: 'data.records.updated' } to also hear predicate writes, which carry a count, not records) + FROM { type: 'record.deleted' } -> TO { type: 'data.record.deleted' } + (add { type: 'data.records.deleted' } to also hear predicate writes) + FROM { type: 'field.changed' } -> TO { type: 'data.record.updated' } + (read the field from the DataEvent payload's `changes`; no event is published per field) + ``` + + **Fix.** Rename each value as mapped above. `tsc` refuses the old values at a + `RealtimeEventType` / `SubscriptionEvent` position (the type no longer contains + them), and a `SubscriptionSchema`, `SubscriptionEventSchema` or + `RealtimeConfigSchema` parse refuses them with the prescription. A handler keyed + on an old name never ran, so the rename changes behaviour only by making it fire. + Nothing in the open framework parses a subscription today — it mounts no realtime + transport — so no running deployment changes behaviour. + + ### The kit + + - **Schema.** `RealtimeEventType` lists the five emitted names; its error map + gives each retired value its own prescription (the `HookBodyCapability` + precedent for a removed enum value). `realtime.test.ts` pins the enum equal to + `DataEventType` + `BulkDataEventType`, and pins each refusal's code, path and + first sentence through `SubscriptionSchema`. + - **ADR-0087.** The D3 entry `realtime-event-type-unemitted-values-retired` + carries the prescription to `os migrate meta` and the upgrade guide. No D2 + conversion and no `RETIRED_KEYS_BY_MAJOR` row: an enum value is not a key, and + a subscription is neither a stack collection member nor a stored row, so no + conversion seam would ever see one. + - **Docs.** The `realtime` reference page is regenerated; the + `RealtimeEventPayload.type` and `RealtimeEvent.type` examples name emitted + events. + + +- 92fe081: **BREAKING** — the inner `name` on an analytics cube's measures and dimensions (`MetricSchema.name`, `DimensionSchema.name`) is now refused at parse: nothing ever read it. The record key a member is declared under IS its name — the analytics API publishes it as `.` and a query names it that way. Delete the inner `name`; to rename a member, rename its key. + + Clause-②: no (narrowing) + + `measures` and `dimensions` are records, and the key was always the member's identity: `GET /api/v1/analytics/meta` publishes every member as `${cube.name}.${key}` (in `@objectstack/service-analytics` and in `@objectstack/driver-memory`), and both SQL strategies and the in-memory driver resolve a member by indexing the bag with that key. Measured before removal, with a lit control: zero reads of a member's inner `name` in non-test source, against four reads of the neighbouring `measure.label` / `dimension.label` in the same two `getMeta` projections. So the inner `name` was a REQUIRED second copy of the identity that nothing read — and one that disagreed with its key was silently ignored (this repository's own in-memory driver fixtures authored `totalAmount: { name: 'total_amount', … }` and queried `orders.totalAmount`). + + **Removed rather than enforced** (ADR-0049 enforce-or-remove; the triage verdict on the card, by the maintainer's criterion for declared-but-unenforced families): Cube.dev and LookML key a member by its declared name, with no second inner name that can disagree — and here the record key already delivered it. + + ## FROM → TO + + | you wrote (17.4 and earlier) | write instead | + | --- | --- | + | `measures: { total_amount: { name: 'total_amount', label: 'Total', type: 'sum', sql: 'amount' } }` | `measures: { total_amount: { label: 'Total', type: 'sum', sql: 'amount' } }` | + | `dimensions: { status: { name: 'status', label: 'Status', type: 'string', sql: 'status' } }` | `dimensions: { status: { label: 'Status', type: 'string', sql: 'status' } }` | + | an inner `name` that DIFFERS from its key, e.g. `totalAmount: { name: 'total_amount', … }` | nothing changes at runtime — `orders.totalAmount` was already the name every query used. Delete the inner `name`, or, if `total_amount` is the name you meant, re-key the member and update every query, dashboard and report that names `orders.totalAmount` | + + **The one-line fix:** delete `name` from every metric and dimension; the key it is declared under is its name. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`Metric` / `Dimension` type the key `never`), and the parse — `defineCube()`, `defineStack({ analyticsCubes })`, `PUT /api/v1/meta/analytics_cube/:name` — refuses it at `measures..name` / `dimensions..name` with the prescription: + + > `measures..name` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it never had an effect: the record key is the metric's name. … Delete the key. To rename a metric, rename its key in `measures` — and every query, dashboard and report that names `.`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. + + ## The retirement kit + + - **`retiredKey()` tombstones, not a bare deletion** — even though both member shapes are `strictObject`s (the `action.aria` posture). A bare delete would still be loud, but only as a generic unrecognized-key report that cannot carry the prescription; the tombstone types the key `never` for `tsc` and raises the upgrade text at parse. The keys therefore stay in the walked shape: both liveness rows stay `dead` with a `REMOVED` note, and the authorable-surface baseline marks `data/Metric:name` and `data/Dimension:name` `[RETIRED]`. + - **The D2 conversion `cube-member-inner-name-removed`** (protocol 18, retired from the load path) deletes the inner `name` from every metric and dimension of every `analyticsCubes[]` entry. It is owed because the key was REQUIRED, so every stored or built cube carries it. It strips a disagreeing value too — the key already won everywhere, so no query or discovery answer changes — and its notice prints both spellings (`from: name "total_amount"`, `to: (removed; the record key "totalAmount" is the name)`). Its D3 record is the semantic entry `cube-member-inner-name-retired`, which asks the author of a disagreeing name which spelling they meant. + - **The producers stop writing it** (`@objectstack/service-analytics`): the dataset compiler (`compileDataset`), `CubeRegistry.inferFromObject` and the ad-hoc query mint no longer put an inner `name` on the members of the cubes they build. The members are filed under the same keys as before, so `/analytics/meta`, `/analytics/query` and `/analytics/sql` answer exactly as they did. The package README's cube example is corrected. + - **The `measures` / `dimensions` descriptions now say it**: "keyed by metric name: the record key IS the metric's name, published and queried as `.`". + + ## Reach, measured + + - This repository, non-test: the showcase cube (`examples/app-showcase`, 8 members), the published `objectstack-ui` skill's `defineCube` example (6), the `service-analytics` README (3), and the three internal cube mints above — every one wrote the inner `name` EQUAL to its key, and all are corrected here. Test fixtures: about 300 member literals and map-built members across 84 test and fixture files in eight packages, all EQUAL to their key except 21 in `driver-memory`, which disagreed (camelCase key, snake_case inner name) and were already queried by key. + - Out-of-repo authors: NOT MEASURED. + + ## What an operator with a STORED cube sees + + A `sys_metadata` `analytics_cube` row or a built artifact written before this release carries the inner `name` on every member. Nothing breaks at read: the conversion replays on rehydration and at the artifact door and strips it, so the cube is served canonical and parses. `os migrate meta --stored --apply` rewrites the rows. + + +- c4c68ca: feat(spec): the stored-filter conversion rewrites a filter on a block whose rows are inline, as it does on any other block + + The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` no longer leaves every filter of a page component whose rows are inline (`data: { provider: 'value', … }`, a `data` array, or `staticData`) as stored. Such a filter, the binding's `dataSource.filter` included, is now rewritten to the `[{ field, operator, value }, ...]` rule array exactly as it is on a block that queries an object. What still stays as stored, and is still reported as a TODO, is only a filter with a part that has no lossless rule spelling: a combinator, a null value, or an operator the rule vocabulary does not spell. That holds on any block. + + Why the conversion declined, and why it no longer needs to: the `object-map`, `object-tree`, `object-calendar` and `object-gantt` blocks match that filter against their own rows in objectui's in-memory data source (`ValueDataSource.find`). The conversion was written against an objectui version whose `find` excluded every row for a rule array, so it left those filters alone and said so in the TODO. The objectui version this repository pins (`.objectui-sha`, the same pin the previous release shipped) lowers a rule array before it matches, and it selects the rows the stored form selected. That was measured over every operator the conversion maps: 114 filters on eight rows, null and missing values included. The same filters select no row on the objectui build just before that fix. So the decline was already protecting nothing: it only left convertible filters unconverted and reported TODOs that no longer needed to exist. + + What an operator sees: + + - `os migrate meta --stored` now lists such a page as a pending rewrite. It used to list it as a `skipped` row with a TODO. A preview over a database whose only legacy filters sat on inline-row blocks therefore exits 1 until `os migrate meta --stored --apply` rewrites them. + - Until then, every stored-row read replays the same rewrite, so the block reads the rule array and shows the same rows. + - Nothing an author writes is accepted or refused differently. The conversion stays retired from the authoring path, and no schema changes. + + The migration entries `element-data-source-and-object-block-filter-rule-array` and `object-grid-default-filters-rule-array`, and the protocol-18 step rationale, no longer say that inline-row filters are left as stored. + + ADR-0087 disposition: already registered. This changes the behaviour of the registered D2 conversion `page-component-filter-record-to-rule-array` and edits its two D3 entries. There is nothing new to register. + + Clause-②: no +- 5363e2d: feat(spec,service-automation): a flow run's result carries the flow's authored label as `flowLabel` (#20318) + + Clause-②: yes (widening) + + **The widening.** `AutomationResult` (`@objectstack/spec/contracts`) gains one + optional member, `flowLabel?: string`, and `TriggerFlowResponseSchema` + (`@objectstack/spec/api`) mirrors it on `data`. The automation engine sets it to + the flow definition's `label`, copied verbatim, the same way it copies + `successMessage` and `errorMessage`. Nothing is removed or renamed, and no + existing member changes meaning. + + **Why.** A flow runner names the flow it is running, in its header and in its + completion toast, and translates that name against the `flows..label` + translation key, falling back to the authored label. The runner only held the + flow's API name, so there was no authored label to fall back to. The console's + reader of the translation key is a separate change. + + **Which results carry it.** + + - **Set** on every result of an evaluation of a registered flow: `status: 'paused'` + (first attempt, retry attempt, a resume that pauses again), a terminal success + (including the two skip exits), `'failed'` (including an exhausted retry budget), + `'stranded'`, `'refused'`, and a resumed parent whose delegated child failed. + - **Absent** on every refusal that carries a `code` (the run never dispatched, or a + resume never continued it) and when the flow is not registered. + - **Subflow chains** answer with the label of the run the caller addressed, which + is the parent. The child that supplied the screen does not lend its label. + - **Never the API name.** `FlowSchema` requires `label`, so the value is always + what the author wrote, an empty string included. + + **At the wire.** Both runner doors relay the result verbatim on a `200`, so + `data.flowLabel` arrives on `POST /api/v1/automation/:name/trigger` (a paused or + finished launch) and on `POST /api/v1/automation/:name/runs/:runId/resume` (a + further pause or the completion). A `400 FLOW_FAILED` answer is unchanged: its + `error.details` keep their fixed set (`errorMessage`, `summary` and, on resume, + the stranded verdict), with no `flowLabel`. + + **For a consumer.** A client that parses the trigger response with + `TriggerFlowResponseSchema` now keeps `data.flowLabel`, where an undeclared key + would have been stripped. A caller that deep-compares a whole `AutomationResult` + from `execute()` or `resume()` sees one more key on the results listed above. +- c876a74: fix(spec,driver-turso)!: a turso config that forces `mode: 'replica'` with no `syncUrl` is refused where it is written and when the driver is built, instead of running as a plain local database that never syncs + + Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves. + + An embedded replica is a local file kept in sync with the remote named in `syncUrl`. A config that forced `mode: 'replica'` on a `file:` url with no `syncUrl` (or an empty one) was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the built driver before this change, with and without `sync`: it constructed with `transportMode` `'replica'`, `isSyncEnabled()` answered `false`, no sync interval started, the sync call did nothing, and every read and write went to the local file. A datasource declared as a replica ran as a plain local database that never replicated, with no error and no warning. + + **BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out: + + - **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service; + - **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened. + + The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. The sibling refusals keep their order. A forced replica on a remote url, an in-memory url or a bare path still meets its `url` refusal first. One with `sync` and no `syncUrl` still meets the `sync` refusal first; the schema now reports the `mode` issue beside it. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it. + + ### Migration: FROM → TO + + | You wrote | Write instead | + | --- | --- | + | `url: 'file:./data/replica.db', mode: 'replica'` (no `syncUrl`, or `syncUrl: ''`) | an embedded replica: keep the `file:` url and name the remote, `syncUrl: 'libsql://my-db.turso.io'` | + | the same | a plain local database: drop `mode` (`url: 'file:./data/app.db'` alone) | + + A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and ran as a local database. The way out is the table above. + + Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero. + + +- f1e921a: feat(spec)!: `$empty` joins `FILTER_OPERATORS`, and the view operators `is_empty` / `is_not_empty` lower to it (#20446) + + A stored 「is empty」 / 「is not empty」 — `['field', 'is_empty', …]`, `isempty`, `is_not_empty`, `isnotempty`, in a view rule, a sharing rule or any filter array — now lowers to `{ field: { $empty: true | false } }` instead of `$null`. `$empty` is answered by the field's DECLARED type: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. So an 「is empty」 rule on a text field now also finds `''`, and on a multi-value field also finds `[]`, which the `$null` lowering missed. `is_not_empty` is its exact complement. `$empty` is in `FILTER_OPERATORS` (and `ALL_OPERATORS`) now, and `canonicalAstOperator` folds the empty pair onto `is_empty` / `is_not_empty` rather than onto `is_null` / `is_not_null`. On `@objectstack/driver-memory`, a QueryAST comparison node (`{ type: 'comparison', operator: 'is_empty' }`) is answered by the same declared-type arm. + + **BREAKING**: two things accepted before are refused now, each loudly and with its fix. + + - **A `{ $empty: … }` object written as a field value** (a `where` pasted into an insert or update payload) is refused with `VALIDATION_FAILED` (`invalid_type`, "$empty is a filter operator, not a value"). Before, a text-like field stored it as data. + FROM `update('task', { title: { $empty: true } })` → TO write the value itself (`{ title: '' }`, `{ title: null }`); a filter belongs in `where`. + - **`is_empty` / `is_not_empty` where no face holds the column's declared type** is refused with `INVALID_FILTER` / 400 (`READ_SCOPE_COMPILE_FAILED` / 500 on an analytics read scope). The `$null` lowering answered these. The compositions: + - the built-in `id`, which no object declares. FROM `['id', 'is_empty', true]` → TO `['id', 'is_null', true]` / `is_not_null`; + - a federated (external) object on a driver that does not implement `registerExternalObject` (driver-memory, driver-mongodb). The boot already reports such an object as NOT bound to its remote table, naming it, and its reads answered from a table named after the object. FROM `is_empty` on such an object → TO bind it on a driver that implements federation (driver-sql and its heirs, driver-turso); + - an `AnalyticsService` constructed without `sourceFieldMeta`. FROM such a host → TO pass `sourceFieldMeta` (the package README shows it), or filter with `is_null` / `is_not_null`; + - a multi-value column on a SQL dialect `driver-sql` does not model (a knex client other than SQLite, PostgreSQL or MySQL). FROM `['tags', 'is_empty', true]` there → TO `['tags', 'is_null', true]` / `is_not_null`. + + Stored sharing rules and views that use 「is empty」 are not rewritten; they are re-read under the new meaning. Production rules that use 「is empty」 on a text or multi-value field were not measured; each finds more rows (the `''` / `[]` ones) from this release. + + Clause-②: yes (narrowing) + + +- 7a1faf1: **`objectstack validate` and `objectstack build` now report the ADR-0087 conversions `defineStack` applied, and `objectstack validate --strict` fails on them.** + + `defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, in either mode, and prints one `defineStack: PATH: 'OLD' → 'NEW' (converted at load; conversion 'ID', retires in protocol N)` line on stderr. The two commands received that already-converted stack, so their own conversion pass found nothing to convert: `--json` answered `conversions: []` for every `defineStack` config, and `objectstack validate --strict` exited 0 on a spelling that stops loading in a named protocol major. A CI job gating on either could not see the retirement coming. + + - `@objectstack/spec`: `defineStack` (both modes) records every conversion notice it applied on the stack it returns, beside the provenance mark and stamped in the same act. The record is non-enumerable and frozen, so the schema, `Object.keys` and `JSON.stringify` never see it and no compiled artifact changes. `composeStacks` records its inputs' records in input order, counting the same built stack passed twice once. **New export:** `stackConversionsOf(value)` returns the `ConversionNotice[]` a producer recorded, the same element the commands' `conversions` field publishes, and `[]` for a value no producer returned. Like the mark, the record does not survive a spread or JSON copy. + - `@objectstack/cli`: the config loader reads the record off the default export before it merges named exports into it (that merge is a spread, which drops the record as it drops the mark). `objectstack validate` and `objectstack build` add it to their `conversions` list right after the config loads. Their own conversion pass still runs, and still reports what it converts on a key merged in from a named export of the config module, which `defineStack` never saw. The `--json` envelope keeps its shape (`valid` / `success`, `errors`, `warnings`, `conversions`): `conversions` now lists each conversion once. + + **What a CI job sees:** `objectstack validate --strict` and `objectstack validate --json --strict` now exit 1 for a config whose only advisory is a live conversion, which is what `--strict` ("treat warnings as errors") documents. Without `--strict` the exit stays 0. The fix is the one the notice names: author the canonical spelling it prints, for example `subtitle` instead of `description` on a `page:header` component. The text face lists the conversion in its warning block. The stderr line from `defineStack` is unchanged and still printed once per conversion. + + Clause-②: yes (widening) — one new export, `stackConversionsOf`, on the spec package root. Nothing `objectstack build`, or `objectstack validate` without `--strict`, accepted before is refused. `--strict` now applies its documented meaning to the conversions a `defineStack` config carries. It does not add a new rule. +- 05cb2bc: fix(spec,driver-turso)!: a turso config that forces `mode: 'local'` beside a `syncUrl` is refused where it is written and when the driver is built, instead of running as an embedded replica under a `local` label + + Clause-②: yes (narrowing) — the accept set of the `turso` `datasource.config` contract narrows by one combination. No key is added, removed or renamed, and no exported symbol moves. + + A `syncUrl` names the remote an embedded replica syncs with. A config that forced `mode: 'local'` on a `file:` url (or `:memory:`) beside a non-empty `syncUrl` was accepted by `@objectstack/spec`'s `TursoConfigSchema`, by the published mirror in `@objectstack/driver-turso`, and by `new TursoDriver()`. Measured on the driver source before this change, with a client that counts syncs: it constructed with `transportMode` `'local'`, then synced on connect, started the sync interval, and `isSyncEnabled()` answered `true` — exactly what the same config with no `mode` (a replica) did. A datasource declared local was kept in sync with a remote, and only a label said otherwise. + + **BREAKING** accept-set narrowing on a published schema and a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, at both doors together, with one message whose prescription names both ways out: + + - **at authoring**, as one `custom` issue on `mode` (`config.mode` on a datasource): `DatasourceSchema`, `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service; + - **at construction**, `VALIDATION_ERROR` / 400 from `new TursoDriver()` (and `createTursoDriver()`), before any client or database is opened. + + The message is the same text at both doors, and a test holds the constructor's copy equal to the schema's issue byte for byte. It is the twin of the forced `mode: 'replica'`-without-`syncUrl` refusal, the other way round: honouring `mode: 'local'` by skipping the sync would ignore a declared `syncUrl` instead, which is the same defect with the keys swapped. The sibling refusals keep their order: a forced local mode on a remote url or a bare path still meets its `url` refusal first. An empty `syncUrl` is unset and is still accepted. The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode: this refusal reaches it only as byte-identical text, and the spec contract and the constructor are the two doors that judge it. + + ### Migration: FROM → TO + + | You wrote | Write instead | + | --- | --- | + | `url: 'file:./data/replica.db', mode: 'local', syncUrl: 'libsql://my-db.turso.io'` | an embedded replica: drop `mode` (`url` and `syncUrl` select the replica) | + | the same | a plain local database: drop `syncUrl` (and `sync`), keeping `url: 'file:./data/app.db'` with or without `mode: 'local'` | + + A datasource row stored in this shape is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted and synced with the remote under a `local` label. The way out is the table above. + + Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors the shape, and no host default or environment variable sets `mode` or `syncUrl` (a turso `mode` reaches the driver only from an authored `datasource.config`). Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero. + + +- 1a75e39: fix(spec,drivers): a `datetime` filter `$lte '9999-12-31'`, or a `$between` whose maximum is that day, includes the whole last supported day on every backend (#20600) + + Clause-②: yes (widening) — three new exports on `@objectstack/spec` (`data`) and `@objectstack/core`: the constant `UNBOUNDED_ABOVE`, its type `UnboundedAbove` and the guard `isUnboundedAbove`; `nextUtcCalendarDay` answers the constant for one input that used to answer a string. Nothing any door accepted before is refused, and nothing is removed or renamed. + + **BREAKING for TypeScript and JavaScript callers of `nextUtcCalendarDay`** (`@objectstack/spec/data`, re-exported by `@objectstack/core`): its return type gains a member and its answer for one input changes from a string to a symbol, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). No filter an author writes and no stored row changes meaning except that a whole-day upper bound on `9999-12-31` now includes that day. + + `9999-12-31` is the last day of the supported years (0001..9999). A bare-day upper bound on a `datetime` field — `$lte`, a `$between` maximum, an analytics `dateRange` end — means that whole day, and is compiled as "before the next day's midnight". That day has no next day with a `YYYY-MM-DD` spelling: `nextUtcCalendarDay('9999-12-31')` answered the five-digit `'10000-01-01'`, which sorts below `'2026-…'` as text. So on SQLite, where a `datetime` column is ISO text, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered no rows; PostgreSQL parsed the bound as an instant and answered them. The memory and mongo drivers, the analytics strategies and the draft preview built their bound from the same answer, and `formula`'s RLS `check` evaluator compared a `'2026-…'` value against it and denied the write. + + Every supported value is at most the last millisecond of `9999-12-31`, so that day's whole-day bound bounds nothing. `nextUtcCalendarDay('9999-12-31')` now answers `UNBOUNDED_ABOVE`, a symbol that is neither `null` ("not a calendar day", which would compile the day's midnight and miss the rest of it) nor a string, and every backend compiles no upper bound for it: + + - `$lte` / `<=` on that day asks only that the value is not null: `IS NOT NULL` on the SQL drivers and the analytics echo, `$ne: null` on the memory and mongo drivers. + - A `$between` / `between` whose maximum is that day, and an explicit analytics `dateRange` ending on it, keep only their minimum. + - The type-blind `formula` `check` evaluator and the draft preview admit every value that denotes an instant, and compare any other value as written. + - `$gte`, `$gt`, `$lt` and `$eq` on that day are unchanged: they anchor to its midnight, as on every other day. `9999-12-30` and every earlier day compile the same bound as before. + + Measured through `POST /api/v1/data/:object/query`, rows at `2026-07-15T14:00Z`, `9999-12-30T10:00Z`, `9999-12-31T00:00Z`, `T10:00Z` and `T23:59:59.999Z`: on SQLite, `$lte '9999-12-31'` and `$between ['2026-01-01', '9999-12-31']` answered none of them and now answer all five; `$between ['9999-12-31', '9999-12-31']` answered none and now answers the three on that day. PostgreSQL 16 answers the same before and after. `$lte '9999-12-30'` answers the first two rows on both, before and after. + + **If your code stops compiling.** `nextUtcCalendarDay` now returns `string | UnboundedAbove | null`, where `UnboundedAbove` is a `symbol` with a structural brand. TypeScript refuses that member in a template literal (TS2731), a relational comparison (TS2469) and a `string` parameter (TS2345), so code that used the answer as a day string no longer compiles until it handles the last day. Test the answer with `isUnboundedAbove(answer)` (or `typeof answer === 'symbol'`) first: on its false branch the answer is `string | null` as before, and on its true branch there is no upper bound to compile. `answer === UNBOUNDED_ABOVE` compares correctly but does not narrow, because the branded type is not a unit type. The type is structural on purpose: `@objectstack/spec` ships `./data` as `index.d.mts` and `index.d.ts`, and a `unique symbol` would be two unrelated types in a program that meets both. + + **If your JavaScript code handled the answer as text.** For `'9999-12-31'` it is now a registered symbol (`Symbol.for('objectstack.calendarDay.unboundedAbove')`), not `'10000-01-01'`: a template literal or a relational comparison on it throws a `TypeError`, and better-sqlite3 and `pg` refuse to bind it. Every other input answers exactly as before. + + The shared temporal conformance kit (`TEMPORAL_ROWS` / `TEMPORAL_CASES` in `@objectstack/spec/data`) gains the row `z_last` (`9999-12-31T10:00:00.000Z`) and five last-day cases, so every backend it drives is held to this answer; three existing `$gte` / `$gt` cases now also expect `z_last`. + + +- d830d71: feat(spec): writing a retired key now fails `tsc` with an error that says the key was retired and where its migration is printed (#20621) + + Clause-②: yes + + + + **What an author sees.** Every key a `retiredKey()` tombstone declares used to be typed `undefined`, so writing one failed `tsc` with an error that named no retirement: + + ``` + error TS2322: Type 'string[]' is not assignable to type 'undefined'. + ``` + + Upgrading authors read those errors as typing bugs. The same line now fails like this, for arrays, objects and primitive values alike: + + ``` + error TS2741: Property ''[REMOVED] Key retired: run `os validate` for its migration.'' is missing in type 'string[]' but required in type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'. + error TS2322: Type 'string' is not assignable to type '{ '[REMOVED] Key retired: run `os validate` for its migration.': never; }'. + ``` + + A hover on the key shows the same text. `os validate` and the parse print the key's own prescription, which says what replaced the key and gives the one-line fix. + + **What changed.** The declared type of each tombstoned key, on both the input side (`z.input`, the bare `X` aliases) and the parsed side (`z.infer`, the `XParsed` aliases), is now `` { '[REMOVED] Key retired: run `os validate` for its migration.': never } | undefined `` instead of `undefined`. No value can have that object type, because its one property is typed `never`. So `tsc` still accepts only absence, as before. Both sides carry the same type, which keeps the ADR-0122 isomorphism pins true. + + **What did not change.** Runtime behaviour is the same. Each tombstone is still `z.never().optional()`. The parse error and its prescription, the text `os validate` prints, the ADR-0087 conversions, the JSON schemas and the authorable-surface artifacts are all unchanged. No export was added or removed. + + **BREAKING**: a read of a tombstoned key into a slot typed `undefined`, or typed with the key's old type, no longer compiles, because the key's declared type is now its tombstone mark. The key never holds a value, so delete the dead read. It ships as `minor` under the launch-window convention. + + **Who might notice.** Code that assigns a tombstoned key's value to a slot typed exactly `undefined` (for example `const x: undefined = page.assignedProfiles`) no longer compiles. The key never holds a value, so delete the read. The published declaration files are about 7.5% larger, because the declaration emitter writes the type out at every site of the 287 `retiredKey()` calls. +- 1ab9892: feat(spec)!: retire an analytics cube's `refreshKey` — the refresh cadence and data-change probe nothing read (#20637) + + **BREAKING** — `refreshKey` on an analytics cube (`CubeSchema`), with its `every` and `sql`, is now refused at parse: nothing ever read it, and no analytics result is cached, so a declared refresh cadence refreshed nothing. Delete the key. Every analytics query is computed when it is asked, as it always was. A refresh cadence is declared again when a result cache exists. + + Clause-②: no (narrowing) + + Measured before removal: `git grep refreshKey` over the non-test sources of `packages/services`, `packages/drivers` and `packages/rest` answered 0 lines (4 for the neighbouring `.public` in the same pathspec). `@objectstack/service-analytics` references no cache or job service; its one cache is request-scoped (dimension labels). The one in-repo author was the showcase app (`every: '1 hour'`), which no longer writes it. + + **Removed rather than enforced** (ADR-0049 enforce-or-remove; the maintainer's ruling on the card, letter C): a result cache keyed by cube, query, read scope and tenant is a subsystem with its own design, and a key that does nothing until then is the residue ADR-0049 removes. `sql` also had no safe seam: raw SQL on a schedule, outside the read scope every other cube `sql` goes through. + + ## FROM → TO + + | you wrote (17.5 and earlier) | write instead | + | --- | --- | + | `refreshKey: { every: '1 hour' }` | nothing — delete the key | + | `refreshKey: { sql: 'SELECT MAX(updated_at) FROM orders' }` | nothing — delete the key | + | `refreshKey: { every: '1 hour', sql: '…' }` | nothing — delete the key | + + **The one-line fix:** delete `refreshKey` from every cube. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`Cube` types the key `never`), and the parse — `defineCube()`, `defineStack({ analyticsCubes })`, `PUT /api/v1/meta/analytics_cube/:name` — refuses it at `refreshKey` with the prescription: + + > `analytics_cube.refreshKey` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing read it: no analytics result is cached, so neither `every` nor `sql` ever refreshed anything. Delete the key; every analytics query is computed when it is asked. A refresh cadence is declared again when a result cache exists. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. + + ## The retirement kit + + - **A `retiredKey()` tombstone** on `CubeSchema`, a `strictObject` — so the refusal carries the prescription rather than a bare unknown-key report, and `tsc` fails first. The nested `every` / `sql` shape is gone with it. `RETIRED_KEYS_BY_MAJOR[18]`: `data/Cube:refreshKey`. The key had no default, so no retired-default residue is owed. + - **The D2 conversion `cube-refresh-key-removed`** (protocol 18, retired from the load path) deletes the whole block from every `analyticsCubes[]` entry, one notice per cube, as a lossless delete. A built artifact or a stored `analytics_cube` row that carries it loads through the rehydration seams, which replay it. + - **The D3 entry `cube-refresh-key-retired`** asks the author whether anything they built assumed cube results were cached or refreshed on a schedule. They never were. + - **Ledger:** the `refreshKey.every` / `refreshKey.sql` rows collapse into one `dead` tombstone row. + - **No deprecation window**, per the project's startup-stage posture. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. + + +- fbec216: feat(spec)!: the ADR-0087 migration chain leaves the package root for the new `@objectstack/spec/migrations` entry (#20646) + + **BREAKING** — the migration chain and change-manifest names (ADR-0087 D3/D4), with their types, are no longer exported from the package root `@objectstack/spec`. They are exported, unchanged, from the new entry `@objectstack/spec/migrations`. + + A `major`-class change — an existing import path stops resolving for these names — recorded as `minor` under the launch-window convention. + + **Why.** The migration registry is mostly the guidance text `objectstack migrate meta` prints, and the root re-exported it. The registry does work when its module loads (the list of majors and each step's rationale are computed then), so no bundler could prove it unused, and all of that text rode in every bundle of the root, whatever the consumer imported. This is the source-side payback of the Studio console's first-screen ceiling raise that the maintainer ruled on the 17.5.0 upgrade. Measured on the splitting PR against its merge base `1a75e39d4a` (tsup build, gzip -9): + + | | before | after | + | --- | --- | --- | + | `dist/index.js` (CommonJS root) | 3,780,033 B / 1,067,061 B gzip | 2,009,810 B / 565,386 B gzip | + | `dist/browser/index.mjs` (the ESM root a browser bundler pulls) | 3,764,293 B / 1,065,388 B gzip | 1,994,748 B / 563,787 B gzip | + | a browser bundle of the ten names the Studio console imports from the root (rolldown, minified) | 700,884 B gzip | 301,287 B gzip | + + The ADR-0087 **conversion layer stays on the root**: `defineStack` and `normalizeStackInput` read it at run time, so its names (`ALL_CONVERSIONS`, `CONVERSIONS_BY_MAJOR`, `applyConversions`, `applyConversionsToFlow`, `applyConversionsToStoredItem`, `collectConversionNotices`, the `CONVERSION_*_CODE` constants and their types) import from `@objectstack/spec` exactly as before. + + ### FROM → TO + + | removed from `@objectstack/spec` | import instead from | + | --- | --- | + | `MIGRATIONS_BY_MAJOR`, `MIGRATION_MAJORS`, `MIGRATION_SUPPORT_FLOOR` | `@objectstack/spec/migrations` | + | `RETIRED_KEYS_BY_MAJOR`, `RETIRED_DEFS_BY_MAJOR` | `@objectstack/spec/migrations` | + | `applyMetaMigrations`, `composeMigrationChain`, `MigrationFloorError` | `@objectstack/spec/migrations` | + | `composeSpecChanges`, `composeReleaseChanges` | `@objectstack/spec/migrations` | + | `SpecChangesSchema`, `SpecConvertedSchema`, `SpecMigratedSchema`, `SpecSurfaceAddSchema`, `SpecSurfaceRemoveSchema`, `SpecReleaseChangesSchema`, `SpecReleaseSurfaceSchema` | `@objectstack/spec/migrations` | + | types `MigrationStep`, `MigrationApplication`, `MigrationChainResult`, `MigrationHopResult`, `MigrationTodo`, `SemanticMigration`, `SpecChanges`, `SpecConverted`, `SpecMigrated`, `SpecSurfaceAdd`, `SpecSurfaceRemove`, `SpecReleaseChanges`, `SpecReleaseSurface`, `SurfaceDiff`, `ReleaseSurfaceDiff`, `PreviousReleaseRegistries` | `@objectstack/spec/migrations` | + + **The one-line fix: change the import path.** + + ```ts + // before + import { applyMetaMigrations, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec'; + // after + import { applyMetaMigrations, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec/migrations'; + ``` + + The compiler finds every site: `TS2305` ("Module '"@objectstack/spec"' has no exported member …"); at run time the binding is `undefined`. Nothing else changes: the chain, its steps and semantic entries, the retired-key and retired-def tables and the change-manifest schemas are the same objects, and `objectstack migrate meta` replays the same chain. + + ⚠️ **Out-of-repo consumers are NOT MEASURED beyond objectui.** Inside this repository the moved names had five importers — `os migrate meta` (the only runtime one) and four tests — all moved in the same PR. objectui at the pinned `.objectui-sha` imports none of the moved names from anywhere. The `cloud` repository was not measured. + + The ADR-0087 D3 semantic entry `migrations-entry-split` carries the judgement: an import path is TypeScript source, not metadata, so there is no source a D2 conversion could rewrite. + + Clause-②: yes (narrowing) + + +- 35587f7: feat(spec): a dataset answer declares its base object as `object` (#20647) + + Clause-②: yes (widening) + + `AnalyticsResult` (`@objectstack/spec/contracts`) gains one optional member, + `object?: string`, and `AnalyticsResultResponseSchema` (`@objectstack/spec/api`) + mirrors it on `data`. It is the base object of the dataset the answer was + computed from, by machine name. Nothing is removed or renamed, and no existing + member changes meaning. + + **For a consumer.** Code typed against `AnalyticsResult` can read `object` from a + `queryDataset` answer without a cast, and a parse with + `AnalyticsResultResponseSchema` keeps `data.object` where it used to strip it. The + contract asks every dataset answer to carry it, whatever dimensions are selected + and whether or not rows came back. A cube query answer has no dataset behind it + and carries none. + + **Producers.** This release declares the member. `@objectstack/service-analytics` + sets it on every dataset answer once #20644 lands. +- 1940afd: fix(spec): `translateDashboard` lets an explicit override beat the packaged catalog when it is handed the packaged base (#20680) + + `translateDashboard` (`@objectstack/spec/system`) now follows the rule ADR-0029 D9.2a records for objects: an explicit override beats a packaged default. When the caller supplies `packagedBase` (the dashboard as its code package ships it, before any tenant overlay), a catalog string replaces a served string only while that string still equals its packaged counterpart. The comparison is made per string: the dashboard `label` and `description`, each widget's `title`, `description` and sub-caption (`options.description`), each global filter's `label`, and each static option `label`. Each string is matched by the key the bundle addresses it by (widget `id`, filter key, option value). A widget, filter or option that the packaged base does not carry counts as authored, so its strings keep their values. + + Why: an org overlay on a packaged dashboard (ADR-0126 Regime O) published, and `?layers=true` reported it as effective, but the served widget title stayed the shipped one whenever the dashboard's bundle carried that title. The platform's `system_overview` is one such dashboard, because `platform-objects` ships an `en` bundle that repeats every widget title. The catalog translated the packaged declaration and was applied over the tenant's edit. + + What changes for a caller: + + - `translateDashboard`'s third parameter is typed `TranslateDocumentOptions` (was `ResolveOptions`). That type is `ResolveOptions` plus the optional `packagedBase`, and `translateMetadataDocument` already passed it through. Every existing call compiles unchanged. + - Without `packagedBase` (`undefined` or `null`), the output is byte-identical to before: the catalog applies. No serving layer in this release passes a dashboard base yet, so no served dashboard changes answer with this package alone. + - An edit back to exactly the shipped string is a no-op: the catalog still translates it. + + Nothing is removed or renamed, and there is nothing to migrate. +- f5c7b2c: The `object-grid` page block now declares `description`, `emptyState` and `keyboardNavigation`, so a page that authors them validates clean instead of having each reported as a prop the block does not declare (#20694). + + Clause-②: yes (widening) + + - **`description`** — an `I18nLabel` (a string, or an inline locale map): one line of help text the grid draws above its rows, resolved against the display locale. + - **`emptyState`** — `{ title?, message?, icon? }`, what the grid draws instead of an empty table. It is the list view's own empty-state shape, now exported as `EmptyStateSchema` (author type `EmptyState`) and taken by reference on both `list-view` and `object-grid`, so the two cannot drift apart. + - **`keyboardNavigation`** — a boolean, marked `[EXPERIMENTAL — not enforced]`: arrow-key cell navigation on the WAI-ARIA grid pattern, on by default when `editable` is set. No renderer reads it yet, so authoring it changes nothing today. + + Nothing that parsed before is refused now. The `object-grid` row still refuses every key it does not declare, and the list view's `emptyState` accepts exactly the values it accepted before the shape was extracted. One refusal message is reworded: an `action` or `button` key inside an empty state is still refused and still points at the list view's `addRecord` block, now phrased so that it also reads true on `object-grid`, which has no add-record block. + + On `object-grid`, write `emptyState.title` and `emptyState.message` as plain strings for now: the grid renderer draws both as they are and does not yet resolve an inline locale map there the way it resolves `description`, so a map in either member fails to render. + + Where it surfaces: the component-props gate (`os validate`, `os build`, `os lint`) no longer reports these three keys on an `object-grid` node, and the published JSON Schema for `ObjectGridProps` describes them. +- 6afccda: feat(spec): a semantic migration names the D2 conversions whose applied edits it judges + + Clause-②: yes (widening) + + `SemanticMigration`, the ADR-0087 D3 entry type exported by `@objectstack/spec`, + gains one optional member, `conversionIds?: readonly string[]`. It lists the ids + of the D2 conversions whose applied edits the entry judges. Each id is the same + `conversionId` that the conversion's `MigrationApplication` rows carry, so a + printer of an `applyMetaMigrations` result can show the entry beside those edits + for review. The chain copies the field onto the entry's `MigrationTodo`, so + `objectstack migrate meta --json` shows it on that todo. Nothing is removed or + renamed, and every entry is still reported as a todo of its hop. + + One link ships: `flow-decision-edge-branching-first-match` judges the + `mode: 'inclusive'` edits that `flow-decision-mode-inclusive-explicit` writes. +- 671d4c1: feat(spec): a dataset answer declares its four drill-through sidecars (#20700) + + Clause-②: yes (widening) + + `AnalyticsResult` (`@objectstack/spec/contracts`) gains four optional members, and + `AnalyticsResultResponseSchema` (`@objectstack/spec/api`) mirrors them on `data`: + `dimensionFields` (drillable dimension name to its field), `drillRawRows` (each + row's stored grouped values, aligned to `rows`), `drillRawTotals` (the same for + `totals`) and `drillRanges` (each row's date-bucket range, `[gte, lt)`). Nothing is + removed or renamed, and no existing member changes meaning. + + `@objectstack/service-analytics` already sets them on a drillable `queryDataset` + answer. Code typed against `AnalyticsResult` can now read them without a cast, and + a parse with `AnalyticsResultResponseSchema` keeps them where it used to strip them. +- 9ad6544: fix(spec): `translateView` lets an explicit override beat the packaged catalog when it is handed the packaged view (#20731) + + `translateView` (`@objectstack/spec/system`) now follows the rule ADR-0029 D9.2a records for objects, and that `translateDashboard` already follows: an explicit override beats a packaged default. When the caller supplies `packagedBase` (the view as its code package ships it, before any tenant overlay), a catalog string replaces a served string only while that string still equals its packaged counterpart. It is the same comparison, not a second one. + + The comparison is made per string: the view `label` and `description`, each bulk-action def's `label`, `confirmText` and `confirmLabel`, and each of its params' `label`, `help` and `placeholder`. A def or param is matched by the `name` the bundle addresses it by, and one that the packaged view does not carry counts as authored, so its strings keep their values. + + Why: an org overlay on a packaged view (ADR-0126 Regime O) changed the label of the showcase's `showcase_task.in_progress` and published. The metadata protocol's item and list reads served the edit, but a `zh-CN` reader was served `进行中`, the catalog's translation of the label the package shipped. The catalog (`objects.._views.`) translated the packaged view and was applied over the tenant's edit. + + What changes for a caller: + + - `translateView`'s third parameter is typed `TranslateDocumentOptions` (was `ResolveOptions`). That type is `ResolveOptions` plus the optional `packagedBase`, and `translateMetadataDocument` already passed it through. Every existing call compiles unchanged. + - Without `packagedBase` (`undefined` or `null`), the output is byte-identical to before: the catalog applies. No serving layer in this release passes a view base yet, so no served view changes answer with this package alone. + - An edit back to exactly the shipped string is a no-op: the catalog still translates it. A label written as an inline locale map is not an override either; only a string is compared. + + Nothing is removed or renamed, and there is nothing to migrate. +- c9c182e: fix(spec)!: a `time` value carries no zone — `ClockTimeValueSchema` refuses a `Z` or a UTC offset, so a `time` field default, an action param default or a submitted `time` action param with one is refused when it is authored or submitted, not on every insert that falls back to it + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows the `time` stored form (`valueSchemaFor({ type: 'time' })`) to the zone-less wall clock `HH:MM[:SS[.fraction]]` that the record validator already enforces on write (ADR-0053 D-C1). It ships as `minor` under the launch-window convention for accept-set narrowings; the breaking-ness is carried by this banner and the ADR-0087 disposition above. + + Refused now, where they parsed before: + + - `FieldSchema`: a `time` field's literal `defaultValue` with a zone (`'10:00Z'`, `'10:00+08:00'`). Before, it parsed and each insert that fell back to it was refused `400 VALIDATION_FAILED` / `invalid_time` on a field the caller never sent. + - `ActionParamSchema`: a `time` param's literal `defaultValue` with a zone. + - `validateActionParams` (the action dispatcher, ADR-0104 D2): a submitted `time` param value with a zone, now `invalid_shape`. + + ## FROM → TO + + | you wrote | write instead | + | --- | --- | + | `defaultValue: '10:00Z'` or `'10:00+00:00'` | `defaultValue: '10:00'` | + | `defaultValue: '10:00+08:00'` | the wall clock you meant, `'10:00'` or `'02:00'`, or a `datetime` field for an instant | + + **The one-line fix:** drop the `Z` or offset from every `time` value, or use a `datetime` field. + + **Stored metadata.** The D2 conversion `time-default-utc-suffix-dropped` (retired from the load path) drops a `Z` or a zero offset from a stored `time` default on a field or on an action param typed `time`, so such a row loads canonical. It leaves a non-zero offset as stored and reports it as a TODO naming the field or param, which `os migrate meta --stored` lists; the row keeps loading, fails the schema wherever it is parsed, and needs the rewrite by hand. The D3 entry `time-default-zone-refused` carries that judgement. + + **Unchanged:** a zone-less wall clock, the `NOW()` token and expression defaults on a `time` field, and every `date` and `datetime` value. The repo census found no shipped `time` default with a zone. +- f10d802: feat(spec)!: retire a page header's `breadcrumb` switch — no renderer ever drew a trail for it (#20758) + + **BREAKING** — `breadcrumb` on a `page:header` component (`PageHeaderProps`) is retired, with its `true` default: no renderer ever drew a trail for it. objectui drew an empty slot that nothing filled, and the console draws the navigation trail once, in the app shell's header. Delete the key, whether it was `true` or `false`. The shell's trail is unchanged. + + Clause-②: no (narrowing) + + Measured before removal: objectui's `PageHeaderRenderer` reads the key only to draw an empty `div[data-page-breadcrumb-slot]`, and nothing fills it. The one producer is objectui's Studio page-block inspector ("Show breadcrumb"), so stored pages may carry either value. The one in-repo author found was the published `objectstack-ui` skill's record-page example. No example app authors it. The `nav:breadcrumb` component type is not part of this retirement: the Studio page palette still offers it. + + ## FROM → TO + + | you wrote (17.5 and earlier) | write instead | + | --- | --- | + | `{ type: 'page:header', properties: { title, breadcrumb: true } }` | `{ type: 'page:header', properties: { title } }` | + | `{ type: 'page:header', properties: { title, breadcrumb: false } }` | `{ type: 'page:header', properties: { title } }` | + + **The one-line fix:** delete `breadcrumb` from every `page:header`'s `properties`. + + **What an author who still writes it sees.** A page is never refused for it. A page component's `properties` is an open bag, so `definePage()`, `defineStack({ pages })` and the page write door accept the page as before. `os validate` / `os build` / `os lint` report the key as a warning at `properties.breadcrumb`, with the prescription: + + > `page:header` property `breadcrumb` was removed in @objectstack/spec 17 (ADR-0087 D2) — no renderer ever drew a trail for it: objectui drew an empty slot and nothing filled it, and the navigation trail is drawn once, by the app shell's header. Delete the key, whether it was `true` or `false`; the shell's trail is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + A typed `PageHeaderProps` input fails `tsc` at the key. + + ## The retirement kit + + - **A `retiredKey()` tombstone** on `PageHeaderProps`, a `strictObject`, beside the `icon` that row lost at 17. `RETIRED_KEYS_BY_MAJOR[18]`: `ui/PageHeaderProps:breadcrumb`. No retired-default residue stage is owed: the `true` default was never written into a built artifact, because a page parses its component `properties` as an open bag and only the advisory props lint reads this row. + - **The D2 conversion `page-header-breadcrumb-removed`** (protocol 18, retired from the load path) deletes the key from every `page:header`, `true` and `false` alike, with one notice per header. It reaches headers in regions, nested in a container's `children`, and in a slotted page's named slots. A stored `page` row or a built artifact that carries the key loads through the rehydration seams, which replay it. + - **The D3 entry `page-header-breadcrumb-retired`**: a header that said `false` reads as absent after the strip, so it shows the empty slot's spacing again until the renderer stops drawing the slot. + - **No deprecation window**, per the project's startup-stage posture. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. + + +- b280546: A caller-supplied value for a `formula` field is stripped on every engine write path, in every context, and reported through `droppedFields` / `onFieldsDropped` under a new `reason`, `computed`; and `ObjectQL.validate` runs the write's own field doors, so a dry run built on it predicts what the write will do (#20805). + + Clause-②: yes (narrowing) + + + + **BREAKING**: `ObjectQL.validate` now refuses a row that carries a key the object does not declare, exactly as `insert` and `update` refuse it: the call throws `INVALID_FIELD` / 400 naming the field. It used to answer `valid: true` for that row while the write it previews refused it, so the protocol's `validateData` and the import dry run built on it said "ok" for rows the commit then failed. It ships as `minor` under the launch-window convention; the widening half is the new `reason` arm. + + **A formula value is stripped, never refused.** A `formula` field is computed on every read and no driver has a column for it, so a full read returns the key and a record written back carries it: a form save, a flow's `update_record`, a `GET` then `PUT`. The key used to reach the driver, and the driver decided. On SQL drivers the whole write failed with the driver's own error (`SqliteError` "table … has no column named …", with no `status` and no `field`; the REST door relabelled it `400 INVALID_FIELD` "Unknown field" for a field the object declares). On the in-memory driver the value was stored as a shadow column nothing reads. Now the engine takes the value out before the defaults, the hooks and the other strips, completes the write, and reports one `{ reason: 'computed' }` event per call. That holds on `insert` (one row or a batch), `insertMany`, and `update` by id and by predicate, on every driver, and in every context, `isSystem` included: there is no column for any caller's value to land in. Measured on SQLite and the in-memory driver through `protocol.createData`, `POST /api/v1/data/:object`, `PATCH /api/v1/data/:object/:id` and `engine.update`: each now answers success with `droppedFields: [{ fields: ['doubled'], reason: 'computed' }]`, the stored row carries no such key, and the read still returns the computed value. + + - **`computed` is not `readonly`.** `isSystem` exempts the static `readonly` strip and does not exempt this one. A `formula` field also declared `readonly: true` is reported once, as `computed`. + - **`strictReadonlyWrites` refuses it.** That option's coverage is derived from what `onFieldsDropped` reports, so a caller that passes it and sends a formula value now gets `ERR_READONLY_FIELD_REJECTED` with a `computed` drop in `drops`, and nothing is written, `isSystem` included. The refusal message names the reason and its remedy; a refusal without a `computed` drop reads exactly as before. + - **Hooks are handed the payload that will be stored.** A `beforeInsert` / `beforeUpdate` hook no longer sees the formula key in `ctx.input.data`; `ctx.submitted` on update still carries the caller's submission as sent. + - **Consumers of `DroppedFieldsEvent['reason']` must handle `computed`.** The contract requires a branch on `reason` to be exhaustive. In this release the strict refusal message (`@objectstack/objectql`) and the flow `create_record` / `update_record` step warning (`@objectstack/service-automation`) word it. + + **What `validate` runs now.** Before judging a row, `ObjectQL.validate` runs the write's own doors, by the same functions the write calls: the declared-field door (the refusal above), the computed-field strip, and the caller-write strips, under the write's `isSystem` gate (on `insert` mode the runtime-owned strip and the static `readonly` strip with its re-default; on `update` mode the static `readonly` strip, where a supplied `id` is the address the write binds and is never judged). What the write would drop is reported through a new optional `onFieldsDropped` listener on `validate`'s options, in the same events the write emits. One consequence for verdicts: a reference field declared static `readonly` is now stripped in the preview as it is on the write, so a validation rule that reads through it answers the same on both. + + **Unchanged.** A `summary` field keeps its column: a caller-supplied roll-up value is still stored as sent and overwritten by the next write of a child record. The REST layer's own handling of a missing column (schema drift) is unchanged. +- 975b248: fix(objectql,spec)!: a `groupBy` on a multi-value field and a `count_distinct` on a JSON-stored field are refused with `INVALID_FIELD` / 400 at the engine's `aggregate`, on every driver, and the aggregate × field-type table stops accepting `count_distinct` over the JSON-stored types + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, in two positions, on every driver and for every caller that reaches the engine (the REST query door, a flow or hook, and the analytics strategy that lowers a cube query onto `engine.aggregate`). Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + - A `groupBy` entry that names a **multi-value** field: an inherently-multi option type (`multiselect`, `checkboxes`, `tags`), or a `select`, `lookup`, `user`, `file` or `image` field declared `multiple: true`. Both entry spellings are judged, the field name and the `{ field }` object. + - A `count_distinct` aggregation over a **JSON-stored** field: a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), an inherently-multi option type, or a multi-capable field declared `multiple: true`. + + **BREAKING** (`@objectstack/spec`): `AGGREGATE_FIELD_TYPE_COMPATIBILITY.count_distinct` no longer lists the ten JSON-stored types (the structured-JSON seven and `multiselect`, `checkboxes`, `tags`), so `isAggregateCompatibleWithFieldType('count_distinct', type)` answers `false` for them. Every reader of the table refuses those pairs now: the dataset-measure lint rule (`measure-aggregate-field-type-refused`, run by `os validate` and at a runtime dataset save), the analytics dataset compile leg (`400 DATASET_INVALID`), and the engine door above. The `count` row is unchanged. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`groupBy[0]`, `groupBy[0].field`, or `aggregations[0].field`), the field and its declaration, saying the query was not run, and naming the route inside the first 500 characters the REST door keeps. For a multi-value field the route is to filter by one member: `where` with `$contains` on the field, one query per member. For a structured-JSON field it is to store the part you count in a field of its own, or to count rows with `count`. The thrown error carries `field`, `fields`, `object` and `param` (`groupBy` or `aggregations`). + + **Why a refusal.** Every SQL driver stores these values in a JSON column, and the drivers share no meaning for one as a group key or a distinct key. Measured through `POST /api/v1/data/:object/query` over three rows: grouping by any of the eight multi-value declarations answered one group per array on the in-memory driver, one group per serialized array on SQLite, and 500 `DATABASE_ERROR` on PostgreSQL 16. `count_distinct` over any structured-JSON or multi-value field answered 3 on the in-memory driver (equal values counted apart), 2 on SQLite (serialized text compared), and 500 on PostgreSQL (no equality operator for `json`). No example app and no published stack groups by a multi-value field or counts one distinct, so no meaning is defined for either here. + + **What to write instead.** A dataset measure or a query that counted a JSON-stored field distinct: use `count` over it, or store the scalar part you meant to count in a field of its own and `count_distinct` that field. A grouping by a multi-value field: filter by each member with `$contains` and count. + + **Who is affected.** A caller that grouped by a multi-value field, or counted a JSON-stored field distinct, on the in-memory driver or on SQLite and read the answer as a real one; on PostgreSQL both were already a 500. A dataset whose measure pairs `count_distinct` with a JSON-stored field is refused by the lint rule and the compile leg. + + **Unchanged.** (Two shapes the structured-JSON `groupBy` entry of this same release lists as unchanged are narrowed here: a `multiple: true` select as a group key, and `count_distinct` over a structured-JSON field. This entry is the later word on both.) A `groupBy` or `count_distinct` on a scalar-stored field, a single-value `select` or `lookup` included; `count` over any field; the `having`, filter and sort positions; and an undeclared name, which the REST door answers `INVALID_FIELD` as unknown before the engine is reached. + + `@objectstack/lint`: the dataset-measure refusal's hint no longer says `count_distinct` accepts every type. + + `@objectstack/service-analytics`: the dataset compile leg's refusal of a `count_distinct` measure over a JSON-stored field says why it diverges (the drivers compare the values for equality three ways) and prescribes `count`, or a scalar field for the part being counted; its other refusals no longer say `count_distinct` accepts every type. +- f750119: The `grouping` prop of the `object-grid` and `object-kanban` page blocks is now judged by the list view's own `GroupingConfigSchema`, so a padded grouping field name or a wrong-shaped value is refused on these blocks exactly as it is on `list-view` (#20831). + + Clause-②: yes (narrowing) + + + + **BREAKING**: `ComponentPropsMap['object-grid'].grouping` and `ComponentPropsMap['object-kanban'].grouping` were `z.unknown()`, so any value parsed. Both now take `GroupingConfigSchema` by reference: `{ fields: [{ field, order?, collapsed? }, ...] }`, at least one entry, each `field` the stored name with no leading or trailing whitespace. Both renderers already read exactly that shape: the grid groups by every `grouping.fields[i].field` and reads `order` / `collapsed`, and the kanban board takes `grouping.fields[0].field` as its swimlane field when no `swimlaneField` is authored. A value outside it validated green before and rendered one `(empty)` group on the grid, or one swimlane holding every card on the board, with no error. + + What is refused now, and the one-line fix for each: + + | Authored `grouping` | Refused as | Write instead | + | --- | --- | --- | + | `{ fields: [{ field: ' business_unit ' }] }` | `custom` at `grouping.fields.0.field`, naming the received value | `{ fields: [{ field: 'business_unit' }] }` | + | `'business_unit'` (a bare string) | `invalid_type` at `grouping` | `{ fields: [{ field: 'business_unit' }] }` | + | `42`, `true`, or any other non-object | `invalid_type` at `grouping` | delete the key; it never grouped anything | + | `{ fields: [] }` | `too_small` at `grouping.fields` | delete the key | + | `{ fields: [...], showCounts: true }` (an undeclared key) | `unrecognized_keys` at `grouping` | delete the undeclared key | + + On `object-kanban`, an authored `swimlaneField` still wins over `grouping`; when both are set, deleting `grouping` is the whole migration. A fully-spelled grouping parses byte-identically; a short entry `{ field }` parses clean and gains the list view's defaults (`order: 'asc'`, `collapsed: false`), which is how the grid already read it. + + Where it surfaces: the component-props gate reports a refused value as a `component-props-invalid` finding at the offending path on `os validate`, `os build` and `os lint` (advisory, as every finding of that gate is). A page component's `properties` are not parsed on the metadata save path, so a stored page keeps loading and rendering as it does today until its source is fixed; the ADR-0087 semantic entry `ui-object-block-grouping-config-typed` carries the same table for `os migrate meta`. + + The published JSON Schemas for `ObjectGridProps` and `ObjectKanbanProps` now describe the `grouping` shape; the non-padded field-name rule is a runtime refinement the JSON Schema does not express, recorded for both as `grouping.fields.element.field` in `dropped-refinements.baseline.json`, beside the same site on every list-view schema. +- 32d3b3c: feat(spec): `PackageSchema.visibility` defaults to `org` (was `private`), the create-time default every publish path already produced + + Clause-②: yes + + `PackageSchema` (`@objectstack/spec/marketplace`) filled an omitted `visibility` with + `private`, but no path that creates a package ever reached that value: the cloud control + plane gives a new package `org` when the create request omits the key, and + `os package publish` used to send `org` itself. The declared default now matches what the + runtime does: `org`, which makes a package published from one environment installable in + the owner organization's other environments. + + - **What changes:** `PackageSchema.parse(row)` on a row with no `visibility` now returns + `visibility: 'org'`. A row that names `private`, `org` or `marketplace` is read exactly + as before, and any other value is still refused. + - **What does not change:** the accepted values, and `CreatePackageRequestSchema.visibility`, + which stays optional with no default. A create request that omits the key reaches the + control plane without it, and the control plane's default applies. + - **If you relied on the old default:** pass `visibility: 'private'` explicitly. +- bee75ce: feat(spec)!: a form view's subform columns are the inline grid column contract, and a column that declares no `type` is judged as the type it renders (#20901) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on two published authoring surfaces, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. The console's master-detail grid reads one column shape from two carriers: a relationship field's `inlineColumns` and a form view's `subforms[].columns`. Only the first was judged, and only by the type a column declares. + + **`@objectstack/spec`** + + - **`FormViewSchema.subforms[].columns`** now references `InlineGridColumnSchema`, the strict, name-keyed column a relationship field's `inlineColumns` already takes. It was `z.array(z.any())`, so every column published clean, including a key the grid never reads and a key the other carrier refuses. Every rule the column schema holds now applies on the form view too, with its own message: an unknown key is named; the retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`; a column without `name` is refused; `scale` on a column declaring `type: 'currency'` is refused with the currency ruling's remedy. This reaches `view.form` and every `view.formViews` entry, wherever a view is parsed against the spec: `defineStack`, `objectstack validate`, and the `view` metadata type's registered schema (`ViewMetadataSchema`). + - **`defineStack`'s cross-reference check** now judges a column that declares no `type` as the type it renders. The console fills such a column's type from the child field, so an identity-only column over a `currency` field renders as a currency column. The check resolves the child field, re-parses the column with that type through `InlineGridColumnSchema`, and reports that schema's own refusal (`STACK_CROSS_REFERENCE_INVALID`, 422). Today that means `scale` on an identity-only column over a `currency` child field. Both carriers are walked: `inlineColumns` resolves against the object that owns the relationship field, and `subforms[].columns` against the subform's `childObject`. A child object the stack does not declare, or a column naming no field of it, is not judged. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `subforms: [{ childObject: 'invoice_line', columns: [{ field: 'quantity' }] }]` | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'quantity' }] }]` | + | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency', scale: 2 }] }]` | `subforms: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency' }] }]` | + | `columns: [{ name: 'amount', scale: 2 }]` where `amount` is a `currency` field of the child object (either carrier) | `columns: [{ name: 'amount' }]` | + | a column carrying a key the column schema does not declare | the column without that key | + + The one-line fix: write each form-view subform column as `{ name, … }` using only the keys a relationship field's `inlineColumns` accepts, and delete `scale` from any column that renders as a currency column, whether it declares `type: 'currency'` or takes it from a `currency` child field. Nothing replaces `scale` there: the currency's ISO 4217 minor unit decides the displayed decimals. + + ## Who is affected, measured + + On `origin/main` `cb4c31dd52`: zero authored `subforms` in the repository, and one authored `inlineColumns` block (the showcase invoice, seven identity-only columns, none carrying `scale`). No example, template or test fixture outside this change's own pins changes verdict. Deployed metadata was not measured. +- 31c3996: Clause-②: no + + The field form offers `useGrouping` on `number` fields: one plain boolean row beside `scale`, gated to `number` as `scale` is, whose control copies the field form's `allowCreate` row (the same `z.boolean().optional()` node, no default). The key was declared by `FieldSchema` and graded `live` by the liveness ledger once the console's number display began to answer an authored value first, but no form offered it, so an author's only door was the Source tab. The help text follows the key's own description: unset lets the renderer decide, off never groups (a year or an ID), on always groups. It also says that an untouched switch writes nothing, so it reads off even where the renderer groups. + + ⛔ **No schema accept set moves and no export changes.** What changes is the **form payload** `getMetaTypes()` serves and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles, whose two new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills. +- 95555e7: The import row report and the validate-only answer can now say which fields a write drops. `ImportRowResultSchema` and each `ValidateDataResponseSchema.results[]` row gain an optional `droppedFields`: an array of `DroppedFieldsEventSchema`, the engine's own strip event. So the reason vocabulary is the engine's (`readonly`, `readonly_when`, `primary_key`, `computed`), and there is no second enum. The row still succeeds: `ok`, `action` and `valid` are unchanged. A server that does not produce the report omits the key, so an absent key alone does not prove nothing was dropped. + + `ImportRowResultSchema` also declares `warnings`, which the REST import dry run already serves: the findings the validate verdict admits, in the `ValidateDataIssue` shape, on an ok dry-run row. Until now `ImportRowResultSchema.parse` stripped the key, and readers typed by the spec could not see it. + + `ImportJobResultsSchema.results` now says what an async reader gets: a capped sample, failures first. An ok row's `droppedFields` or `warnings` reaches that reader only if the row falls inside the sample. The cap is unchanged. + + A consumer that branches on `reason` must stay exhaustive over `DroppedFieldsEvent['reason']`. The known exhaustive consumer is objectui's write-warning toast table, `STRIPPED_LINE` in `packages/app-shell/src/providers/writeWarningToast.ts`, which covers all four reasons today. A reader that renders the import or preview report should word `reason` through that table rather than a second one. When the union widens again, the table keyed by it fails type-check on the missing reason, while a second table would fall behind without a sound. +- a29a0ea: feat(spec)!: an `object-master-detail-form` block's detail entries are a strict shape, and their columns are the inline grid column contract (#20928) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. The console's master-detail grid reads one column shape from three carriers: a relationship field's `inlineColumns`, a form view's `subforms[].columns`, and an `object-master-detail-form` page block's `details[].columns`. The first two were judged; the third was `z.array(z.unknown())`. + + **`@objectstack/spec`** + + - **`ComponentPropsMap['object-master-detail-form'].details`** is now an array of strict detail entries: `childObject` (required), `relationshipField`, `columns`, `formFields`, `inlineMode` (`grid` | `form`), `amountField`, `sortField`, `totalField`, `title`, `minRows`, `maxRows` and `addLabel` — the keys the console's `MasterDetailForm` reads off an entry. An unknown key is named, with a rename for the near-misses a form view's `subforms[]` entry also answers (`foreignKey` → `relationshipField`, `object` → `childObject`, …). + - **`details[].columns`** references `InlineGridColumnSchema`, the strict, name-keyed column the other two carriers take. Every rule the column schema holds applies here too, with its own message: an unknown key is named; the retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`; a column without `name` is refused; `scale` on a column declaring `type: 'currency'` is refused with the currency ruling's remedy. Page-component `properties` is read by the component-props gate, so `objectstack validate`, `build` and `lint` report these as advisory `component-props-unknown-key` / `component-props-invalid` findings; a stored page still saves and loads, because `properties` is not parsed on the metadata save or load path. + - **`defineStack`'s cross-reference check** now reaches the block wherever a page carries it (a region, a container's children, a slot) and judges a detail column that declares no `type` as the type it renders, as it already does on the other two carriers: an identity-only column over a `currency` field of the entry's `childObject` that carries `scale` is refused with the column schema's own message (`STACK_CROSS_REFERENCE_INVALID`, 422). A child object the stack does not declare, a column naming no field of it, and a column the column schema refuses on its own (left to the component-props gate) are not judged there. + - **New type `ObjectMasterDetailFormPropsParsed`** — the post-parse shape of `ObjectMasterDetailFormProps`. The two now differ, because a column's `readonlyWhen` / `requiredWhen` bare-string predicate normalizes to an Expression envelope at parse. + + **`@objectstack/lint`** + + - **`field-no-consumers`** reads an `object-master-detail-form` detail entry as the child collection it is: a column `name`, `amountField` and `relationshipField` credit the field of the entry's `childObject`, `totalField` the parent's, and an entry with no `columns` credits the columns the child derives. A child field drawn only by a master-detail block's grid was reported inert. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `details: [{ title: 'Lines' }]` | `details: [{ title: 'Lines', childObject: 'invoice_line' }]` | + | `details: [{ childObject: 'invoice_line', columns: ['product', 'quantity'] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'product' }, { name: 'quantity' }] }]` | + | `details: [{ childObject: 'invoice_line', columns: [{ field: 'quantity' }] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'quantity' }] }]` | + | `details: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency', scale: 2 }] }]` | `details: [{ childObject: 'invoice_line', columns: [{ name: 'amount', type: 'currency' }] }]` | + | `columns: [{ name: 'amount', scale: 2 }]` where `amount` is a `currency` field of the entry's `childObject` | `columns: [{ name: 'amount' }]` | + | a detail entry or column carrying a key its shape does not declare | the entry or column without that key | + + The one-line fix: give every detail entry its `childObject`, write each column as `{ name, … }` using only the keys a relationship field's `inlineColumns` accepts, and delete `scale` from any column that renders as a currency column, whether it declares `type: 'currency'` or takes it from a `currency` child field. Nothing replaces `scale` there: the currency's ISO 4217 minor unit decides the displayed decimals. + + ## Who is affected, measured + + On `origin/main` `ebdb6f2aca`: one authored `object-master-detail-form` block in the examples (the showcase project workspace, one entry `{ title, childObject, addLabel }`, no columns), which parses unchanged, and one documentation example whose three bare-string columns are rewritten as `{ name }` columns in this change. Zero `field`-keyed detail columns. Deployed metadata was not measured. +- 83480c6: `ISecurityService` (`@objectstack/spec/contracts`) gains an optional `getQueryableFields(object, context)`: the field names field-level security lets the caller filter, sort, group or aggregate by on the object, the query-side twin of `getReadableFields` (#20935). + + Clause-②: yes (widening) + + - It is the exact complement of the fields the engine's field guards refuse when a query names them as a filter, a sort key, a group key or an aggregate input. It is a subset of `getReadableFields`, and the two differ by exactly the fields the caller is served masked: a field whose `maskingRule` applies to the caller is readable (served, its value replaced) and not queryable. + - It fails soft like `getReadableFields`: `undefined` means no answer, `[]` means no field is queryable. A system context gets every field. + - It is optional. A consumer checks `typeof svc.getQueryableFields === 'function'`. When the method is missing, or answers `undefined`, the consumer must treat every field that declares a `maskingRule` as not queryable, whoever the caller is. Falling back to `getReadableFields` alone would admit exactly the masked fields. +- 5d5e679: feat(spec)!: an analytics cube member's `sql` is a column reference — a SQL expression there is refused at parse, and a derived value is declared on an ADR-0021 dataset (#20943) + + Clause-②: yes (narrowing) + + **BREAKING** — shipped as `minor` under the launch-window convention + (`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by + this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, + never by the level). + + `MetricSchema.sql` and `DimensionSchema.sql` — the `sql` of every member in an + analytics cube's `measures` and `dimensions` — admit a column reference only: a + field of the cube's object (`amount`), a relationship path of bare identifiers + ending in one (`account.amount`, `account.owner.region`), or `'*'` for a count. + Any other value — a `CASE WHEN …`, an aggregate or a ratio of aggregates, a quoted + or `$`-prefixed spelling, an empty string — is refused at parse with a + prescription. This is ADR-0021's "zero raw SQL / zero raw expressions" carried + from the dataset layer to the cube members it compiles to (maintainer ruling D on + the card): an expression names no single field, so no platform check can judge + which fields it reads, and the two analytics strategies never agreed on it — the + raw-SQL path ran it verbatim and the ObjectQL path refused it. The rule is a + `pattern` in the published JSON Schema too, so a document validated against + `json-schema/**` is judged as the parse judges it. + + ## FROM → TO + + A derived value moves to an ADR-0021 dataset over the same object. A conditional + count or sum is a dataset measure with its own structured `filter`; a ratio, sum, + difference or product of measures is `derived: { op, of: [...] }` over measures + named in the same dataset. + + ``` + FROM defineCube({ name: 'delivery', sql: 'task', measures: { + done_rate: { label: 'Done Rate (%)', type: 'number', + sql: "SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)" }, + } }) + -> parsed; the expression ran verbatim on one strategy and was refused on the other + TO -> ZodError at measures.done_rate.sql (invalid_format): `measures..sql` is a + column reference: a field of the cube's object (`amount`), a relationship path ending + in one (`account.amount`), or `'*'` for a count. A SQL expression there was retired … + + defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', + dimensions: [/* … */], + measures: [ + { name: 'task_count', aggregate: 'count' }, + { name: 'done_count', aggregate: 'count', filter: { status: 'done' } }, + { name: 'done_rate', derived: { op: 'ratio', of: ['done_count', 'task_count'] }, format: '0.0%' }, + ] }) + ``` + + **Mind the scale.** A `derived` ratio is a 0–1 fraction. An expression that + multiplied by 100 returned percentage points; pair the ratio with a `%` numeral + pattern (the server marks a ratio column's percent scale as a fraction) and + re-check any consumer that read the old number raw. + + **A dimension that bucketed a column with a CASE expression** has no expression + form in the cube layer or the dataset layer: group by the column itself, or keep + the bucket as a field of the object and name that field. + + **The one-line fix:** parse each cube; every refusal at `…sql` is one member to + move — replace it with the column it aggregates, or move the derived value to a + dataset measure as above, and point the dashboards, reports and queries that named + `.` at the dataset measure. + + **What an author who still writes it sees.** `CubeSchema`, `defineCube()`, + `defineStack({ analyticsCubes })` (`STACK_SCHEMA_INVALID` / 422) and the + `analytics_cube` write door refuse the member at its `sql` path with the + prescription. `tsc` does not: the key's type is still `string`. + + ## The retirement kit + + - **Schema.** `MetricSchema.sql` / `DimensionSchema.sql` carry the pattern and + their prescriptions (`data/analytics.zod.ts`). A column reference parses + byte-identically to before. The retired metric `filters` guidance and the + analytics query's `filters` guidance no longer offer "fold the condition into + the metric's own `sql` expression" as a live channel; the `metric-filters-removed` + conversion summary and its D3 entry and step-18 rationale fragment say the same. + - **ADR-0087.** The D3 entry `cube-member-sql-expression-retired`, with its + step-18 rationale fragment. No D2 conversion — an expression has no mechanical + rewrite into a dataset — and no `RETIRED_KEYS_BY_MAJOR` row: no key left the + shape, so the authorable-surface, api-surface and JSON-schema manifest + ratchets are unchanged. + - **Liveness.** The `analytics_cube` ledger rows `measures.sql` and + `dimensions.sql` stay `live`, re-verified, with the narrowing recorded. + - **Docs.** The `data/analytics` reference page is regenerated. + - **Example.** The showcase cube's `done_rate` expression member moves to the + `showcase_task_metrics` dataset as `done_count` (a count filtered on + `status: 'done'`) and `done_rate` (`ratio` over `done_count` and `task_count`, + format `0.0%`). + - **`@objectstack/service-analytics`** (README only): its query-body section no + longer tells a reader to fold a per-metric condition into the metric's own + `sql` expression. The runtime is unchanged: its expression branches remain for + a cube that reaches the service without meeting the parse, and their deletion + is a separate change. + + ## Reach, measured + + - This repository: one authored expression member (the showcase `done_rate`), + moved here. Test fixtures in `@objectstack/service-analytics` that build + expression members WITHOUT the parse keep exercising the runtime's expression + branches, unchanged. + - Out-of-repo authored cubes: NOT MEASURED. + + +- e07566b: `deriveInlineGridColumns` (`@objectstack/spec/data`) derives the default columns of an inline master-detail grid, and `field-no-consumers` stops calling two kinds of in-use child field "inert" (#20951). + + Clause-②: yes (widening) + + - **`@objectstack/spec`.** New exports from `@objectstack/spec/data`: `deriveInlineGridColumns(def, { relationshipField?, exclude?, maxColumns? })`, its element type `DerivedInlineGridColumn`, and `DEFAULT_MAX_INLINE_GRID_COLUMNS` (`6`). The function answers which child fields an inline grid draws when its author listed no columns: a relationship field with `inlineEdit` and no `inlineColumns`, or a `subforms` entry with no `columns`. It returns identity-only entries (`{ name }`, plus `defaultHidden: true` on columns past the visible budget, which collapse into the column chooser and are never dropped), in the child's field order, skipping system, audit, tenancy, ownership and sort-position names, the relationship field, `system` / `readonly` / `hidden` fields and the types a grid cell cannot edit. It is the renderer's current rule, reproduced exactly; the renderer hydrates each column from the child field. No schema accepts anything new or refuses anything new. + - **`@objectstack/lint`.** `field-no-consumers` now reads a `subforms` entry's `amountField` and `relationshipField` against the entry's `childObject`, and keeps `totalField` on the parent. It also credits the columns of a derived inline grid through `deriveInlineGridColumns`. Before, `os validate` warned that the child's summed amount column, the subform's relationship field and every derived grid column were inert, and credited a same-named parent field in the amount column's place. A field the derivation leaves out (for example a `hidden` one) is still reported. +- 11d28c1: feat(spec)!: a dashboard widget with no dimension declares two or more measures only on a type that renders them — `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / `sankey` are refused at `values` (#20958; objectui#8894 ruling D's principle) + + Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface GAINS two exports: the one constant the rule reads, `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`, and the check itself, `checkDashboardWidgetDimensionlessMeasureArity`, exported so objectui's `.shape` mirror can chain it. + + + + **BREAKING** accept-set narrowing at `dashboard.widgets[].values`, shipped as + `minor` under this repo's launch-window convention for breaking changes + (`check-changeset-no-major` refuses `major` while the window is open, so + breaking-ness is carried by this banner and by the ADR-0087 disposition above, + never by the bump level). The prescription is registered under protocol major 18 + as `dashboard-widget-dimensionless-multi-measure-refused`. + + **What was wrong.** Outside the metric family, `DashboardWidgetSchema.values` + (`z.array(z.string()).min(1)`) had no upper bound. Measured on this tree before + the change: `{ type: 'pie', dataset: 'sales', values: ['a', 'b'] }` with no + `dimensions` parsed through `DashboardWidgetSchema`, and so did `donut`, + `funnel`, `scatter`, `radar`, `treemap` and `sankey` — while `bogusProp` on the + same widget was refused by name, the lit control. After it, the same body is + refused at `defineStack`, at `os validate` (which loads through `defineStack`), + and on the metadata save path (`422 INVALID_METADATA`, active and draft). With + nothing to split by, those seven types draw `values[0]`: every measure after it + is queried and dropped on the floor by the renderer. The maintainer's ruling D + (「协议不正确的应该先修改协议」) fixes the protocol where it admits measures a + widget type cannot render; the metric-family narrowing was its first + application, and this is the same principle on the chart types. + + ### Write instead + + | wrote | write instead | + |---|---| + | `{ id: 'mix', type: 'pie', dataset: 'sales', values: ['amount_sum', 'count'] }` (no `dimensions`) | `{ id: 'mix', type: 'table', dataset: 'sales', values: ['amount_sum', 'count'] }` — a row of measures | + | the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure | + | the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['amount_sum'] }` **and** `{ id: 'mix_count', type: 'pie', …, values: ['count'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) | + + No conversion does this for you: whether a dimensionless two-measure pie meant a + table, a bar chart or two pies is an authoring choice. The refusal lands at + `widgets[N].values` as ONE `custom` issue naming the widget's `id`, the number + of measures and the authored `type`, and it lists the types that do render + several measures on a dimensionless widget, read from + `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` (`table`, `pivot`, `bar`, `column`, + `horizontal-bar`, `line`, `area`, `combo`). That constant is the one list — the + check, the refusal text and the `values` doc string read it, and objectui's + mirror is to import it rather than restate it. A type that later gains a + declared multi-measure rendering joins it with no migration. + + **Nothing else moves.** The seven types WITH a dimension, and with one measure, + parse exactly as before; every type in the multi-measure set keeps accepting + any number of measures with no dimension; the metric family's refusal is + unchanged and still ONE issue (this check steps aside for `metric` / `kpi` / + `gauge` / `solid-gauge` / `bullet` and for a typeless widget, which resolves to + `metric`); an empty `values` keeps its `too_small`; a `type` outside + `ChartTypeSchema` reports the type refusal alone. Census at the branch point + (`05be35259`), every tracked `.ts` / `.tsx` / `.js` / `.json` / `.md` / `.mdx`: + 23 widget literals on the seven types, every one with one dimension and one + measure, and 0 dimensionless multi-measure widgets on them; the same scan over + an objectui checkout (`1263e40`) reads 0 as well. +- b3917d9: feat(spec)!: `action:button` / `action:icon` refuse `endpoint` with the rename `ActionSchema` already prescribes — `endpoint` → `target` (#21005) + + **BREAKING** — `endpoint` on an `action:button` or `action:icon` component (`ActionButtonProps`, `ActionIconProps`) is no longer a declared key. `ActionSchema` has always refused `endpoint` with "Did you mean `endpoint` → `target`?", while these two rows accepted it. objectui's console registers its own `api` handler, which reads `target` and never `endpoint`, so an `api` button written with `endpoint` passed the props gate and called nothing. The rows now refuse it with the same rename, read from the one alias table the action and both rows share. Write the endpoint as `target`. + + Clause-②: yes (narrowing) + + ## FROM → TO + + | you wrote (17.5 and earlier) | write instead | + | --- | --- | + | `{ type: 'action:button', properties: { actionType: 'api', endpoint: '/api/v1/x' } }` | `{ type: 'action:button', properties: { actionType: 'api', target: '/api/v1/x' } }` | + | `{ type: 'action:icon', properties: { actionType: 'api', endpoint: '/api/v1/x' } }` | `{ type: 'action:icon', properties: { actionType: 'api', target: '/api/v1/x' } }` | + | `endpoint` on a block with no `actionType` | add `actionType: 'api'` and rename `endpoint` to `target` | + + **The one-line fix:** rename `endpoint` to `target` in the block's `properties`; the value (the URL the `api` action calls) is unchanged. + + **What an author who still writes it sees.** A page is never refused for it: a page component's `properties` is an open bag, so `definePage()`, `defineStack({ pages })` and the page write door accept the page as before. `os validate` / `os build` / `os lint` report `component-props-unknown-key` as a warning at `properties.endpoint`, with the rename "Did you mean `endpoint` → `target`?" — the same clause `ActionSchema` prints. The two rows also stop answering `path` with the edit-distance guess `patch` (the declarative write's field values): `url`, `endpoint`, `path` and `href` all rename to `target`, on the action and on both blocks alike. A typed `ActionButtonProps` / `ActionIconProps` input fails `tsc` at `endpoint`. + + ## The migration kit + + - **The D2 conversion `action-block-endpoint-to-target`** (protocol 18, retired from the load path) renames `endpoint` to `target` on an `action:button` / `action:icon` whose `actionType` is `api`, the one meaning the key declared, with one notice per block. It reaches blocks in regions, nested in a container's `children`, and in a slotted page's named slots, so a stored `page` row or a built artifact that carries the key loads with `target` through the rehydration seams, which replay it. An already-present `target` wins: a twin with the same value is dropped. A block with no `actionType`, another `actionType`, a non-string `endpoint`, or a `target` that names a different endpoint is left as stored and reported as a TODO. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + - **The D3 entry `action-block-endpoint-spelling-retired`** names what the rename cannot decide: the TODO sites above, and code — a custom action handler that read `endpoint` off the action reads nothing once the block carries `target`. + - **No deprecation window**, per the project's startup-stage posture. + + Census at landing: no producer in this repository (examples, templates, platform pages, fixtures) or in objectui's examples authors `endpoint` on either block. ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. + + +- 70dae53: feat(spec): discovery reports which optional `/auth` route families are mounted, starting with the better-auth admin family (`authFamilies.admin`) (#21046) + + Clause-②: yes + + **New key.** `DiscoverySchema` declares an optional `authFamilies` block, `{ admin: boolean }`. `admin` says whether the better-auth admin family (`{routes.auth}/admin/*`: `list-users`, `set-role`, `update-user`, `ban-user`, …) is mounted on this deployment. On a deployment that does not enable the admin plugin those routes answer a plain `404`, the same as a mistyped path, so a caller checks `authFamilies.admin` before building a URL into the family. `@objectstack/spec/api` also exports the block's schema (`AuthFamiliesSchema`, type `AuthFamilies`) and its reader, `readAuthFamilies(authService)`. + + **Same answer as `/auth/config`.** The value is the auth service's own `getPublicConfig().features.admin`, the object `GET /api/v1/auth/config` serves. Both discovery producers read it through `readAuthFamilies`: `getDiscovery()` in `@objectstack/metadata-protocol` (served by `@objectstack/rest` at `GET /api/v1/discovery`) and `getDiscoveryInfo()` in `@objectstack/runtime` (served at `GET /.well-known/objectstack`). Neither re-derives whether the admin plugin is on, so on one boot the two documents and `/auth/config` agree. On a stock boot `authFamilies.admin` is `false`. With the admin plugin on (`plugins.admin: true`, or SCIM, which forces it on) it is `true`. + + **When the key is absent.** A producer that cannot read the answer emits no `authFamilies`, rather than a guessed `false`. That happens when no `auth` service is registered (then `routes.auth` is absent too), when the registered service has no `getPublicConfig()`, or when that call throws (`/auth/config` answers `500 AUTH_CONFIG_ERROR` in that state). Treat an absent block as "not known to be mounted". + + **What did not change.** No existing key, route or status moved. The unmounted admin routes still answer a plain `404`. +- 665cab3: docs(spec)!: the security service contract's field answers for a caller who resolves no permission set exclude the fields that declare `requiredPermissions` (#21063) + + Clause-②: yes (narrowing) + + + + **BREAKING for implementers and consumers of `ISecurityService` field answers.** + + **What changed.** The contract in `@objectstack/spec/contracts` now states the + field answers for a non-system caller who resolves no permission set. Such a + caller holds no permission-set field grant and no capability. No grant narrows + its answers, and a field's own declarations still apply: a field that declares + `requiredPermissions` is not in its `getReadableFields` answer (unless a + `maskingRule` on the field serves it masked, which keeps it as a served + column), and it is not in its `getWritableFields` answer. + `getMetadataReadableFields` answers the same for that caller when the + deployment's fallback set resolves to nothing. The contract used to say the + data-plane answer for that caller was the full field set, because the engine + middleware skipped its whole field gate for it. The middleware skips only its + permission-set grant gates. + + **Who this reaches.** An implementation of `ISecurityService` must answer this + way for that caller. A consumer that relied on the full field set for that + caller now receives the narrower answer from the reference implementation + (`@objectstack/plugin-security`). + + **What to do.** An implementation folds each field's `requiredPermissions` + into its answer for this caller exactly as it does for a caller whose + permission sets lack the capability. A consumer needs no change. +- 62b90d7: fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope, and the security contract says so (#21079) + + Clause-②: yes (narrowing) + + + + **BREAKING for callers who resolve no permission set.** Shipped as `minor` under the launch-window convention. + + **What changed.** ADR-0056 D2 gives an unauthenticated principal the deny baseline, not "no checks", and ADR-0090 D9 gives a guest the `guest` position and nothing else. A non-system caller that carries a principal (a position, a named permission set or a user id) but resolves no permission set was instead admitted to every object no set grants, for reads and writes, and read with the record-sharing predicate as its only row scope. Now an empty set list grants nothing: + + - **Object admission refuses it.** Every engine operation (find, findOne, count, aggregate, insert, update, delete) is refused with `403 PERMISSION_DENIED`, the same refusal any caller gets for an object its sets do not grant. `ISecurityService.canReadObject` and `canExport`, and the write preview's admission, answer `false` for it. + - **Its row scope is the deny filter.** `ISecurityService.getReadFilter` answers the filter that matches zero rows for it, as it already did on a resolution failure. + - **The second principal of a delegated request is held to the same answer.** An agent acting on behalf of a delegator who resolves no permission set was already refused by the engine; `canReadObject`, `canExport` and the write preview now refuse it too. + + The field answers for this caller (`getReadableFields`, `getQueryableFields`, `getWritableFields`, `getMetadataReadableFields`) are unchanged: they are field-level answers, and the contract now says that object admission is not part of them. The `ISecurityService` docblocks in `@objectstack/spec/contracts` that stated the old zero-set admission (`canReadObject`, `canExport`, the metadata-plane field projection) and the deny cases of `getReadFilter` narrow to match. No method, parameter or return type changes. + + **Who this reaches.** + + - An unauthenticated request carried as the guest envelope, on a deployment that grants anonymous callers no permission set. + - A context that names only permission sets the deployment does not register. + - A signed-in user on an embedder that switches the baseline off (`fallbackPermissionSet: null`) and grants that user nothing. + + A context that carries no principal at all (no position, no named set, no user id) is handed through as before; ADR-0096 stages it separately. A caller who resolves at least one permission set is decided by its sets, as before, and so is a system context. The public form submit is unaffected: its declaration-derived grant admits the create and its read-back ahead of object admission. Signed-in users of a stock `objectstack serve` deployment are unaffected: it applies the member baseline to every one of them, so none resolves an empty list. + + **Migration.** A caller that resolves no permission set is refused object admission and reads nothing. An app-declared anonymous endpoint (`authRequired: false`) can no longer read or write objects until the `guest` anchor's bindings are resolved for anonymous callers (#21158). An embedder that sets `fallbackPermissionSet: null` must grant its signed-in users a set explicitly. + + **For implementers of `ISecurityService`.** Answer `canReadObject`, `canExport` and the object-admission half of a write `false`, and `getReadFilter` with your deny filter, for a non-system caller that carries a principal and resolves no permission set; admit only the principal-less context. +- cb45469: fix(service-analytics)!: the analytics native-SQL strategy declines an object an engine middleware is registered for, so the engine serves it and that object's read gates apply; the engine answers which objects carry one (`IObjectQLEngine.hasObjectMiddleware`) (#21080) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the analytics doors serve for one class of query. It ships as `minor` under the launch-window convention for narrowings. + + **What changes.** On a SQL driver, `NativeSQLStrategy` compiled a query to SQL and ran it through the driver's raw-SQL seam, so no engine operation ran and no engine middleware did. It applied the security service's object admission and read filter and nothing else, so the read gates that live in the engine as per-object middlewares did not apply there: a caller admitted to such an object at object level read grouped results and counts over every row, rows about parent records that caller cannot read included. It now declines a query that reads (as its base object, a declared join, or through a relationship path) an object the data engine holds a middleware registered for. The ObjectQL strategy serves it through the engine with the caller's context, so the engine's middlewares run, and the analytics answer for that caller equals the data door's. On the stock composition the objects that move off the native path are `sys_comment`, `sys_activity` and `sys_attachment` (read gates), `sys_approval_request` (the snapshot redaction), and `sys_user_position` and `sys_permission_set` (write-side middlewares, which move as a side effect: a middleware does not declare its operation). No shipped dataset or dashboard reads any of them. + + **What is newly refused.** A query on such an object that the ObjectQL strategy cannot serve is refused with that strategy's existing `400`, where the native strategy used to serve it: for example a dimension reached through a relationship path combined with a measure that cannot be recombined across it (`avg`, `count_distinct`). Correctness wins over the fast path for a gated object. + + **It fails closed.** `AnalyticsServicePlugin` asks the data engine. An engine without `hasObjectMiddleware`, or no engine, cannot say, and the strategy declines then too: every query on such a host is served by the ObjectQL strategy, and the plugin says so once at `warn`. A host that constructs `AnalyticsService` with `executeRawSql` and without the new `hasObjectMiddleware` config member keeps the native path for every object and is told so once at construction. + + **New, additive.** `IObjectQLEngine.hasObjectMiddleware?(objectName): boolean` (`@objectstack/spec`), `ObjectQL.hasObjectMiddleware(objectName)` (`@objectstack/objectql`): whether a `registerMiddleware(fn, { object })` names the object; a global registration (no `object`, or `'*'`) is keyed to none and is not counted. `AnalyticsServiceConfig.hasObjectMiddleware` (`@objectstack/service-analytics`), which the plugin fills from the data engine. + + **Unchanged.** Objects no middleware names keep the native path. The middleware chain, `registerMiddleware` and every gate are unchanged. + + **What to do after upgrading.** Nothing on the stock composition. A host whose `"data"` service is not ObjectQL should implement `hasObjectMiddleware` to keep the native path for ungated objects. A host that builds `AnalyticsService` itself with `executeRawSql` should pass `hasObjectMiddleware` from its engine. +- d6d6e87: feat(spec,client)!: `ActionSchema` gains `outcomeMessages` — success copy per closed handler `outcome`, interpolating `${result.*}` — and `client.environments.delete` no longer guarantees `message` on its archive answer (#21095) + + Clause-②: yes (narrowing) + + + + **BREAKING for TypeScript readers of `client.environments.delete`'s archive answer**: `message` is now `message?: string`. Code that assigns it to a `string` stops compiling at that read. Nothing else in the SDK changes, and the request is unchanged. + + **`ActionSchema.outcomeMessages`** (`@objectstack/spec/ui`). A server-executing action can succeed in more than one way: an environment delete archives, finds the environment already archived, defers a purge, or destroys it. One static `successMessage` cannot say which of these happened. The handler now reports a closed `outcome` fact in its success payload, and the action declares the copy for each outcome: + + ```ts + outcomeMessages: { + archived: 'Environment ${result.environmentId} archived.', + already_archived: 'Environment ${result.environmentId} was already archived.', + } + ``` + + - Keys are snake_case outcome names; values are `I18nLabel`s. A key that is not snake_case is refused at its own path (`invalid_key`). + - The key is valid on `type: 'api'` and `type: 'script'` actions only, the two types with a success payload that can carry an `outcome`. It is refused with a prescription on `url` / `modal` / `flow` / `form`, beside `resultDialog` (which suppresses the success toast), and beside `operation: 'update'` (no handler, so no outcome). + - `successMessage` and each outcome message may interpolate `${result.*}`, the server-response scope `onSuccess.navigate` already declares. This is not a new dialect. + - The console picks `outcomeMessages[result.outcome]`, falls back to `successMessage`, and then to its default text. The console does not read it yet. Until it does, the key is accepted, validated, translated and extracted, and the liveness ledger grades it `planned`, so `os lint` tells an author who writes it that it is not shown yet. + + **Translation.** `TranslationData` carries the copy beside `successMessage`: `objects.OBJECT._actions.ACTION.outcomeMessages.OUTCOME` and `globalActions.ACTION.outcomeMessages.OUTCOME`. Outcome keys there are snake_case too. `translateAction` overlays them per outcome (object-scoped first, then global) and only for outcomes the action declares. `os i18n extract` emits one key per declared outcome. + + **`client.environments.delete`** (`@objectstack/client`). The control plane is replacing its English `message` with the closed `outcome` fact: the server returns facts, and the console composes the message in the user's locale. The archive answer declares `outcome?: 'archived' | 'already_archived' | 'purge_deferred'`, and the teardown answer declares `outcome?: 'destroyed'`. Both `outcome` and `message` are optional, because a 200 may carry either one or both depending on which control-plane release answers. To learn what happened, read `deleted` and `purgeDeferred`, which every answer still carries, or `outcome` when it is present. +- df1feae: `ERROR_CODE_LEDGER['@objectstack/service-automation']` now lists `MAPPING_NOT_FOUND` and `UNSUPPORTED_TRANSFORM`, the two registered codes the connector sync executor (`pullConnectorSource`) stamps onto `ConnectorPullError.code` (#21106). + + Clause-②: yes + + Provenance, not identity. The per-package face of `ERROR_CODE_LEDGER` changes in this release in two steps, and neither changes the `ErrorCode` union, the wire or any HTTP answer: + + - The bulk-import runner, the mapping pipeline and the data-error classification moved out of `@objectstack/rest` (#20919), and each code's row moved to the package that now stamps it. `@objectstack/core` gains `AMBIGUOUS_MATCH`, `BLANK_MATCH_KEY`, `NO_MATCH`, `SUMMARY_RECOMPUTE_FAILED` and `UNSUPPORTED_TRANSFORM`. A new `@objectstack/types` key lists `CONCURRENT_UPDATE`, `ERR_DATASOURCE_UNAVAILABLE` and `UNIQUE_VIOLATION`. `@objectstack/rest` no longer lists those seven, because it stamps none of them now; it keeps `UNSUPPORTED_TRANSFORM`, which it still stamps. + - `@objectstack/service-automation` gains the two rows above. Both codes were already registered, under `@objectstack/rest` (and `UNSUPPORTED_TRANSFORM` under `@objectstack/core` as well). + + So a consumer reading `ERROR_CODE_LEDGER['@objectstack/rest']` sees seven fewer entries, and one reading the `@objectstack/core`, `@objectstack/types` or `@objectstack/service-automation` key sees the new ones. Nothing to migrate: every code keeps its wire value and its status. +- 336e191: feat(spec,cli): shared seam for projecting stored metadata bodies, and the audit rewrite command + + Clause-②: no + + `@objectstack/spec/kernel` gains the family-wide primitives for the + stored-metadata-body security invariant, beside the per-type redactor registry + they build on: `STORED_METADATA_BODY_OBJECTS` / `isStoredMetadataBodyObject`, + the `STORED_METADATA_BODY_COLUMN` / `STORED_METADATA_TYPE_COLUMN` names, and + `redactStoredMetadataBody` / `redactStoredMetadataRow` / `redactStoredMetadataRows`. + These project a stored row's body through the one `getMetadataTypeRedactor` + definition, so every surface that serves, copies or evaluates such a body shares + one rule rather than a copy per package. Additive — no existing export changes. + + `@objectstack/cli` gains `os migrate audit-metadata-bodies`, the one-off rewrite + of at-rest metadata-body copies in `sys_audit_log` / `sys_activity` (dry run by + default, `--apply` to write, idempotent). +- 24c554d: feat(spec)!: a `record:line_items` page block's props are a strict shape, and its columns are the inline grid column contract (#21142) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface: a new `ComponentPropsMap` row judges a props bag nothing judged before. Shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a failing key or column as an advisory `component-props-unknown-key` / `component-props-invalid` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`ComponentPropsMap['record:line_items']`** — new row, `RecordLineItemsProps`. `record:line_items` was the one entry on the string-arm registration ledger (`STRING_ARM_REGISTERED_TYPES`, now empty), so the props gate skipped it as unregistered and any key rode through. The row declares the fifteen keys the console's `LineItemsPanel` reads: `childObject`, `relationshipField` (required), `columns` (required, at least one), `parentObject`, `parentId`, `recordId`, `amountField`, `totalField`, `title`, `readonly`, `minRows`, `maxRows`, `filter` (the ViewFilterRule array), `sort` (the SortItem array) and `limit` (a positive integer). `childObject` may come from the component-level `dataSource` binding instead. An unknown key is named. A near-miss gets its rename (`foreignKey` → `relationshipField`, `filters` → `filter`, …). The four keys of an `object-master-detail-form` detail entry that this block does not read (`addLabel`, `sortField`, `formFields`, `inlineMode`) are refused with the reason. + - **`columns`** references `InlineGridColumnSchema`, the strict, name-keyed column a relationship field's `inlineColumns` takes. The retired `field` spelling (and `fieldName`, `key`) is refused with the prescription naming `name`, and a column without `name` is refused. This block draws a column exactly as declared: it does not hydrate `label`, `type` or `options` from the child object's field, so `defineStack`'s identity-only column check does not reach it. + - **New types `RecordLineItemsProps` and `RecordLineItemsPropsParsed`.** They differ because a column's `readonlyWhen` / `requiredWhen` bare-string predicate normalizes to an Expression envelope at parse. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `columns: [{ field: 'title', label: 'Title' }]` | `columns: [{ name: 'title', label: 'Title' }]` | + | `{ childObject: 'invoice_line', columns: [...] }` with no `relationshipField` | `{ childObject: 'invoice_line', relationshipField: 'invoice', columns: [...] }` | + | `columns: []`, or no `columns` | at least one `{ name, label?, type?, … }` column | + | `addLabel`, `sortField`, `formFields` or `inlineMode` on the block | the block without that key | + | any other key the shape does not declare | the block without that key | + + The one-line fix: key every column `name`, give the block its `relationshipField` and at least one column, and remove any key the shape does not declare. + + ## Who is affected, measured + + On `origin/main` `1ecb871beb`: one authored `record:line_items` block in the examples, the showcase project detail page. All five of its columns were keyed `field`, so its Tasks grid rendered empty cells; they are keyed `name` in this change. No documentation example authors the block. Deployed metadata was not measured. +- 3dc33b2: **BREAKING** — an anonymous public form no longer offers record search. The form field's `publicPicker` block (`view.form.sections[].fields[].publicPicker`: `displayFields`, `maxResults`, `filter`, `object`) is removed, and the anonymous lookup route `GET /api/v1/forms/:slug/lookup/:field` is deleted. A public form's `lookup`, `master_detail` and `user` fields are now always left off its anonymous rendering, whatever the form declares. + + Clause-②: yes (narrowing) + + Retired immediately (ADR-0087 D2), with no alias window: the maintainer's ruling reverses the earlier one that had declared the key. Mainstream web-to-lead forms do not let an anonymous visitor search records either, and no example, template, plugin or first-party UI declared or called the picker. + + ## FROM → TO + + | you wrote (17.5 and earlier) | write instead | + | --- | --- | + | `{ field: 'account', publicPicker: { displayFields: ['name'], maxResults: 10 } }` on a public form | delete the `publicPicker` block — the field is left off the anonymous rendering anyway | + | a public form whose visitors chose from a short, fixed list of records | a `select` field with static `options` listing the choices | + | a public form whose visitors had to pick an existing record | the same form behind sign-in (an internal form), where the lookup field searches with the signed-in user's own access | + | a client calling `GET /api/v1/forms/:slug/lookup/:field` | nothing to call: the path is no longer registered and answers what any unregistered path answers (`404 ENDPOINT_NOT_FOUND`) | + + **The one-line fix:** delete the `publicPicker` block; an anonymous public form no longer offers record search. Use a `select` field with static `options`, or put the form behind sign-in. + + **What an author who still writes it sees.** `tsc` fails at the authoring site (`FormFieldInput` types the key `never`), and the parse — `defineView()`, `defineStack({ views })`, `os validate`, `PUT /api/v1/meta/view/:name` — refuses it at `…sections[N].fields[N].publicPicker` with the prescription: + + > `view.form.sections[].fields[].publicPicker` was removed in @objectstack/spec 17.6.0 (ADR-0087 D2) — an anonymous public form no longer offers record search: lookup, `master_detail` and `user` fields are always left off the anonymous rendering, and the anonymous record-search route (`GET /forms/:slug/lookup/:field`) no longer exists. Delete the key (the whole `publicPicker` block). To let a visitor choose from a fixed list, use a `select` field with static `options`; to let them pick an existing record, put the form behind sign-in. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. + + **What a REST client sees.** The two error codes only that route produced, `LOOKUP_NOT_PUBLIC` and `LOOKUP_TARGET_MISSING`, leave the error-code ledger with it. `GET /api/v1/forms/:slug` and `POST /api/v1/forms/:slug/submit` are unchanged apart from the unconditional strip above. + + ## The retirement kit + + - **A `retiredKey()` tombstone on the form field**, so the parse carries the prescription instead of a bare unknown-key verdict. The block's own schema and its two types go with it: `FormFieldPublicPickerSchema`, `FormFieldPublicPicker` and `FormFieldPublicPickerParsed` are no longer exported, and `ui/FormFieldPublicPicker` is no longer published as a JSON Schema. + - **The D2 conversion `form-field-public-picker-removed`** (protocol 18, retired from the load path) deletes the key from every form field of every form payload — `sections[]`, `groups[]`, top-level `fields[]` and nested rows. Its D3 record is the semantic entry `form-field-public-picker-retired`, which asks the author how a visitor should now choose. + - **`@objectstack/rest`:** the lookup route and its filter-lowering helper are deleted, and the resolve route's strip of lookup / `master_detail` / `user` fields no longer has an opt-in. + - **`@objectstack/lint`:** the preset-comparand rule no longer reads a picker's `filter` (its claiming reader for that position went with the key). + + ## What an operator with a STORED form sees + + A `sys_metadata` view row saved before this release may still carry the key. Nothing breaks at read: the conversion replays on rehydration and strips it, so the view is served canonical and parses, and the field stays off the anonymous rendering either way. `os migrate meta --stored` lists those rows, and `--apply` rewrites them. + + +- 9969228: feat(spec): an object declares which of its fields is the record's picture — `imageField`, beside `nameField` + + Clause-②: yes (widening) + + `ObjectSchema` accepts one more optional key, `imageField`. It names the field + whose value is the record's picture, the way `nameField` names the field that is + the record's name: one object-level declaration, read by the record page header + (the record chrome every record detail page shares) — not a per-page + `page:header` prop. + + ```ts + defineStack({ + objects: [{ + name: 'crm_account', + nameField: 'name', + imageField: 'logo', + fields: { + name: Field.text({ label: 'Name' }), + logo: Field.image({ label: 'Logo' }), + }, + }], + }); + ``` + + - **What it may name.** A field of the same object whose type is `image` or + `avatar`. A name the object does not declare, or a field of any other type + (a `text` URL column, a `file`), is refused at parse with an issue at + `imageField` that names the two accepted types — so `defineStack`, + `ObjectSchema.create()`, `os validate` and the metadata save door + (`422 INVALID_METADATA`) all refuse it. + - **An empty field.** The contract the reader is held to: a record whose + picture field is empty shows no picture — no initials or placeholder in its + place. + - **Who draws it.** No renderer reads the key yet. The record chrome in + `@object-ui/components` is the reader to come, and until it lands an authored + `imageField` is accepted, stored and served but nothing draws it. It takes + effect when that renderer ships, with no re-authoring. The liveness ledger + records the key as `planned`. + + Nothing that parsed before is refused: the key is new, and an object that does + not set it is unchanged. +- 434c6c7: feat(spec)!: an analytics dataset dimension's and measure's `field` is a column reference — a SQL expression there is refused at parse, as it already is on the cube members a dataset compiles to (#21220) + + Clause-②: yes (narrowing) + + **BREAKING** — shipped as `minor` under the launch-window convention + (`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by + this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, + never by the level). + + `DatasetDimensionSchema.field` and `DatasetMeasureSchema.field` — the `field` of + every entry in an ADR-0021 dataset's `dimensions` and `measures` — admit a column + reference only: a field of the dataset's object (`amount`), or a relationship path + of bare identifiers ending in one (`account.amount`, `account.owner.region`); a + measure also admits `'*'` for a count, and a count may still omit `field`. Any + other value — an arithmetic, an aggregate, a `CASE`, a subquery, a function call, + a quoted or `$`-prefixed spelling, a padded or empty string, a broken path — is + refused at `dimensions.N.field` / `measures.N.field` with a prescription, and so + is `'*'` on a dimension. + + Why: the dataset layer was declared to take no raw SQL (ADR-0021 "zero raw SQL / + zero raw expressions") and `field` was documented as a field or a relationship + path, but it was a bare string and parsed anything. The analytics dataset door + already refused an expression `field` on every query (`PERMISSION_DENIED` / 403, + inline or saved), so such a dataset could be saved and never answered — declared, + never enforced (ADR-0049). That door never judged an empty `field`: it skips one, + which is how a `count` measure with `field: ''` kept counting rows on SQLite's + native-SQL path (the D2 repair below). The accept set is the one the cube members a dataset + compiles to already hold: the dataset compiler copies `field` into the member's + `sql` verbatim, and both now read one shared declaration. `'*'` is refused on a + dimension because grouping by every column is no axis — both analytics strategies + answered such a dimension `500`. The rule is a `pattern` in the published JSON + Schema too, so a document validated against `json-schema/**` is judged as the + parse judges it. + + ## FROM → TO + + ``` + FROM defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', + dimensions: [{ name: 'priority', field: 'priority' }], + measures: [ + { name: 'task_count', aggregate: 'count', field: '' }, + { name: 'done_points', aggregate: 'sum', + field: "CASE WHEN status = 'done' THEN points ELSE 0 END" }, + ] }) + -> parsed; the dataset door refused the expression on every query + TO -> ZodError at measures.0.field and measures.1.field (invalid_format): + `measures[].field` is a column reference: a field of the dataset's object … + + defineDataset({ name: 'task_metrics', label: 'Task Metrics', object: 'task', + dimensions: [{ name: 'priority', field: 'priority' }], + measures: [ + { name: 'task_count', aggregate: 'count' }, + { name: 'done_points', aggregate: 'sum', field: 'points', filter: { status: 'done' } }, + ] }) + ``` + + A conditional count or sum is a measure with its own structured `filter`; a + ratio, sum, difference or product of measures is `derived: { op, of: [...] }` + over measures named in the same dataset. **Mind the scale:** a `derived` ratio is + a 0–1 fraction, so an expression that multiplied by 100 returned percentage + points — pair the ratio with a `%` numeral pattern. A dimension that bucketed a + column with an expression has no expression form: group by the column itself, or + keep the bucket as a field of the object and name that field. + + **The one-line fix:** parse each dataset; every refusal at `…field` is one member + to change — name the column, omit `field` on a plain count (never `field: ''`), + or move the computation to a measure `filter` or a `derived` measure. The one + mechanical case is done for you: `os migrate meta --from 17` lists, and every + stored-row rehydration replays, the D2 conversion + `dataset-count-measure-empty-field-removed`, which drops a `count` measure's empty + `field` (it still counts rows). Nothing else has a mechanical rewrite. + + **What an author who still writes it sees.** `DatasetSchema`, `defineStack({ + datasets })` (`STACK_SCHEMA_INVALID` / 422), the `dataset` write door and + `POST /api/v1/analytics/dataset/query` (which parses every dataset it is handed, + inline or saved, and now answers `400 VALIDATION_FAILED` at the path where it + answered `403 PERMISSION_DENIED` before) refuse the member at its `field` path + with the prescription. `tsc` does not: the key's type is still `string`. + + ## The retirement kit + + - **Schema.** `ui/dataset.zod.ts` holds both keys to the pattern; the pattern is + declared once, in the non-public `data/analytics-column-reference.ts`, and the + cube layer's `CUBE_MEMBER_SQL` is that same `RegExp`. A dimension's pattern is + the same column path without the `'*'` arm. A column reference parses + byte-identically to before. + - **ADR-0087.** D2 carries the one lossless repair: the conversion + `dataset-count-measure-empty-field-removed` (`retiredFromLoadPath`, so an author + is refused at parse while stored rows and `os migrate meta` replay it) drops a + `count` measure's `field: ''`, which compiles to `COUNT(*)` without it. The D3 + entry `dataset-member-field-expression-refused`, linked to that conversion and + with its step-18 rationale fragment, carries the rest — a non-count measure or a + dimension with `''` and every expression have no mechanical rewrite into a + column. No `RETIRED_KEYS_BY_MAJOR` row: no key left the shape, so the + authorable-surface, api-surface and JSON-schema manifest ratchets are + unchanged. + - **Liveness.** The `dataset` ledger rows `dimensions.field` and + `measures.field` stay `live`, re-verified, with the narrowing recorded. + - **Docs.** The `ui/dataset` reference page is regenerated. + - **Runtime.** Unchanged: the analytics dataset door's refusal stays as defence + in depth for a dataset that reaches the service without meeting the parse — a + row stored before this change, which the build probe hands over as read. + + ## Reach, measured + + - This repository: no authored dataset carries a non-column `field` — the + examples, `platform-objects`, the hand-written docs and the published skills + were read. Two test fixtures that sent an expression `field` on purpose were + re-pinned: the service door's test builds them unparsed, and the REST route's + test now expects the route's `400`. + - Studio's dataset inspector (objectui) seeds a new dimension or measure row with + `field: ''`. A plain count saved that way parsed before; its query answered + `500` on the ObjectQL path, while SQLite's native-SQL path accepted the + `COUNT()` it compiled to. A row already stored that way is repaired on load by + the D2 conversion above. A NEW save of that shape is refused at the save door + with the prescription to omit the key, because the write path parses with the + current schema and replays no conversion; the producer-side change is + objectui's. + - Out-of-repo authored datasets: NOT MEASURED. + + +- cfa9315: feat(spec, objectql, plugin-security): one shared filter lowering, run once at the engine and RLS seams (ADR-0053 D-D1, amended) + + Clause-②: yes + + `@objectstack/spec/data` exports `lowerFilterCondition(filter, options?)` and its `FilterLoweringOptions` type. It is not exported from the package root entry. It is a pure `FilterCondition → FilterCondition` rewrite that applies three rules once: + + - `$between` becomes `$gte` its minimum and `$lte` its maximum. + - A `$lte` whose comparand is a bare `YYYY-MM-DD` day becomes `$lt` the next day, in the calendar-string domain. On the last supported day (`9999-12-31`) a lone `$lte` becomes `{ $null: false }`, and a `$between` keeps only its minimum. + - The NULL-polarity guards the drivers already compile. A `$ne` of a value, a `$nin` or a `$notContains` holds for a row with no value. Every leaf of a `$not` operand is made total. + + The rewrite is copy-on-write, idempotent and never refuses. A node it rewrites keeps its filter-subtree provenance mark. With `options.isDatetimeColumn` (a typed seam), the first two rules change only a declared `datetime` column. Without it they apply to every column. + + As ADR-0053 D-D1 (amended 2026-09-30) requires, the seams now run it once, after the comparand doors and after filter-token resolution: + + - **`@objectstack/objectql`** runs it on every filter position, typed by the object's declared fields. That covers `where` on `find`, `findOne`, `count`, `update` and `delete`, and `aggregate`'s `where`, `aggregations[i].filter` and `having`. `having` is typed by the aggregated row's columns, so `max` of a `datetime` field counts as a `datetime`. Drivers receive the lowered filter. A date macro such as `{today}` is resolved before the lowering reads it. + - **`@objectstack/plugin-security`** runs it on every compiled RLS policy filter (`using` and `check`), right after the two comparand faces. `SecurityPlugin` now hands the compile seam the object's declared `datetime` columns (`RlsFieldGuard.datetime`). A guard without that set treats no column as `datetime`. + + Row answers stay the same on every driver. Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input. One result changes. The engine evaluates `aggregate`'s `aggregations[i].filter` and `having` itself, and that evaluator now treats a row or group with no value the way every driver's `where` already does. It no longer counts such a row in a `$between` on a `datetime` column. It now keeps such a row under a `$not` over an ordering such as `$lt`. + + Nothing is removed or renamed, and there is nothing to migrate. +- 315888d: feat(spec): one list of page-component slot positions, derived from the component rows and read by every page walk — `page:card`'s `footer` is now walked by all three (#20940) + + The platform has three walks that descend into a page component's `properties` bag, and each kept its own list of where child components hang: the ADR-0087 conversion walker (`children`, `body`, `footer`, `items[].children`), `@objectstack/lint`'s `walkPageComponents` (the same four) and the exported `walkAddressedPageComponents` (`children`, `items[].children`). So a node in a card's `footer` — a declared, rendered slot ("Card footer components (slot)") — was judged by `os lint` and skipped by every consumer of the exported walk: `translatePage` left its copy untranslated, `os i18n extract` offered no key for it, and objectui's validator passed it unjudged. + + **`@objectstack/spec` — new exports `pageComponentSlotPositions()` and `PageComponentSlotPosition` (`@objectstack/spec/ui`).** The component rows now mark each composition slot at its declaration, and `pageComponentSlotPositions()` derives the one list from `ComponentPropsMap`: `children`, `footer` and the panel position `items[].children`, plus the tombstoned `body` flagged `retired: true`. The marker changes nothing about the schema it marks — the parse, the JSON Schema and the authorable surface are unchanged. The list is derived on first call and memoized, never at import. `minor` because the package's public surface grows by these two exports. + + **`walkAddressedPageComponents` descends `properties.footer`.** It reads the list's authorable entries, in the list's order (`children`, `footer`, then `items[].children`); signature and return shape are unchanged. What follows from it: + + - `translatePage` translates the copy of a component in a card footer through `pages..components.`, like any other nested component. + - `os i18n extract` offers those keys, and `os i18n check` counts them, for a stack whose card footers hold components with an `id` and copy. + - objectui's validator, which judges the nodes this walk visits, now judges a card footer's nodes. + + `page:card.body` stays undescended, as #5775 ruled: it is not an authorable spelling. + + **The conversion walker reads every entry, the retired one included.** Its reach does not change: it descends `children`, `body`, `footer` and `items[].children`, as before. Stored documents still carry `body`, the renderers still draw it, and a conversion that runs before `page-card-body-to-children` meets the sub-tree there. Within one component the visit order is now `children`, `body`, `footer`, then the panels. That order is observable only as the order of the notices for a component that carries both a direct slot and panels. + + **`@objectstack/lint` — `walkPageComponents` reads the list's authorable entries.** It walks `footer` as before, and it stops walking the retired `body` spelling. The walk matches by shape, so this drops a `body` array on any component, not only on `page:card`. #5775 (maintainer ruling 2026-08-06, direction A) made `children` the one composition key. The renderers keep reading `body` only as a back-compat fallback for stored documents. On `page:card` the tombstone's rename prescription still refuses `body`, and so does the thin containers' guidance; the sub-tree is judged once it sits under `children`. So the rules built on this walk no longer report findings about nodes under any component's `body` array. The conversion walker keeps reaching them for stored documents. + + **`@objectstack/cli`:** no code change. `os i18n extract` and `os i18n check` pick up the `footer` component keys through the shared walk. The extractor's object-section pass stops reading `record:details` sections under a retired `body`, through lint's walk. + + **Why no ADR-0087 ledger entry.** Nothing an author writes moves: no spec key is retired or renamed, no stored `sys_metadata` shape changes, and no conversion or migration id is touched. `objectstack migrate meta` has nothing to act on. + +### Patch Changes + +- 93d4e0e: docs(spec): the `picklist` field key's description, two doc comments and the refusal of `picklist` with `options` no longer say that the reference is resolved and its options served (#19518) + + Clause-②: no +- 88b484e: feat(objectql): the runtime resolves a field's `picklist` onto its served options, validates writes against the resolved list, and merges `picklistExtensions` additively + + Clause-②: no + + - **Load.** `defineStack({ picklists })` and `defineStack({ picklistExtensions })` now register, from a manifest and from a nested plugin, through the same registration seam as every other collection. The compiled-artifact door registers `picklists` as `picklist` items, so `GET /meta/picklist` serves them on an artifact boot. + - **Merge.** A picklist's options are its own, followed by the options every `picklistExtensions` entry adds. A value the list already carries is refused with `422 INVALID_METADATA`, which names both declarations, whichever of the two registered first. The later declaration never replaces the earlier one. A package that registers again replaces its own extension. Uninstalling a package removes the values it added. + - **Serve.** A field with `picklist: 'NAME'` is served with the resolved options written onto it and `picklist` kept (`PicklistServedFieldSchema`), on every object read, including objects stored in `sys_metadata`. The list's translations (`picklists.NAME.options.VALUE`) relabel those options per request locale. An option marked `default: true` in the list fills an omitted field on insert, as an inline option does, and the import template reads it the same way. + - **Unknown name.** A packaged field that names a picklist no loaded package declares fails the boot at `kernel:ready` with `INVALID_METADATA`, and so does a `picklistExtensions` entry that extends such a list. The error names every such field or extension and the package that declared it. After boot, an artifact registered through the `manifest` service is checked before any of it registers. A field whose list does not resolve is served with no options and accepts no value. + - **Write validation.** The write door judges a picklist-bound field against the resolved options, and its refusal names the picklist. The wire code stays `invalid_option`. The validation message catalog gains three message keys for this (`invalid_option_picklist`, `invalid_option_value_picklist`, `invalid_option_picklist_unresolved`) in en, zh-CN, ja-JP and es-ES. They change the message text only, never the wire. + - **Writing the served body back.** The served body carries `picklist` and `options` together. Writing it back through the metadata door is still refused, with the prescription to drop `options`, as `FieldSchema` declares. Nothing strips it on the write side. + - **Ledger.** `field.picklist`, the `picklist` kind's rows and `translation.picklists` are `live`. `field.picklist` no longer carries `authorWarn`, so `os lint` / `os validate` stop warning an author who writes it. +- f11b5f2: fix(spec): `os migrate meta` guidance for the `actor-*`, `hot-*`, `external-*`, `query-*`, `delete-*`, `etl-*`, `storage-*`, `apimethod-*`, `dashboard-*`, `notification-*`, `record-*`, `runtime-*`, `rls-*` and `scim-*` migration entries states each lesson in words instead of citing tracker numbers + + Clause-②: no + + The ADR-0087 semantic entries of the `actor-*` family (the retired `ctx.user.roles` alias), + the `hot-*` family (the inert `'disk'` / `'distributed'` state strategies and the file-watch + placeholder), the `external-*` family (the retired external-lookup and message-queue + schemas), the `query-*` family (the retired `QueryAST` request members and aggregation + functions), the `delete-*` family (the retired by-id repoint in a `beforeDelete` hook), the + `etl-*` family (the retired ETL pipeline layer), the `storage-*` family (the retired + single-argument `IStorageService.list`), the `apimethod-*` family (the `apiMethods` enum + shrunk to six primitives), the `dashboard-*` family (the `compareTo` offset, the page-only + modal target, the chart-config structure refusal, the single-measure metric tile and the + funnel-only `stageOrder`), the `notification-*` family (the retired inbox cursor), the + `record-*` family (the object-form detail sections and the converged chatter position), the + `runtime-*` family (the retired `HttpServer` wrapper), the `rls-*` family (the refused array + comparand, cross-class field comparison and stored-list ordering in row-level predicates) + and the `scim-*` family (the retired `sys_scim_provider` object) are printed by + `os migrate meta` as the header, `why:` and `verify:` lines of a manual change. Their text + sent the reader to issue-tracker, pull-request and decision-batch numbers — some of which no + longer resolve, and some in another repository — for what a ruling, measurement or fix had + decided; it now says what was decided, in the sentence being read. ADR ids are kept. One + entry of another family is corrected in the same way: `rest-api-endpoint-handler-status-retired` + now names the API skill, whose factual sweep corrected the `handlerStatus` sentence, instead + of the automation skill. + + Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain + rewrites exactly what it rewrote before. No `surface` changes. The generated migration + registry, `spec-changes.json` and the protocol upgrade guide carry the same text. +- 0cb72cf: fix(spec): `os migrate meta` guidance for the two padded list-view field-name entries states the contract-first rule in words, and the notification/embed retirement drops a sweep batch ordinal + + Clause-②: no + + The ADR-0087 semantic entries are printed by `os migrate meta` as the header, `why:` and + `verify:` lines of a manual change. Two of them — + `ui-list-view-grouping-field-padded-refused` and `ui-list-view-groupbyfield-padded-refused` — + explained why a padded field name is refused rather than trimmed by pointing at a rule number + in a contributor guide, a number that names nothing in this repository's guide. Their `why:` + text now states the rule itself: fix the metadata, not the renderer — off-spec metadata is + refused where it is authored, never coerced into working. + + `ui-notification-action-embed-config-retired` named the batch of the v17 unknown-key + strictness sweep that measured the two retired shapes by its ordinal. The sentence already + says what that batch measured and decided, so the ordinal is dropped. + + Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes, and the + chain rewrites exactly what it rewrote before. The generated migration registry, + `spec-changes.json` and the protocol upgrade guide carry the same text. +- c1d8051: fix(spec): `os migrate meta` guidance for twenty-four more migration-entry families — `stack-*`, `evaluated-*`, `aggregation-*`, `authoring-*`, `automation-*`, `cache-*`, `tenant-*`, `client-*`, `spec-*`, `cli-*`, `identity-*`, `import-*`, `tool-*`, `advanced-*`, `cloud-*`, `startup-*`, `sys-*`, `declarative-*`, `sort-*`, `address-*`, `packages-*`, `platform-*`, `session-*` and `strategy-*` — states each lesson in words instead of citing tracker numbers + + Clause-②: no + + The ADR-0087 semantic entries of these twenty-four families are printed by `os migrate meta` + as the header, `why:` and `verify:` lines of a manual change. Their text sent the reader to + issue-tracker, pull-request, decision-batch and summon numbers — some of which no longer + resolve, and some in another repository or a vendor's tracker — for what a ruling, + measurement or fix had decided; it now says what was decided, in the sentence being read. + ADR ids are kept, and so are the rule numbers of this repository's own contributor guide. + + Two entries also carried a tracker number in `surface`, the header line itself: + `authoring-schemas-strict-unknown-keys` now names the unknown-key strictness wave, and + `evaluated-expression-slots-source-required` names the census of engine-evaluated slots. + One sentence is corrected while being rewritten: `cli-command-contribution-retired` said the + `manifest.contributes.commands` tombstone was protocol 17; it is registered under protocol 18. + + Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain + rewrites exactly what it rewrote before. The generated migration registry, + `spec-changes.json` and the protocol upgrade guide carry the same text. +- a918fe7: fix(spec): `os migrate meta` guidance for the remaining migration-entry families states each lesson in words instead of citing tracker numbers + + Clause-②: no + + The ADR-0087 semantic entries are printed by `os migrate meta` as the header, `why:` and + `verify:` lines of a manual change. In the families not yet brought to this line — among them + `turso-*`, `auth-*`, `admin-*`, `ai-*`, `assembled-*`, `change-*`, `device-*`, `epoch-*`, + `incident-*`, `logging-*`, `memory-*`, `send-*`, `standard-*`, `training-*`, `websocket-*`, + `structured-*` and `translation-*` — that text sent the reader to issue-tracker, pull-request, + decision-batch and cross-repository numbers, some of which no longer resolve, for what a + ruling, measurement or fix had decided; it now says what was decided, in the sentence being + read. Verbatim rulings that carried a card or batch number keep only their operative words. + ADR ids are kept, and so are the rule numbers of this repository's own contributor guide. With + this change no semantic entry's printed guidance carries a `#`-numbered tracker id. + + One replacement also named a contributor-guide rule by a number that no longer exists: + `address-location-value-unknown-keys-refused` now states the rule itself — a consumer never + carries an alias for an off-spec key; the metadata is fixed where it is written. + + Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes, and the + chain rewrites exactly what it rewrote before. The generated migration registry, + `spec-changes.json` and the protocol upgrade guide carry the same text. +- 41dcf11: Notes in seven more liveness ledgers cite the commit that decided them, or say the decision in words, instead of a tracker number that no longer resolves + + Clause-②: no + + Notes in the `datasource`, `api`, `query`, `object`, `email_template`, `mapping` and + `webhook` ledgers named GitHub issues that no longer exist, so a reader could not tell why + a row carries its verdict. Each such note now either names the commit that made the + decision or, where the number alone carried the meaning, says what was decided. The + `manifest` ledger's `permissions` note also names the commit that recorded its structured + arm's zero apart. The `liveness/` ledgers ship in this package's tarball, which is why + this is a release note at all. Note text only: no row's status, evidence, proof or date + changes, and no schema, export or runtime behaviour changes. +- c46279f: Notes in twenty-one more liveness ledgers, and one `datasource` evidence string, cite the commit that decided them, or say the decision in words, instead of a tracker number that no longer resolves + + Clause-②: no + + Notes in the `book`, `doc`, `job`, `validation`, `translation`, `hook`, `seed`, `flow`, + `capability`, `qa`, `dashboard`, `action`, `agent`, `skill`, `tool`, `rest_api`, + `route_generation`, `crud_endpoints`, `metadata_endpoints`, `batch_endpoints` and + `analytics_cube` ledgers cited tracker numbers that no longer resolve on GitHub, so a reader + could not tell why a row carries its verdict. Each such note now either names the commit that + made the decision or, where the number alone carried the meaning, says what was decided. The + `datasource` ledger's `ssl.rejectUnauthorized` evidence string cited one such number in its + prose; it now names the commit that made the fix, and its code anchors are unchanged. The + `liveness/` ledgers ship in this package's tarball, which is why this is a release note at all. + Note text and that one evidence parenthesis only: no row's status, proof or date changes, and + no schema, export or runtime behaviour changes. +- 688ddef: Notes in the `app` and `view` liveness ledgers that cited a tracker number which no longer resolves now either cite the commit that decided them or say the decision in words + + Clause-②: no + + Sixteen notes in the `app` and `view` ledgers cited a GitHub issue that no longer exists, so a + reader could not tell why a row carries its verdict. Each such note now either names the commit + that made the decision or, where the number alone carried the meaning, says what was decided. + The `liveness/` ledgers ship in this package's tarball, which is why this is a release note at + all. Note text only: no row's status, evidence, proof, producer or date changes, and no schema, + export or runtime behaviour changes. +- b1aab1e: Notes in the `field` liveness ledger and sentences in the ledger README that cited a tracker number which no longer resolves now either cite a commit in this repository or say in words what the number stood for + + Clause-②: no + + Eleven citations in the `field` ledger's notes and twenty-nine in `liveness/README.md` pointed at + a GitHub issue or pull request that no longer exists, so they led nowhere. Each one now either + names the commit that did or recorded what the number pointed at or, where the number alone + carried the meaning, says that meaning in words. The `liveness/` ledgers and their README ship in + this package's tarball, which is why this is a release note at all. Prose only: no row's status, + evidence, proof, producer or date changes, no README table row or heading is added, removed or + renamed, and no schema, export or runtime behaviour changes. +- 274e162: The `manifest`, `dataset` and `permission` liveness ledgers cite the commit that decided each note, or say the decision in words, instead of a tracker number that no longer resolves + + Clause-②: no + + Notes in these three ledgers named GitHub issues that no longer exist, so a reader could + not tell why a row carries its verdict. Each such note now either names the commit that + made the decision or, where the number alone carried the meaning, says what was decided. The + `liveness/` ledgers ship in this package's tarball, which is why this is a release note at + all. Note text only: no row's status, evidence, proof or date changes, and no schema, + export or runtime behaviour changes. +- 15b586d: Liveness ledger: `connector.actions.description`, `connector.actions.outputSchema` and `app.areas.description` are now `live`, not `dead`. Studio reads each of them at the `.objectui-sha` pin, and each row cites that reader and its producer. Ledger data, two README Notes cells and the regenerated count shards only. ⛔ No schema, parse, `.describe()` or accept-set change. + + The ledgers ship inside this package (`files[]` includes `liveness`), and `@objectstack/lint` reads them to decide which authored keys draw an advisory warning. None of the three rows sets `authorWarn`, so the set of warnings does not change. + + - `connector.actions.description`: the flow designer's Action picker on a `connector_action` node shows each action's description beside its label. + - `connector.actions.outputSchema`: the flow designer offers a `connector_action` node's downstream references from the top-level `properties` of its action's `outputSchema`. + - `app.areas.description`: the Studio app preview lists each area, with its description beneath it when one is authored. + - Both connector rows are fed from the plugin and provider door, as their sibling `actions.*` rows are: the `actions` an author writes on a metadata connector entry never reach the registry the designer reads. + - The regenerated count shards: `connector` has 31 live and 23 dead (was 29 and 25), and `app` has 50 live and 8 dead (was 49 and 9). +- d78a0bd: The `field` liveness ledger grades `useGrouping` `live`, and the key's docblock stops describing a grouping heuristic the renderer does not use + + Clause-②: no + + A number field's authored `useGrouping` is honoured by the console this release builds against: + an authored `true` or `false` decides whether the value renders with thousands separators, and an + absent key keeps the renderer's interim rule. The `liveness/field.json` row therefore moves from + `planned` to `live`, citing the objectui reader and the sites that carry the key to the number + cell, and `liveness/state-counts/field.md` is regenerated to match. The `FieldSchema.useGrouping` + docblock in `src/data/field.zod.ts` said the interim rule looked at a field's `min` / `max` + bounds, and that the renderer half had not landed; both are corrected: the rule reads only a + declared `scale: 0` (ungrouped) against any other `scale` or none (grouped), and the renderer half + reads an authored value first. Text and ledger only: the schema, its `.describe()` string, every + export and all runtime behaviour are unchanged. +- c9d234c: The number-comparand refusal now says "a numeric aggregated column" at `having`, and names PostgreSQL's server error only where a driver actually binds the comparand + + Clause-②: no + + **Two false phrases, at two positions.** At `having`, filtering a `count` / + `sum` / `avg` result (or a groupBy column) against a non-numeric comparand + answered `filter on 'total' compares a declared number field …` — `total` is + the aggregated row's own column, not a declared field of the object; the + verdict is handed the numeric class the column belongs to, which has no + `FieldType` of its own. And at `having` and the per-aggregation `filter`, the + `not-a-number`, `boolean` and `date` clauses each named "(PostgreSQL with a + server error)", a fact about `where`: the engine evaluates both of those + clauses itself, on every driver, before any row is read, so a comparand there + never reaches a driver bind and PostgreSQL never answers it. + + **Measured, unchanged: the per-aggregation `filter`'s column IS a declared + field.** That position narrows the object's RAW rows before any aggregation + runs, against the object's real field map — so its refusal keeps "a declared … + field", exactly as `where`'s does. Only the PostgreSQL clause moves there, + because the engine evaluates that position itself too. + + **FROM** `filter on 'total' compares a declared number field against "abc" at + having.total.$gt, which is not a number: it has no numeric reading, and + backends answer it differently (PostgreSQL with a server error). …` + + **TO** `filter on 'total' compares a numeric aggregated column against "abc" + at having.total.$gt, which is not a number: it has no numeric reading. …` + + The `where` message is unchanged, byte for byte, and so is the accept set: no + comparand that was refused before is now accepted, and none that passed is now + refused. This is a wording fix. + + **What moved to carry it.** `NumberComparandRefusalSite` (`@objectstack/spec`) + gains two optional fields the engine door already knew and now passes along: + `aggregated` (the column is an aggregated-row column, not a declared field — + `having` sets it; `where` and the per-aggregation `filter` do not) and + `boundByDriver` (this position reaches a live driver bind — `where` alone sets + it true; unset defaults to `true`, so a site built before this change, or any + caller who never sets these fields, renders exactly as it always has). + `@objectstack/objectql`'s door passes both explicitly at each of its three + call sites; no second rule and no driver-level change. +- 24d521e: refactor(spec): protocol 18's migration step keeps its `rationale` as key-sorted fragments and derives its `conversionIds` — no value changes (#20535) + + Nothing a consumer reads changes. `MIGRATIONS_BY_MAJOR[18].rationale` (48,953 + characters), `MIGRATIONS_BY_MAJOR[18].conversionIds` (45 ids, same order) and the + whole `MIGRATIONS_BY_MAJOR` value are byte-identical to the previous release, and so + is the rationale `migrate meta` prints for the 17 → 18 hop. + + What changed is how the step is written, so two major-18 retirements can be in + flight at once without conflicting in `packages/spec/src/migrations/registry.ts`: + + - The rationale is `STEP18_RATIONALE`, one `{ id, order, text }` fragment per + retirement, kept sorted by `id` and rendered by `order`, joined with one space. + A retirement adds ONE fragment where its `id` (its D3 semantic entry id) sorts — + never at the end — with `order` one more than the highest present. + - `conversionIds` is read off `CONVERSIONS_BY_MAJOR[18]`, which it copied value + for value. A retirement adds its conversion there only. +- 3a89d45: fix(spec): `nextUtcCalendarDay` and `utcInstantMs` read a bare day in the years 0001..0099 as written, not as 1900..1999 + + `nextUtcCalendarDay` proves a bare `YYYY-MM-DD` is a real day by building it and reading it back. It built the date with `Date.UTC`, which reads a year from 0 to 99 as 1900 + year, so `0050-01-01` came back as `1950-01-01`, the round trip failed, and the helper answered `null` for every day of those years. `utcInstantMs` asks the same round trip about a bare day, so it answered `null` for them too. The date is now built with `setUTCFullYear`, which takes the year as written; an impossible day (`0050-02-30`, `0100-02-29`) is still refused, not rolled over. + + What an author sees: a `datetime` filter `$lte '0050-01-01'`, or a `$between` whose maximum is that day, now includes the whole day, as it already did for `'2026-07-15'`. Before, it stopped at the day's first instant, so a row stored at `0050-01-01T10:00:00.000Z` was missed. Measured through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16: `$lte '0050-01-01'` answered only the row of `0049-12-31` and now also answers the two rows of `0050-01-01`; `$between ['0050-01-01', '0050-01-01']` answered no rows and now answers both. The next day's midnight stays out, and the 2026 control answers the same before and after. The other callers of the two helpers (the memory and mongo drivers, the analytics strategies, the engine's `having` filter and `formula`'s RLS `check` evaluator) import them from this package, so the correction reaches them with no change of their own. +- f379f57: refactor(spec): protocol 18's conversions are authored as identifier-sorted entries with an explicit application order — no value changes (#20574) + + Nothing a consumer reads changes. `CONVERSIONS_BY_MAJOR[18]` (46 conversions, same + order), `ALL_CONVERSIONS` (113, same order) and `MIGRATIONS_BY_MAJOR[18].conversionIds` + are value-identical to the previous release, so the loader and the migration chain + apply the same conversions in the same sequence. + + What changed is how the list is written, so two major-18 retirements can be in flight + at once without conflicting in `packages/spec/src/conversions/registry.ts`: + + - `CONVERSIONS_BY_MAJOR[18]` is read off `MAJOR_18_CONVERSIONS`: one + `{ conversion, order }` entry per conversion, kept sorted by the conversion's + identifier and applied by ascending `order` (ties by the conversion's `id`). A + retirement adds ONE entry where its identifier sorts — never at the end — with + `order` one more than the highest present. + - A new conversion is defined directly above the definition of the entry that follows + it in that list, not at the end of the definitions. +- 7510663: feat(spec): the protocol-18 migration step records the html-tier `div` refusal as the semantic entry `ui-html-page-div-refused` (#20592) + + Clause-②: no + + `MIGRATIONS_BY_MAJOR[18].semantic` gains one entry, `ui-html-page-div-refused`. + It records the narrowing `@objectstack/cli` takes on when its JSX page gate + reaches the SDUI component manifest that `@objectstack/console` ships: a project + with no `sdui.manifest.json` of its own has its `kind: 'html'` pages checked + against that manifest, which does not declare `div`, so `objectstack validate`, + `compile` and `lint` refuse a `div` there (`jsx-forbidden-tag`, + `jsx-unknown-component`). The entry prescribes `box` for a plain wrapper, names + the layout containers to reach for instead only when their layout is wanted, and + says how to prove the rewrite done. + + What moves for a consumer of this package: `MIGRATIONS_BY_MAJOR` carries the + entry, and `objectstack migrate meta` prints it, because its default range + already runs to protocol 18, the highest major with a step. Nothing else does. + The protocol-18 step is not cut yet, so `spec-changes.json` and the protocol + upgrade guide, which project the steps up to the current protocol major, are + unchanged, and no schema accepts or refuses anything it did not before. +- d7631d5: **A `defineStack` or `composeStacks` call that refuses now carries the ADR-0087 conversions it applied on the error it throws, so `stackConversionsOf(error)` reads them off a caught refusal.** + + `defineStack` rewrites a deprecated metadata spelling to its canonical shape before it validates, and records each conversion on the stack it returns (`stackConversionsOf(stack)`). A call that then refused returned no stack, so the conversions it had applied were lost: they reached stderr only, as a warn-once line that a second stack with the same path does not print again. A tool that catches the refusal, such as a `--json` door, had no way to report both the refusal and the retiring spelling. + + - `defineStack` (strict and `strict: false`) stamps the conversions applied so far on every ADR-0112 refusal it throws after its conversion pass: the schema parse, the six cross-field refusals and the bound-action merge's shape refusal. The record is the same `ConversionNotice[]` a built stack carries, under the same symbol key, non-enumerable and frozen. A refusal whose source needed no conversion carries an empty record. + - `composeStacks` stamps its inputs' records on every refusal it throws, by the same rule it uses for the artifact it returns. + - `stackConversionsOf(value)` now also reads the record off such a refusal: `catch (error) { const conversions = stackConversionsOf(error); }`. It still answers `[]` for any other value, including a plain `Error` and a throw that is not one of these refusals. + + Nothing is accepted or refused differently. Each refusal keeps its `code`, `status`, `name`, message and `issues`, and `hasStackProvenance` still answers `false` for it. No export is added. + + Clause-②: no +- ace770d: fix(spec): the `ui-html-page-div-refused` migration entry states when `dev` and `start` compile (#20649) + + Clause-②: no + + The entry's `reason`, which `objectstack migrate meta` prints as its `why:` + line, said `dev` and `start` run `objectstack compile` first. They run it before + they boot only when the artifact is missing or `--compile` is passed, and `dev`'s + watch mode runs it when a watched file changes. The text now says so. No schema + accepts or refuses anything it did not before. +- ed54768: A credential typed as a literal into a flow position that every flow reader is served now draws one `flow-credential-literal` warning at `os validate`, `os build`, `os lint` and the runtime publish gate, and the spec describes of those positions route an outbound credential to a declarative connector's `credentialRef` (#20654). + + Clause-②: no + + **Why.** A flow definition is served, as authored, to every member who can read flows. The flow read path withholds the credential slots the spec declares, but it cannot withhold a value inside an open map or a url, because it cannot tell a credential there from an ordinary value. The supported home for an outbound credential is a declarative connector: its `auth: { type, credentialRef }` names a secrets-layer reference that is resolved at boot and never stored in metadata. + + **What the warning covers.** An `http` node's `config.headers` entry, a query parameter of an `http` node's `config.url`, and a node's `connectorConfig.input` at any depth, including nodes inside `try_catch`, `loop` and `parallel` regions. A value draws when it is a non-blank string with no `{…}` template, and either its name reads as a credential (`Authorization`, `Cookie`, `x-api-key`, a name carrying `token`, `secret`, `password` and similar) or it opens with an auth scheme (`Bearer`, `Basic`, `Token`, `Digest`, `ApiKey`) followed by a value. A `{variable}` template is resolved per run and draws nothing. + + **What it does not do.** It never refuses: every finding is a `warning`, and a save, validate, build or lint that passed before still passes (`--strict` promotes it, as it promotes every warning). It never echoes the value it names. Nothing is withheld on any read. + + **Fix, by where the credential sits.** Declare a `connectors:` entry with a `provider` and call it from a `connector_action` node. A header credential goes to `auth: { type: 'bearer', credentialRef }`, or to `auth: { type: 'api-key', headerName, credentialRef }` for a key in a named header. A key in the url's query string goes to `auth: { type: 'api-key', paramName, credentialRef }`. On a connector node, drop the credential from `input`: the connector authenticates through its own `auth.credentialRef`. + + `@objectstack/lint` exports the rule `lintFlowCredentialLiterals`, its id `FLOW_CREDENTIAL_LITERAL`, and the one predicate it asks, `isCredentialShapedLiteral(name, value)`. In `@objectstack/spec`, only the descriptions of `HttpConfigSchema.headers` and a flow node's `connectorConfig.input` change; no shape changes. +- 99786f9: fix(spec): the stored-filter conversion's TODO for a null-valued key is true on every block, and no longer tells the operator to drop the key + + The ADR-0087 D2 conversion `page-component-filter-record-to-rule-array` leaves a record-form filter with a `null`-valued key as stored and reports it as a TODO, which `os migrate meta --stored` lists. The TODO's reason used to say the renderer skips that key, so it "constrains nothing", and to "Drop the key". That holds only where the block queries an object. Where the block's rows are inline (`data: { provider: 'value' }` or `staticData`), the objectui version this repository pins matches the key against the rows and selects the rows whose value is null, so following the advice there widened what the block shows. + + The reason now states both behaviours, says no one rule keeps both, and leaves the choice to the operator. For a stored `{ owner_id: null }` it names the rule `{"field":"owner_id","operator":"is_null"}` for the rows with no `owner_id` value, and says that a filter leaving `owner_id` unconstrained has no rule for it. The protocol-18 migration entry `element-data-source-and-object-block-filter-rule-array` says the same. + + The TODO for a key set to an empty operator object (`{ amount: {} }`) also said it "constrains nothing". The renderer refuses it instead: where the block queries an object it refuses the filter with `INVALID_FILTER` (400), and where the block's rows are inline it shows no rows. The reason now says that, and keeps its advice to drop the key, which is the renderer's own remedy. + + Nothing else changes. Both filters are still left exactly as stored and still reported as a TODO, on any block. No schema, conversion verdict or exit code moves. + + Clause-②: no +- 63bfe69: fix(objectql)!: a `time` field is a zone-less wall clock — a time of day written with a `Z` or an offset (`"10:00Z"`, `"10:00+08:00"`), and an instant whose UTC year has no four-digit spelling (`"+010000-01-01T10:00:00Z"`), are refused with `VALIDATION_FAILED` / 400 (`invalid_time`) instead of being stored verbatim on memory and SQLite and read back differently, or failing with a 500, on PostgreSQL (#20671) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a `time` field accepts as a written value. It ships as `minor` under the launch-window convention for accept-set narrowings (`check-changeset-no-major` refuses `major` until GA; the breaking-ness is carried by this banner and the ADR-0087 disposition above). + + The record validator's `time` arm now asks `@objectstack/core`'s one temporal rule, the same one the `time` filter comparand door asks, so a value is refused as a written `time` exactly when it is refused as a `time` comparand. It reads two things: a bare wall clock `HH:MM[:SS[.fraction]]` in range, and an instant in one of the ISO 8601 spellings a `datetime` is written in, on a calendar day that exists, whose UTC year has four digits (its UTC time of day is stored). Everything else is refused with `VALIDATION_FAILED` / 400 and the field code `invalid_time`, naming the field, on insert, update, a multi-row update and `engine.validate`, before anything is written. + + Refused now, where they were accepted: + + - **A time of day with a zone suffix**: `"10:00Z"`, `"10:00+08:00"`, `"10:00:00+0800"`, `"10:00:00.250Z"`. A `time` field carries no zone. The refusal has its own sentence, which `@objectstack/spec`'s validation-message catalog now carries in all four locales (`invalid_time_zoned`; English: "… is a time of day with no time zone: drop the Z or offset (HH:MM or HH:MM:SS), or use a datetime field for an instant"). The wire code stays `invalid_time`. + - **An instant the rule does not read as a time of day**: an extended year (`"+010000-01-01T10:00:00Z"`, or a `Date` of it), an instant whose UTC year is 10000 (`"9999-12-31T23:00:00-02:00"`), a day that does not exist (`"2026-02-30T10:00:00Z"`), and a spelling the `datetime` arm already refuses (`"2026-07-15 10:00Z"`, a space and a zone; `"2026-07-15t10:00:00z"`, lower case). + + What a caller sees, before and after, through `POST /api/v1/data/:object` and a read-back, the process in America/New_York, PostgreSQL 16 at `Asia/Shanghai`: + + | written to a `time` | memory | SQLite | PostgreSQL | now, on all three | + |:--|:--|:--|:--|:--| + | `"+010000-01-01T10:00:00Z"`, `"9999-12-31T23:00:00-02:00"` | 201, read back as written | the same | 500 `DATABASE_ERROR` | 400 `invalid_time` | + | `"10:00Z"`, `"10:00+08:00"`, `"10:00:00+0800"` | 201, read back as written | the same | 201, read back `"10:00:00"` | 400 `invalid_time`, the zone sentence | + | `"2026-07-15 10:00Z"` | 201, `"10:00:00"` | the same | the same | 400 `invalid_time` | + + **Who is affected.** A caller that writes a `time` field as a string with a `Z` or an offset, or as an out-of-range instant: a REST or SDK client, a flow, an MCP `create_record` / `update_record` call written by a model. A row already stored with such a value keeps it; nothing rewrites it. PostgreSQL stored a zone-suffixed time of day as its bare wall clock, so only a memory or SQLite deployment can hold one. An update that omits the field is not affected; one that sends the old value back is refused, naming the field. A `time` field whose literal `defaultValue` carries a `Z` or an offset has each insert that falls back to that default refused the same way. The server import (`POST /api/v1/data/:object/import`) turns a `time` cell into `HH:MM:SS` itself before the write, and already refused a zone-suffixed time-of-day cell, so its cells are unchanged. + + **Unchanged**, measured identical before and after on memory, SQLite and PostgreSQL through REST: + + - a bare wall clock: `"10:00"` and `"10:00:00"` read back `"10:00:00"`, `"10:00:00.250"` reads back `"10:00:00.250"`; + - a full ISO instant with a four-digit year, stored as its UTC time of day: `"2026-07-15T10:00:00Z"` and `"2026-07-15T18:00:00+08:00"` read back `"10:00:00"`; + - a `Date` with a four-digit year, still accepted; an epoch-millisecond number, a `{placeholder}` and an out-of-range clock (`"25:00"`), still refused with `invalid_time`; + - every `date` and `datetime` value, and every filter comparand. +- 4f83db5: fix(spec): the published bundles' text equals the source again — the build marks pure calls only, never a string (#20686) + + The build annotates each call of `lazySchema`, `strictObject` and `defineForm` as pure so a consumer's bundler can drop the schemas it never reaches. The rule that placed those annotations also rewrote a marked name quoted inside a string, so one D3 migration entry of protocol 18 (`dashboard-widget-stage-order-non-funnel-refused`) shipped a build-time comment marker inside its `acceptanceCriteria` text in `@objectstack/spec/migrations`, where its source reads `` `z.strictObject(DashboardWidgetSchema.shape)` ``. `os migrate meta` prints that text as the entry's `verify:` line when protocol 18 is the migration target. The published value now equals the source, character for character. + + The annotations now come from the TypeScript parse of each file, so a marked name inside a string, template text, a comment or a declaration is never touched. Every call that carried an annotation still carries one, and the published code is otherwise unchanged. One call, `z.strictObject(…)` in the Turso driver config schema, now has its annotation in front of the call rather than after the `z.`, where esbuild had been dropping it. Measured with esbuild, a consumer that imports one schema from `@objectstack/spec/data` or `defineStack` from the root gets a bundle of the same size as before. + + Clause-②: no +- bbcd20c: An import row now says which of its fields the write dropped, on the dry run and on the commit. A column mapped to a `formula` field, a static `readonly` field or a runtime-owned field is legally stripped by the engine: the row still succeeds, and the create door already reported the strip as `droppedFields`. The import row answered a bare `ok` / `created` on both halves, so a file whose formula column was ignored read exactly like a file that wrote it. `runImport` now copies the engine's own per-row report onto each `ok` row as `ImportRowResult.droppedFields`: from the `validateData` verdict on the dry run, from the row's `insertManyData` outcome, and from the `createData` / `updateData` response of a single-row write. The synchronous route, the async job's results and the job's dry run all carry it; no REST change was needed. + + Clause-②: yes (widening) + + - **Verbatim, in the engine's vocabulary.** The events are the engine's `DroppedFieldsEvent`s, one per reason (`computed`, `readonly`, `readonly_when`, `primary_key`). The import reads no reason and keeps no list of non-writable types, so a reason the engine adds later reaches the row unchanged. A reader that branches on `reason` must stay exhaustive. + - **Where the key is absent although something may have been dropped.** A create batched through `createManyData` (a protocol without `insertManyData`) is reported only as a batch-level union that names no row, so those rows carry no key. And a row the import would UPDATE is previewed in `update` mode, which runs no `readonlyWhen` or primary-key strip, so its dry run can name fewer fields than its commit. The `ImportRowResultSchema.droppedFields` describe now says both. + - **Unchanged:** `ok`, `action`, the counters, the failed rows and the async job's results cap. A clean row, a failed row and a skipped row carry no `droppedFields`. + + `ImportProtocolLike.insertManyData`'s declared outcome now names the optional `droppedFields` it already answered with. +- c8111a5: docs(spec): the Automation API docblock says the toggle door switches packaged flows only, and names a customer flow's switch (#20726) + + The module docblock of `api/automation-api.zod.ts` listed `POST /api/v1/automation/:name/toggle` as "Enable/disable flow". That file ships as source, and its docblock is also the source of the Automation API reference page. The line now reads "Enable/disable a packaged flow". A new paragraph says what a flow authored in the deployment uses instead: its `status`, published with the complete definition through `PUT /api/v1/automation/:name`. The toggle door refuses such a flow with 409 `RESOURCE_CONFLICT`. The `IAutomationService.toggleFlow` docblock, which read "Enable or disable a flow", says the same. This is prose only: no schema, type or export changes. +- 4b4ee88: docs(spec): the `FilterCondition` docblock says the query engine refuses the nested-relation form + + `FilterCondition`'s form 4, `{ relation: { field: value } }`, stays in the type and the schema (nothing is narrowed: `FilterConditionSchema` parses it as before), and its docblock now states what the engine answers: `INVALID_FILTER` / 400 on every driver, because no data-path driver follows a relation into the related object. It names the route that works — filter the related object first, then match the relation field against the ids it returns (`$in`, or `$contains` per id on a multi-valued relation). The `QueryFilter` example no longer teaches the form, and the `Filter` nested arm's comment points at the refusal. +- 93e9e42: fix(spec): two ADR-0087 migration entries state what the tree does — `etl-pipeline-layer-retired` dates the `syncConfig.schedule` deletion to `@objectstack/spec` 17, and `driver-sql-unresolvable-where-column-refused` names the remote `aggregate()` refusals + + Clause-②: no + + **`etl-pipeline-layer-retired` (protocol 17).** The entry explains that connector-attached + `syncConfig` has no reader outside `packages/spec`, and cites the same measurement that removed + `syncConfig.schedule`. Its `replacement` text said that key was retired "in 18". It was deleted + in `@objectstack/spec` 17 under ADR-0049 (first released in 17.5.0), as the note at the deleted + position in `integration/connector.zod.ts` already says. The sentence now reads "the same + measurement that deleted `syncConfig.schedule` in @objectstack/spec 17 under ADR-0049". + + **`driver-sql-unresolvable-where-column-refused` (protocol 18).** The entry named only a `where` + column on `find()` / `findOne()` / `count()` and `INVALID_FILTER` / 400. On the remote face of + `TursoDriver`, the `aggregate()` door answered `[]` for a missing column or a missing table. It + now refuses as the local face does: `INVALID_FILTER` / 400 for a `where` column the table lacks, `INVALID_FIELD` / 400 for a + `groupBy` or aggregation column the table lacks, and `DATABASE_ERROR` / 500 for an object whose + table is absent. The entry's `surface` now names that door and those codes. Its remedy adds + grouping and aggregating, and running schema sync so the object's table exists. Its acceptance + criterion now also covers a report or dashboard that groups by, or aggregates over, a name the + object has no column for. + + Text only: no entry id, conversion or matching logic changes, and `os migrate meta` rewrites + exactly what it rewrote before. The generated migration registry, `spec-changes.json` and the + protocol upgrade guide carry the corrected text. +- ca5408c: docs(spec): the `FilterCondition` docblock says the query engine serves the nested-relation form in `where` + + `FilterCondition`'s form 4, `{ relation: { field: value } }`, now states the served semantics: the engine reads the related object with the condition as the caller (its row scope and field permissions apply), matches the relation field against the ids it returns (`$in`, or any member on a multi-valued relation), reaches one level, and refuses a condition matching more related records than its cap rather than truncating. The `QueryFilter` example shows the form again, and the `Filter` nested arm's comment says the engine serves one level. The type and the schema are unchanged. +- ebb66aa: docs(spec): the `strictObject` `guidance` option and the `ToolSchema` guidance table no longer describe `guidance` rows as tombstones for retired keys + + Clause-②: no + + `StrictObjectOptions.guidance` (in the published declarations) and the docblock above + `TOOL_RETIRED_KEY_GUIDANCE` in `src/ai/tool.zod.ts` (shipped as source) called the slot a + place for "tombstones for retired keys". A tombstone is `retiredKey()` in the shape, which + keeps the key declared, and a `guidance` row for a declared key never fires. The docblocks + now say what the slot is for: prescriptions for keys the shape does not declare — + wrong-layer pointers, and the upgrade for a spelling removed from the shape. + + Text only: no schema, no guidance entry, no prescription and no parse behaviour changes. +- ceee88f: fix(driver-sql, driver-turso, plugin-security, spec)!: `SqlDriver` and `TursoDriver` compile the filter they are handed — their copies of the whole-day bound and of the NULL-safe `$not` rewrite are deleted, and the RLS compile seam lowers type-blind when it cannot read the declared types (ADR-0053 D-D1 items 5, 7 and 9, #20822) + + Clause-②: no (narrowing) + + + + **BREAKING**: `SqlDriver` in `@objectstack/driver-sql` loses three `protected` methods: `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite`. They were the driver's copy of the whole-day bound, which the shared lowering now applies once, at the seams, before a driver sees the filter. A subclass of `SqlDriver` that calls one of them, or overrides one with the `override` modifier, no longer compiles (TS2339, TS4113). That includes a subclass of `SqliteWasmDriver` or `TursoDriver`, which extend `SqlDriver`. A subclass that re-declares one without `override` still compiles, but the driver never calls it, so the rule it carried stops applying. It ships as `minor` under the launch-window convention. The class's public methods are unchanged. + + FROM → TO: a `SqlDriver` subclass that called `this.calendarDayUpperBoundRewrite(table, field, op, value)`, `this.calendarDayBetweenRewrite(table, field, value)` or `this.calendarDayExclusiveUpperBound(table, field, value)`, or overrode one of them, lowers the filter with `lowerFilterCondition` from `@objectstack/spec/data` instead, before the driver compiles it: `lowerFilterCondition(where, { isDatetimeColumn })`, where `isDatetimeColumn` answers which columns get the whole-day bound. + + **Supersedes two sentences of this release's shared-lowering entry** (`lowerFilterCondition`, #5930), which this change makes false: + + - "A guard without that set treats no column as `datetime`." Now: when the RLS compile seam has no field guard, or one without a `datetime` set, it cannot read which columns are `datetime`, so it applies the whole-day rule to every column (the `@objectstack/plugin-security` entry below). + - "Each driver keeps its own copy of these rules, and every copy gives the same answer on lowered input." Now: `SqlDriver`, `SqliteWasmDriver` and `TursoDriver` keep no copy of the whole-day bound or of the NULL-safe `$not` rewrite. A read through the engine or the RLS compile seam gets the lowered answer, and a call on the driver itself gets the comparison it wrote (the `@objectstack/driver-sql` and `@objectstack/driver-turso` entries below). + + - **`@objectstack/plugin-security` — an RLS policy compiled with no field guard is lowered type-blind.** The RLS compile seam runs the shared `lowerFilterCondition` on every compiled `using` and `check` filter. When the security plugin could not resolve the object's declared fields (no field guard), or a caller of `RLSCompiler.compileFilter` passes a guard without a `datetime` set, the seam cannot read which columns are `datetime`, and it now applies the whole-day rule to every column (a bare-day upper bound becomes `$lt` the next day), as ADR-0053 D-D1 item 7 rules for a seam that cannot read the type. It used to read no column as `datetime`, which left the bound to each driver's own copy of the rule. Visible on a `using` policy such as `record.signed_on <= '2026-01-05'` on such an object: every row of that day is kept on every driver, including `InMemoryDriver`, which had compared it as written since its own copy was deleted. A guard with a `datetime` set is unchanged, and so are the NULL-polarity guards. + - **`@objectstack/driver-sql` — `SqlDriver` keeps no copy of the rules the seams apply.** Deleted: the whole-day rewrite of a bare-day `$lte` and of a `$between` maximum on a `datetime` column, on the plain and the legacy-normalised column paths, including the last supported day (the protected methods `calendarDayExclusiveUpperBound`, `calendarDayUpperBoundRewrite` and `calendarDayBetweenRewrite` are removed from the class); and the NULL-safe rewrite of a `$not` operand (`nullSafeNegationOperand` and its polarity tables, module-private). A read through the engine or the RLS compile seam is unchanged: the seam hands the driver a filter the shared lowering has already rewritten, and the deleted copies gave the same answer on that input. A caller that passes no seam — `find`, `findOne`, `count`, `aggregate`, `distinct`, `updateMany`, `deleteMany` or `findWithWindowFunctions` called on the driver itself — now gets the comparison it wrote: a bare-day `$lte` compares against that day's midnight, a `$between` is inclusive at both ends, `$lte '9999-12-31'` compares against that midnight, and a `$not` is SQL's three-valued negation, so a row whose compared column is NULL is not returned by it. `$ne`, `$nin` and `$notContains` keep their NULL-safe form, which this emitter spells for the operator itself. The refusal of an `undefined` comparand (`INVALID_FILTER` / 400) is kept: without it some positions would answer instead of refusing. To keep the seam's reading on a direct call, lower the filter first: `driver.find(object, { where: lowerFilterCondition(where, { isDatetimeColumn }) })`, with `lowerFilterCondition` from `@objectstack/spec/data`. A subclass that called or overrode one of the three removed methods: see **BREAKING** above. + - **`@objectstack/driver-sqlite-wasm` — `SqliteWasmDriver` inherits the `SqlDriver` change above**, with the same answers on a seamed read and on a direct call. + - **`@objectstack/driver-turso` — both faces of `TursoDriver` compile the filter they are handed.** Local and replica mode inherit the `SqlDriver` change. Remote mode: `toRemoteFilter` no longer widens a bare-day `$lte` or a `$between` maximum (it still splits a two-bound `$between` into the `$gte` / `$lte` pair the remote transport compiles, both ends inclusive, and still converts each comparand to storage form), and `RemoteTransport` no longer rewrites a `$not` operand (its copy of the polarity tables is deleted). The two faces still answer every filter alike, on a seamed read and on a direct call. The remote transport keeps its refusal of an `undefined` comparand, worded as `driver-sql`'s, so both faces refuse it in one sentence. The same one line keeps the seam's reading on a direct call. + - **`@objectstack/spec` — the ADR-0087 ledger records the removal.** The protocol-18 step of `MIGRATIONS_BY_MAJOR` gains the semantic entry `driver-sql-calendar-day-methods-removed`, which names the three removed methods with their replacement and acceptance criteria. Every upgrade channel that projects protocol 18 carries it. `spec-changes.json` and the generated upgrade guide stop at the current protocol, 17, so neither changes in this release. A subclass that re-declares one of the methods without `override` still compiles and is never called, so the ledger, not the compiler, is the notice that reaches it. +- e18fea6: fix(objectql,driver-mongodb,formula): the `having` and per-aggregation evaluator compiles the whole-day comparison it is handed, and `$contains` asks membership on a JSON-stored field in `MongoDBDriver` and in `matchesFilterCondition` (ADR-0053 D-D1 items 5 and 9; the `FILTER_OPERATORS` `$contains` contract, #20822) + + Clause-②: no + + - **`@objectstack/objectql`: the aggregate evaluator's own whole-day copy is deleted.** The walker behind `having` and `aggregations[i].filter` no longer widens a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, on a `datetime` column to the whole day, and no longer drops the bound on `9999-12-31`. Through `engine.aggregate` nothing changes: the engine's seam lowers both positions with the shared `lowerFilterCondition` (`@objectstack/spec/data`) before the walker runs, by the object's declared `datetime` fields for the per-aggregation `filter` and by the aggregated column's type for `having`. A caller that passes no seam gets the comparison it wrote: `applyInMemoryAggregation(rows, ast, tz, fields)` called directly now counts `{ at: { $lte: '2026-02-01' } }` against that day's midnight. To keep the seam's reading on a direct call, lower each `filter` first with `lowerFilterCondition(filter, { isDatetimeColumn })`. + - **`@objectstack/driver-mongodb`: `$contains` / `$notContains` ask membership on a declared JSON-stored field.** On a field `syncSchema` recorded as `multiple: true`, a multi-option type (`tags`, `multiselect`, `checkboxes`) or a JSON type, `translateFilter` (every verb, and the aggregation `$match`) now emits an array-only `$elemMatch` over the members the comparand names, with the candidate rule the SQL dialects bind (`jsonMembershipCandidates`, `@objectstack/core`): `'1'` names the string `'1'` or the number `1`, `'true'` the string or `true`. It used to emit a `$regex`, which MongoDB applies to each element, so `{ owners: { $contains: 'u1' } }` matched a stored `['u10']` and `{ tags: { $contains: 'red' } }` a stored `['redwood']`. `$notContains` is the exact complement, and still admits a row with no value. A scalar column, and a field whose declaration the driver does not hold (an object never synced, a standalone `translateFilter` call), keep the substring `$regex`. + - **`@objectstack/formula`: `matchesFilterCondition` asks membership of a JSON-stored column.** When the caller supplies `options.fields` and it names the column, the declaration decides: membership on a JSON-stored column, substring on any other. Otherwise the stored value decides: an array asks membership, anything else substring. A stored array used to fail `$contains` and pass `$notContains` whatever it held. + - **The RLS write check, which evaluates a policy with this function, moves with it.** Under a `check` such as `record.tags.contains('x')` on a multi-valued field, a write whose post-image holds `['x']` (a row the same policy's read shows) is now admitted; it was refused `PERMISSION_DENIED` / 403. `['xy']` stays refused, and the read hides it. + - A scalar written to a declared multi-valued field is judged as written, before the write door wraps it in a list. So `tags: 'xy'`, which the check used to admit while the read hides the stored `['xy']`, is now refused 403. And `tags: 'x'` is now refused 403 too, although the read shows the stored `['x']`. Send the list, `tags: ['x']`. + - **`@objectstack/spec`: docblock only, in the shipped `src/data/filter.zod.ts`.** The three pointers to the deleted `SqlDriver.calendarDayUpperBoundRewrite` / `calendarDayBetweenRewrite` now name the shared `lowerFilterCondition` at the seams, and the `FILTER_OPERATORS` `$contains` implementation-status list gains `driver-mongodb` and `formula`. No schema, type or export changes. + - No exported name changes. +- 660a9b2: fix(spec): `BlueprintNavItemSchema.label` says an absent label is inherited at render time, not defaulted by the expander + + Clause-②: no + + The `label` describe on a blueprint nav item read "defaults to the target label/name". An + expander or an AI author that follows "defaults" copies the target's label into the entry, and + the entry then stops following a rename of that target. The runtime nav entry's `label` has + meant something else since it became optional: absent, the entry inherits the CURRENT label of + what it opens at render time; present, it renders verbatim. The blueprint describe now says + exactly that, and tells the author not to copy the target's label in as a default. + + Describe text only: the key stays `z.string().optional()`, so the schema accepts and refuses + the same blueprints. The reference page `content/docs/references/ai/solution-blueprint.mdx` + is regenerated from it. +- f6ccca4: fix(objectql,rest): a `date` or `datetime` value refused for its year says so — "must be a date in the years 0001 to 9999" / "must be a datetime whose UTC year falls in the years 1000 to 9999" — instead of "must be a valid date (ISO-8601)", which was false for a value such as `0500-07-15T10:00:00Z` (#20846) + + Clause-②: yes (widening) — one new export on `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`. No value's verdict moves and no wire key moves: the field code stays `invalid_date` and its `constraint` stays `{ type }`. + + `POST` / `PATCH /api/v1/data/:object` and each row of `POST /api/v1/data/:object/import` + refuse a `date` outside the years 0001 to 9999 and a `datetime` whose UTC year falls + outside 1000 to 9999. When the value itself is readable — an ISO 8601 string such as + `0500-07-15T10:00:00Z` or `+010000-01-01`, or a `Date` — the refusal's message now + names the kind's years. An author who read "not valid ISO" rewrote the spelling, and no + spelling of that year is admitted. + + - `@objectstack/spec`: the validation message catalog gains `invalid_date_range` and + `invalid_datetime_range` in `en`, `zh-CN`, `ja-JP` and `es-ES`. They are two more + sentences of the `invalid_date` code, never a wire value. The years are the template + parameters `{{firstYear}}` / `{{lastYear}}`. A deployment that overrides a message + under `validation.field.invalid_date` or `validation.field.invalid_datetime` does not + cover these values. To override their text, define + `validation.field.invalid_date_range` / `validation.field.invalid_datetime_range`. + - `@objectstack/core`: `SUPPORTED_TEMPORAL_YEARS` (`{ date: { first: 1, last: 9999 }, + datetime: { first: 1000, last: 9999 } }`, frozen) is the range + `isOutsideTemporalYearRange` judges by. It is exported so a refusal names the range + from the source the doors use, never a copy of its numbers. + - `@objectstack/objectql` and `@objectstack/rest`: the record validator and the import's + cell reader choose the range sentence for such a value. An import cell with more than + four year digits (`+010000-01-01`) is refused by the import's reader. It used to read + "is not a valid date" and now gets the same range sentence as the write door. + + **What is not affected.** Which values are refused is unchanged, and so is the refusal's + code (`invalid_date`) and `constraint`. A value that is not readable keeps its sentence: + "must be a valid date (ISO-8601)" at the write door, `"…" is not a valid date` at the import. + So does a number, which is never a written `date` or `datetime`. +- 26437ae: fix(spec): the `ui-object-grid-page-size-positive-integer-refused` migration entry states what the platform does with a stored `object-grid` page size of `0` — the page saves and loads, and the finding is advisory on the CLI + + Clause-②: no + + **`ui-object-grid-page-size-positive-integer-refused` (protocol 18).** The entry's acceptance + criterion said that a stored page whose `object-grid` node carries `pageSize: 0` "is refused on + its next authoring-path save with a per-key issue at `pagination.pageSize`". Nothing on the + metadata save path judges a page component's `properties`. `PageComponentSchema.properties` is an + open record, and the one judge of the `ComponentPropsMap` row is the component-props gate, an + advisory rule that runs on the CLI only. Measured through `saveMetaItem`, the call behind + `PUT /api/v1/meta/page`: such a page saves (`success: true`, and the result carries no `advisories` member), is stored as an + active row and reads back with `pageSize: 0` intact. On the same page, `os validate`, `os build` + and `os lint` each report one advisory `component-props-invalid` finding at + `properties.pagination.pageSize` and no error. The criterion now says exactly that. It also names + the `pageSizeOptions` entry and the flat `pageSize` shorthand, which are reported the same way at + their own paths. + + Text only: no entry id, conversion or matching logic changes, and `os migrate meta` rewrites + exactly what it rewrote before. The generated migration registry carries the corrected text. The + sibling entry `object-grid-default-filters-rule-array` already stated the advisory-only outcome + and is unchanged. +- c6b3a01: fix(spec): a stored form view whose subform grid columns use the `field` spelling is respelled to `name` on the way in, as a relationship field's `inlineColumns` already are (#20901) + + **`@objectstack/spec`** + + - **New ADR-0087 conversion `form-view-subform-columns-canonicalized` (protocol 18, retired from the authoring path).** A form view's `subforms[].columns` accepted any value through 17.5.0 and now takes the inline grid column contract, which refuses `{ field: 'x' }` with the prescription naming `name`. The conversion rewrites that entry as `{ name: 'x' }`, every other key kept, wherever a form view travels as data at rest: a stored `view` row (its `form`, each `formViews` entry, a form view item's `config`, a flattened form overlay), an assembled manifest's `viewItems`, and `os migrate meta --from 17`, which lists the edit. A built artifact whose declared protocol floor is 17.5.0 or lower is converted too, not refused. An entry that already carries `name` is left alone, including one that carries both `field` and `name`: the parse names both keys, and the author picks one. It is the same respelling `field-column-lists-canonicalized` applies to a relationship field's `inlineColumns`, and both entries run one shared rule. + - **Authored sources are unchanged:** `defineStack` and `objectstack validate` do not replay a retired conversion, so a source that writes `field` on a subform column is still refused with the prescription. Write `name`. + - **Two step-18 migration entries now read true.** `inline-grid-column-currency-scale-refused` no longer says a column declaring no `type` keeps its `scale`: over a `currency` field of the child object, `defineStack` refuses it, under `inline-grid-column-identity-only-currency-scale-refused`, which the entry now names. `form-view-subform-columns-closed` names this conversion as the one mechanical edit on its carrier. +- 2742e53: fix(spec): protocol 18's migration rationale now covers the form view's inline grid columns and the identity-only currency `scale` refusal (#20901) + + **`@objectstack/spec`** + + - **`MIGRATIONS_BY_MAJOR[18].rationale` gains one fragment, `form-view-subform-columns-closed`.** It is the paragraph `os migrate meta --step` shows for the protocol 17 → 18 hop. The new sentences say that a form view's `subforms[].columns` now takes the strict `InlineGridColumnSchema` a relationship field's `inlineColumns` takes, that the conversion `form-view-subform-columns-canonicalized` respells a `{ field }` column as `{ name }` in stored rows and assembled artifacts while an author writing `field` is refused, and that `defineStack` refuses `scale` on a column that declares no `type` when its `name` is a `currency` field of a child object declared in the same stack (`inline-grid-column-identity-only-currency-scale-refused`). + - Text only: no schema, conversion or migration entry changes, and `conversionIds` and `semantic` for step 18 are unchanged. +- a75311d: fix(objectql)!: the engine's `aggregate` asks the aggregate × field-type table for every aggregation over a declared field, so `min` / `max` / `avg` over a type the table refuses answer `INVALID_FIELD` / 400 on every driver instead of one answer per driver + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, on every driver and for every caller that reaches the engine — the REST query door, a flow or hook, a roll-up summary's recompute, and the analytics strategy that lowers a cube query onto `engine.aggregate`. Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + FROM → TO, per aggregation `{ function, field }` naming a declared field: + + - `min` / `max` over a type outside the numeric, temporal and boolean classes — the structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), the multi-option types (`multiselect`, `checkboxes`, `tags`), the string family (`text`, `email`, `url`, `phone`, …), the option and reference types (`select`, `radio`, `lookup`, `master_detail`, `tree`, `user`), `autonumber`, the file family and `formula` — and over any `select`, `lookup`, `user`, `file` or `image` declared `multiple: true`: FROM whatever the driver answered (a document or an array in memory, the serialized text on SQLite, a 500 on PostgreSQL for a JSON-stored field; a collation-dependent string for a text field) TO `400 INVALID_FIELD`. + - `avg` over a type outside the numeric and boolean classes — a `date`, `datetime` or `time` field included: FROM `null` in memory, a coerced number on SQLite (the average YEAR for a datetime), a 500 on PostgreSQL, TO `400 INVALID_FIELD`. + - `count_distinct` is unchanged: it was already refused over the JSON-stored types, in the same words. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`aggregations[0].field`), what the function does and the field with its declaration (`takes the max of 'meta', a declared json field — a structured-JSON value`), saying the query was not run, and naming the types the function accepts, read off the table, inside the first 500 characters the REST door keeps. The thrown error carries `field`, `fields` (every offending aggregation), `object` and `param` (`aggregations`). + + **Why a refusal.** `AGGREGATE_FIELD_TYPE_COMPATIBILITY` already declares which pairs every backend answers the same way, and the dataset compile and lint legs refuse the rest; the engine door asked only its `count_distinct` row. Measured through `engine.aggregate` over two rows: `max` over a `json` field answered `{ a: 1 }` in memory, the string `'{"b":1}'` on SQLite and 500 `DATABASE_ERROR` on PostgreSQL 16 (`function max(json) does not exist`); a `tags` field and a `multiple: true` select or lookup split the same way; `avg` over a `datetime` answered `null`, `2026` and a 500. One query, three answers. + + **What to write instead.** Aggregate a field of a type the function accepts — for `min` / `max`: `number`, `currency`, `percent`, `rating`, `slider`, `progress`, `summary`, `date`, `datetime`, `time`, `boolean` or `toggle`; for `avg`: the same minus the temporal three. A question that was counting in disguise is `count` (or `count_distinct` over a scalar-stored field). A first or last record by a text value is a sort on a list, not an aggregate. A quantity stored as text or JSON belongs in a numeric or temporal field of its own, aggregated there. + + **Who is affected.** A caller that asked `min` / `max` / `avg` of such a field on the in-memory driver or SQLite and read the answer as a real one; on PostgreSQL a JSON-stored field was already a 500. Metadata that lowers onto `engine.aggregate` takes the same verdict at run time: a roll-up summary (`summaryOperations`) whose `min` / `max` / `avg` names such a child field records a failed recompute, a grouped list view's server-side header summary is refused, and a chart or metric component's `aggregate` over such a field is refused. No example app and no published stack authors such a pair. + + **Not judged yet: `sum`.** The `sum` row of the table is held back at this door: a published stack authors a `sum` column summary over a `formula` field, a pair the table refuses, so that row awaits its own decision. `sum` over any field reaches the driver as before. + + **Unchanged.** Every pair the table accepts; `count` over any field, a JSON-stored one included; an aggregation that names no field; an undeclared name or a relationship path, which this door does not judge (the REST door answers an unknown name `INVALID_FIELD` before the engine is reached); a field whose declared type is outside `FieldType`. The structured-JSON `groupBy` entry of this same release lists a structured-JSON field as an aggregated `min` / `max` column as unchanged; this entry is the later word on that shape. + + `@objectstack/spec`: the TSDoc of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` no longer says the engine's `aggregate` door reads only the `count_distinct` row. The table itself is unchanged. +- d98bf24: fix(objectql)!: the engine's `aggregate` judges the `sum` row of the aggregate × field-type table too, so `sum` over a type the table refuses answers `INVALID_FIELD` / 400 on every driver instead of `0` in memory and on SQLite and a 500 on PostgreSQL + + Clause-②: no (narrowing) + + + + **BREAKING** (`@objectstack/objectql`): this narrows what `aggregate` accepts, on every driver and for every caller that reaches the engine — the REST query door, a flow or hook, a roll-up summary's recompute, and the analytics strategy that lowers a cube query onto `engine.aggregate`. Shipped as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + This completes the change of this same release that made the engine's `aggregate` ask the table for `min`, `max` and `avg`, and that held the `sum` row back. Its paragraph "Not judged yet: `sum`" is superseded: this entry is the later word, and the door now asks every row of the table. + + FROM → TO, per aggregation `{ function: 'sum', field }` naming a declared field: + + - `sum` over a type outside `number`, `currency`, `rating`, `slider`, `progress`, `summary`, `boolean` and `toggle` — a `percent` (a rate does not add), the temporal types (`date`, `datetime`, `time`), the string family (`text`, `email`, `url`, `phone`, …), the option and reference types (`select`, `radio`, `lookup`, `master_detail`, `tree`, `user`), `autonumber`, the file family, the structured-JSON types (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), the multi-option types (`multiselect`, `checkboxes`, `tags`) and `formula` — and over any `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`: FROM whatever the driver answered TO `400 INVALID_FIELD`. Measured through `engine.aggregate` over two rows: a `json`, `text`, `select` or `tags` field summed to `0` in memory and on SQLite and answered 500 `DATABASE_ERROR` on PostgreSQL 16 (`function sum(json) does not exist`); a `datetime` field summed to `0` in memory, to the years added on SQLite and a 500 on PostgreSQL; a `formula` field summed to `0` in memory and was already refused `400 INVALID_FIELD` by both SQL drivers, which have no column for it; a `percent` field added the rates on all three. + + **What an author sees now.** `400 INVALID_FIELD`, naming the position (`aggregations[0].field`), what the function does and the field with its declaration (`sums 'meta', a declared json field — a structured-JSON value`), saying the query was not run, and naming the types `sum` accepts, read off the table, inside the first 500 characters the REST door keeps. The thrown error carries `field`, `fields` (every offending aggregation), `object` and `param` (`aggregations`). + + **What to write instead.** Sum a field of a type `sum` accepts: `number`, `currency`, `rating`, `slider`, `progress`, `summary`, `boolean` or `toggle`. A rate stored as a `percent` is averaged (`avg` accepts it), or the quantity it is a rate of is summed. A value computed by a `formula` is stored in a numeric field of its own when it must be summed on the server. A question that was counting in disguise is `count`. + + **Who is affected.** A caller that asked `sum` of such a field on the in-memory driver or SQLite and read the `0` as a real total, and a caller that summed a `percent` field on any driver. On PostgreSQL the other measured pairs were already refused (a 500, or a 400 for a `formula`), and on SQLite so was a `formula`. Metadata that lowers onto `engine.aggregate` takes the same verdict at run time: a roll-up summary (`summaryOperations`) whose `sum` names such a child field records a failed recompute, a grouped list view's server-side header summary is refused, and a chart or metric component's `sum` over such a field is refused. No example app authors such a pair. One published stack authors a `sum` list-column summary over a `formula` field; that summary is computed client-side and does not reach `engine.aggregate`. + + **Unchanged.** Every pair the table accepts, `sum` over the eight types above included; `count` over any field; an aggregation that names no field; an undeclared name or a relationship path, which this door does not judge (the REST door answers an unknown name `INVALID_FIELD` before the engine is reached); a field whose declared type is outside `FieldType`. + + **A correction to the earlier entry of this release.** It listed the multi-capable types declared `multiple: true` as `select`, `lookup`, `user`, `file` or `image`; the list is `select`, `radio`, `lookup`, `user`, `file` and `image` (`MULTI_CAPABLE_TYPES`). A `radio` declared `multiple: true` was refused by `min` / `max` / `avg` there all the same, by its type's own row, and it is refused by `sum` here. + + `@objectstack/spec`: the TSDoc of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` states that the engine's `aggregate` door asks every row of the table, where it said "the other rows" while one was held, and names `radio` among the multi-capable types. The table and the predicate are unchanged. +- 8368f1c: docs(spec): `mapping.connectorSource` is pulled when a job drives it — the describes say where the watermark is read from and that a pull reads one response (#20919) + + The `connectorSource` describe no longer says the pull is not executed: the + connector sync executor in `@objectstack/service-automation` reads the binding, + and nothing schedules a pull until the `job` stage lands. `watermark.field` now + states that the next pull's starting point is read from the TARGET field a + `fieldMapping` entry copies it onto (an unmapped one is refused at pull time), and + `watermark` states the one-response limit: the connector's paging is not followed, + so a paged endpoint yields its first page only. The liveness ledger's + `connectorSource` rows are `live`, with no author warning: that nothing schedules a + pull yet is said on the key's description. The retired `connector.syncConfig` + prescription and the `connector-sync-keys-retired` upgrade entry say the same, and + the entry's acceptance criterion no longer claims the connector is validated at + authoring. + No key, value or default changed. +- 013f97d: docs(spec): the `record_related` action location's docblock names its placement, each row of a related list inside a parent record, instead of "a related list section" (#20937) + + Clause-②: no + + Only the `record_related` line of the `ACTION_LOCATIONS` docblock in `src/ui/action.zod.ts` changes. It now states the contract a renderer implements: a per-row action on each row of a related list shown inside a parent record, in that parent's context only. Unlike `list_item`, which surfaces on every row wherever the object is listed, it never surfaces on the object's own list views. The old words, "actions on a related list section", could be read as the section's toolbar. `ACTION_LOCATIONS` and `ActionLocationSchema` are unchanged: the same six values parse, and no `.describe()` string, export or runtime behaviour moves. The console's placement of `record_related` actions on related-list rows ships separately. +- 399e3aa: docs(spec): the `DashboardWidgetOptionsSchema` doc comment states which widget `options` keys a renderer reads, instead of naming presentation extras (`icon`, `trend`, `columns`, `striped`, `density`) it called renderer-understood + + Clause-②: no — no key is declared and no value is typed, so the accept set is unchanged. + + `options` still parses any key. A widget always binds a `dataset`, so it renders through + objectui's dataset-bound path, and that path reads only the five declared keys + (`dateGranularity`, `sortBy`, `sortOrder`, `limit`, `stageOrder`) and the `description` + sub-caption. Any other key parses and renders nothing. To format a number, set `format` and + `currency` on the dataset measure. To accent a tile, set the widget's `colorVariant`. To style + a chart, set the widget's `chartConfig`. +- ba03198: docs(spec): the shipped `src/migrations/entries/README.md` "Reproduce any row" recipe now fetches both commits into the driver-less bare clone before `merge-tree`, and reads exit 1 with no tree id as a missing object, never a conflict (the old `--shared --no-local` form misread a clean pair as conflicted) (#20970) + + Clause-②: no +- 94608a7: fix(spec): a book tree's synthetic *Uncategorized* group holds only the unplaced docs of the book's own packages, per ADR-0046 §6.4 + + Clause-②: no + + - `resolveBookTree` used to put every unclaimed doc it was handed into the book's *Uncategorized* group. `GET /api/v1/meta/book/:name/tree` resolves over every doc in the environment, so a book's tree listed every other package's ungrouped docs there. The docs portal never showed those docs in the book. + - The group now holds a doc only when it belongs to one of the book's packages: the package that ships the book, or a package a group names with `package`. A doc with no stamped package still counts as the book's. A doc of another package stays reachable through its own package's book. + - The implicit per-package book that the tree route serves for a package id catches no other package's docs either. + - Unchanged: a doc whose `group` names one of the book's groups joins that group from any package. A book that declares no package keeps every unclaimed doc in *Uncategorized*. The docs a book claims, and so every doc's audience, do not change. +- b3d7a70: fix(spec): `ObjectSchema.fields` refuses `constructor` and `prototype` at the offending key (`fields.constructor`, `fields.prototype`), not at the `fields` slot + + Clause-②: no + + A field map carrying a key named `constructor` or `prototype` is refused, as before. The + refusal used to be reported at the path `fields`, which names no field, so a form reading the + structured issues of a refused save could not point at the field that caused it. It is now + reported at that key, like the `__proto__` refusal (`fields.__proto__`) and the key grammar's + `invalid_key` refusal (`fields.Bad Name`) already are. A document carrying both names gets two + issues, one at each key, where it used to get one at the slot. + + Nothing else moves. The same keys are refused and the same documents are accepted. The issue + code (`custom`) and the message are unchanged. The published JSON Schema states the same ban: + it is now written as one `propertyNames` clause per name inside `allOf` instead of one clause + naming both, which accepts and refuses exactly the same documents. +- c27404f: fix(spec): an object's embedded `listViews` are served in the reader's language, from the `_views` keys `os i18n extract` already writes + + Clause-②: no + + - `GET /api/v1/meta/object` and `GET /api/v1/meta/object/:name` now translate each view in an object's `listViews`: its `label`, its `description`, and the copy of its `bulkActionDefs` (label, confirm prompt, confirm button, and each param's label, help and placeholder). They read the keys `os i18n extract` writes for those views, `objects.._views..*`, where `` is the view's key under `listViews`. For example, `sys_account`'s `mine` view is now served as `我的链接` to a `zh-CN` reader. It used to be served as the authored `My Links`. + - A view with no translation for the requested locale keeps its authored text. An object with no `_views` entries is served unchanged. + - A string that was changed after the package shipped still wins over the packaged translation, the same rule a served view document and a dashboard follow. The string is compared with the same view in the packaged object. If it differs, the changed string is served in every locale. A view the packaged object does not declare keeps all of its own strings. + - `translateObject` (and `translateMetadataDocument('object', …)`) in `@objectstack/spec/system` does the translating, so any caller of those functions gets the same result. +- 27c0cf3: fix(spec): the `object-kanban` `quickAdd` retirement no longer sends authors to the `kanban-ui` block, which objectui does not register + + Clause-②: no + + - The refusal of `quickAdd` on `object-kanban` now ends "Delete the key; `object-kanban` offers no quick-add control." It used to say the control "is unchanged on the `kanban-ui` block". objectui retired that block (objectui#8257), so a node of that type saves clean and renders nothing. The refusal itself is unchanged: the same key is still refused, with the same code and path. + - The same sentence replaces the old one in the `os migrate meta --from 17` output (the `object-kanban-quick-add-retired` entry) and in the summary of the `object-kanban-quick-add-removed` conversion. That entry no longer offers "move the board to a host that renders the `kanban-ui` block" as a second way out. + - No author action beyond the existing one. `quickAdd: true` on an `object-kanban` is still a parse error. Delete the key. +- f3b16fc: Raise the published dependency floors to the 2026-10 production dependency group. No API changes. A consumer install resolves these ranges: + + Clause-②: no + + - `zod` `^4.6.1` → `^4.6.5`: `@objectstack/spec`, `@objectstack/core`, `@objectstack/objectql`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/cli`, `@objectstack/mcp`, `@objectstack/metadata`, `@objectstack/metadata-core`, `@objectstack/metadata-protocol`, `@objectstack/driver-turso`. + - `@libsql/client` `^0.17.3` → `^0.18.0`: `@objectstack/driver-turso`. Every behaviour the driver documents was re-measured on 0.18.0 and holds unchanged. That covers the URL scheme routing, the `URL_INVALID` and `URL_SCHEME_NOT_SUPPORTED` refusals, the WebSocket transport having no `fetch` or timeout seam, `syncUrl` being read only by the embedded-replica client, and the `?authToken=` precedence on `url` and `syncUrl`. The driver's refusal messages now name 0.18.0 as the measured version. 0.18.0 changes only the local `file:` client, which now pools connections. The driver creates that client only for an embedded replica, and calls only `sync()` on it. + - `@modelcontextprotocol/sdk` `^1.30.0` → `^1.30.1`: `@objectstack/connector-mcp`, `@objectstack/mcp`. + - `chalk` `^6.0.0` → `^6.0.1`: `@objectstack/cli`, `create-objectstack`. `yaml` `^2.9.0` → `^2.9.1` and `tsx` `^4.23.12` → `^4.23.15`: `@objectstack/cli`. + - `mongodb` `^7.5.0` → `^7.6.0`: `@objectstack/driver-mongodb`. + - `sql.js` `^1.14.1` → `^1.14.2`: `@objectstack/driver-sqlite-wasm`. + - `@noble/hashes` `^2.3.0` → `^2.4.0` and `jose` `^6.2.8` → `^6.2.12`: `@objectstack/plugin-auth`. The better-auth family stays at exactly `1.7.3`. + - `hono` `^4.13.5` → `^4.13.9`: `@objectstack/plugin-hono-server`. + - `pinyin-pro` `^3.29.1` → `^3.29.4`: `@objectstack/plugin-pinyin-search`. + - `@noble/ciphers` `^2.3.0` → `^2.4.0`: `@objectstack/service-settings`. +- 9bdc6d3: fix(spec): a stack whose mapping authors `connectorSource` validates and lints again — the liveness ledger's `live` row no longer carries an author warning + + Clause-②: no + + `os validate` and `os lint` exited 1 on any stack with a `mappings[]` entry that + authored `connectorSource`, and the only output was the liveness lint's internal + error `ledger entry has unrecognised status "live"`. The ledger graded the key + `live` (the connector sync executor reads every key of the binding) and still + asked the lint to warn whoever authored it; the lint has no warning for a key + that works, and stops on that inconsistency by design. The row carries no warning + now, so both commands judge the stack and exit 0 when nothing else is wrong. The + runtime metadata door no longer returns an `authoring-rule-threw` advisory for + the same mapping. + + The note the warning used to carry is on the key's description, where an author + reads it: a pull runs when a `job` drives it, nothing schedules one yet, so the + binding alone moves no rows. The retired `connector.syncConfig` prescription and + the `connector-sync-keys-retired` upgrade entry no longer say that authoring the + binding warns. + + `check:liveness` now refuses a `live` ledger row with `authorWarn: true` at any + depth, and prints how many rows opt into an author warning on every run. A + `planned` row with `authorWarn` still warns. No key, value or default changed. +- 95e24b0: fix(driver-sql,driver-turso)!: an upsert whose conflict lands on another organization's row is refused with `UNIQUE_VIOLATION` and writes nothing, and an upsert never changes a row's organization (#21185) + + Clause-②: no (narrowing) + + + + **BREAKING for `upsert` callers on the SQL drivers and on `TursoDriver`.** + + **What changed.** `upsert` resolves its conflict against the whole table, and the + primary key and a `unique: 'global'` column are installation-wide, so the row a + tenant-scoped call (`options.tenantId` on an object with a tenant column) collided + with could belong to another organization. The merge wrote the payload onto that + row, tenant column included. Now: + + - **A tenant-scoped upsert merges only into a row of the organization the row is + written under**, for any conflict target, the primary key included. A conflict + that lands on a row of another organization, or on a row with no organization, + is refused with `code: 'UNIQUE_VIOLATION'`, `status: 409`, and nothing is + written. That is the answer `create()` gets for the same collision: from the + caller's organization the call is an insert, and that insert collides. The + refusal names no organization and no value of the row it collided with. + - **The tenant column is insert-only** (`insertOnlyUpsertColumns`), like `id`, + `created_at` and `auto_number` columns: an upsert with no tenant context merges + into the row it lands on and keeps that row's organization. + + Mechanism, per face: on SQLite, PostgreSQL and the remote (libSQL) face, the merge + statement carries the organization predicate (`DO UPDATE … WHERE`), so another + organization's row is never written. On MySQL, whose `ON DUPLICATE KEY UPDATE` + takes no `WHERE`, the statement and a read of the landed row run in one + transaction (a savepoint inside a caller's transaction), and the read's failure + rolls the write back. The remote face now also stamps the caller's organization on + the row it inserts, as the local faces do. + + ## FROM → TO + + | you relied on | now | + |:--|:--| + | a tenant-scoped `upsert` merging into a row of another organization | refused with `UNIQUE_VIOLATION` / 409, nothing written | + | an `upsert` payload's tenant value moving the row it merges into | the row keeps its organization; to move a row between organizations, use `update()` | + + **What is not affected.** A tenant-scoped upsert whose conflict lands on a row of + its own organization merges as before, on every target. An upsert that inserts + lands under the caller's organization, or under the organization the payload + names explicitly, as before. +- 1a4c7f8: fix(spec): the `filter-text-operator-declared-type-refused` migration entry's control no longer counts JSON-stored fields, and it names the separate door that refuses text operators there + + Clause-②: no + + **`filter-text-operator-declared-type-refused` (protocol 18).** The entry's acceptance criteria + named every text-valued field, `multiselect` / `checkboxes` / `tags` and lookup and `user` ids + included, as the control that "must keep answering exactly as before". The declared-type door this + entry registers still leaves every text-valued type alone. A second door, though, judges a field by + how it is STORED: on a column stored as JSON it now refuses `$startsWith`, `$endsWith`, + `$icontains`, `$like` and `$ilike` with `INVALID_FILTER` / `400`, as it already refused the scalar + comparisons there. So a stored filter that uses one of those operators on a multi-valued field + answers that `400` after the upgrade, and the old sentence called it a control. + + The criteria now say four things: + + - The control is a text-valued field that is NOT stored as a JSON column. + - The JSON-stored population is `multiselect` / `checkboxes` / `tags`, any field declared + `multiple: true` (a multi-valued lookup or `user` among them), and, on a SQL deployment still + inside the ADR-0104 dual-encoding window, a single-value file-class field. + - That door refuses every text operator except the membership pair `$contains` / `$notContains`, + and its refusal names no declared type, so it is outside this entry's repair list. + - The repair on a multi-valued field is membership: `$contains` for one member, an `$or` of + `$contains` for any-of. A single-value file-class field answers text operators again once the + deployment finishes the media-column move (the column step of + `objectstack migrate files-to-references --apply`). + + Text only: no entry id, `surface`, `replacement`, `reason`, conversion or refusal changes, and + neither door moves. `objectstack migrate meta` prints the corrected `verify:` line, and the + generated migration registry carries the same text. +- c7396f1: The `DashboardWidgetOptionsSchema` doc comment no longer says that a misspelled widget `options` key is an author-time type error. The bag ends in `.passthrough()`, so a misspelled key such as `sortDirection` or `granularity` compiles and parses like any other extra key, and it changes nothing at render time. A wrong value for a declared key, such as `sortOrder: 'sideways'`, is the type error and the parse error. `os validate`, `os build` and `os lint` name the misspelled key with the `unconsumed-widget-option` warning, which does not fail any of the three commands. Only the comment changed. The schema's shape is the same. + + Clause-②: no +- 4b59a38: fix(driver-turso)!: a tenant-scoped call on the remote (libSQL) face reaches the rows the local face reaches, and a remote `create` stamps the caller's organization (#21226) + + Clause-②: no (narrowing) + + + + **BREAKING for callers of a remote-mode `TursoDriver` that pass `tenantId`.** + + **What changed.** The engine hands every driver the caller's organization as + `DriverOptions.tenantId`, and the group posture's membership set as `tenantIds` + (ADR-0131 D8). The local face applies them through `SqlDriver.applyTenantScope` + on every read and on every update and delete predicate, and stamps the + organization on a new row. The remote face's doors received no driver options, + so their statements carried the caller's filter and nothing else. Now: + + - **`find`, `findOne`, `count`, `aggregate`, `update`, `delete`, `bulkUpdate`, + `bulkDelete`, `updateMany` and `deleteMany` carry the caller's tenant scope on + the remote face.** The predicate is not a second copy: the remote face asks the + local face's own chokepoint for it and ANDs what that compiles to onto each + statement. So the rows a scoped call reaches are the same on both faces: the + caller's organization, rows with no organization, and, under the group posture, + the caller's membership set. + - **A remote `create` (and `bulkCreate`) stamps the caller's organization** on a + row that names none, as the local `create` does. An explicit value on the row + is kept. + - **`distinct` still refuses a tenant-scoped call on the remote face**, as before. + - A scope the remote face cannot read is refused (`INTERNAL_ERROR` / 500), never + sent without the scope. + + Where the engine's tenant wall composes a predicate above the driver, it already + kept other organizations' rows out of these answers. Where it composes none (the + posture in which that wall is inert, or an elevated caller that carries its + organization), the driver scope is the only fence, and the remote face had none. + + ## FROM → TO + + | you relied on | now | + |:--|:--| + | a tenant-scoped remote `find` / `findOne` / `count` / `aggregate` reading another organization's rows | those rows are excluded (`findOne` answers `null`); call without `tenantId` to read every organization, as on the local face | + | a tenant-scoped remote `update` / `delete` by id reaching another organization's row | `update` answers `null` and `delete` answers `false`, and the row is untouched | + | a tenant-scoped remote `updateMany` / `deleteMany` / `bulkUpdate` / `bulkDelete` reaching another organization's rows | only rows in scope are written; the count reports them | + | a tenant-scoped remote `create` landing a row with no organization | the row carries the caller's organization; to write a row with none, call without `tenantId` | + + **What is not affected.** A call without `tenantId`, or on an object with no + tenant column, sends the same statement as before. The local and embedded-replica + faces are unchanged. A scoped call's answer for the caller's own rows, and for + rows with no organization, is unchanged. +- 0803a8b: docs(spec): a navigation entry's `label` describe states the one order the label resolves in + + Clause-②: no + + The `label` of a navigation entry (`BaseNavItemSchema`) said a present label "renders + verbatim and is never overwritten", which, read literally, forbids the id-keyed localization + `translateApp` already performs at the `/meta` boundary. Its describe and JSDoc now state one + order: the bundle entry `apps..navigation..label` for the active locale chain, keyed + by the entry's `id` and applied by `translateApp` over the app's `navigation` tree (not + `areas`); else a present label as authored — its inline locale map's value for that locale, + else its text; else, when absent, the current label of what the entry opens, at render time, + localized by the target's own translation. A present label is never replaced by its target's + label and never translated by matching its text. No accepted shape changes: `label` stays an + optional `I18nLabel`. +- 0d42104: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `31971ff1e28f`. + + Clause-②: no + + Several records were corrected rather than moved, because objectui changed what they describe on this hop: `object-calendar` and `object-timeline` now read `navigation` with no cast, and `object-kanban`'s read compiles through a declared member, since objectui declared the key on all three blocks (objectui#8652, objectui#8654); `object-timeline` also publishes a `navigation` input (objectui#8654); and the `action:button` registration now publishes the five `size` values its row declares (objectui#11168). Two stale readings are also corrected: the `object-timeline` start/end binding anchor began one line early at the previous pin as well, and the `object-tree` optionality count now records the comment that names the gate in `plugin-map`'s shell. No key, default, enum member or export moves. +- a3d7588: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `db11afd4967c`. + + Clause-②: no + + One claim was falsified rather than moved: `ObjectMapConfigSchema` is `.strict()` at the new pin (objectui#5157), so the `ListMapConfigSchema` record now says the renderer's schema warns on an undeclared key instead of parsing it clean. No key, default, enum member or export moves. +- b8191f7: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `e420df310f5b`. + + Clause-②: no + + Several records were corrected rather than moved, because objectui changed what they describe on this hop: the four `action:*` registrations now publish the keys their rows declare, `endpoint` and `undoable` aside (objectui#11168), `action:group` and `action:menu` now apply a host-evaluated `disabled` that their rows do not declare (objectui#11182, recorded, not declared), the `object-kanban` default row cap is named `DEFAULT_KANBAN_FETCH_BATCH_SIZE` (objectui#9853), the board no longer forwards `quickAdd` (objectui#8285, objectui#11234), the `object-tree` ladder now judges `data` on the `view-data` arm its row declares (objectui#8348), and `object-map` / `object-gantt` / `object-timeline` now publish `filter` and `sort` inputs (objectui#8220). Two older statements that were already stale are also corrected: an authored `data` array on `object-map` no longer reaches the renderer through the React props channel (objectui#9571), and the `quickAdd` retirement record now notes that the schema-only `kanban-ui` block it points to is retired in objectui (objectui#8257). No key, default, enum member or export moves. +- 1741c5d: fix(spec): the `ReportSchema.chart` doc comment says the chart is drawn below the table of a `matrix` report with `columns` + + Clause-②: no + + The doc comment on `ReportSchema.chart` said the embedded chart is plotted above the report's + table. That holds for a `tabular` or `summary` report, and for a `matrix` report without + `columns`, which renders as a grouped table. A `matrix` report with `columns` renders as a + cross-tab, and objectui's `DatasetReportRenderer` draws the chart below it. The comment now + says so. It also drops a clause saying a chart on a `joined` report "parsed and plotted + nothing": the schema refuses that key today, so the clause no longer described it. + + Doc comment only: the schema accepts and refuses the same reports, and no `.describe()` text + or export changes. +- 3711e0b: Four conversion summaries now state their decision in words instead of citing a tracker number + + Clause-②: no + + The `summary` of four ADR-0087 conversions (`datasource-driver-mongo-to-mongodb`, + `translation-component-submit-label-removed`, `mapping-lookup-params-removed` and + `connector-error-mapping-removed`) cited a GitHub issue that no longer exists. That + text is what `os migrate meta`, `spec-changes.json` and the protocol upgrade guide show + an author, so each now says what was decided and why: one driver id for driver and + config contract, retire rather than re-anchor `submitLabel`, remove rather than + implement the import lookup params, and delete `errorMapping` to end its `userMessage` + collision without a rename. Wording only: no conversion id, surface, retirement state, + transform, schema, export or runtime behaviour changes. +- a8acee2: Provenance comments in `conversions/registry.ts` and `integration/connector.zod.ts` were re-anchored + + Clause-②: no + + Thirteen comment and docblock sites in `src/conversions/registry.ts` and + `src/integration/connector.zod.ts` cited tracker numbers that no longer resolve on + GitHub. They now cite the record that decided the matter: ADR-0087's 2026-09-13 + addendum for the data-at-rest seams `retiredFromLoadPath` does not hold back, and + otherwise the commit in this repository's history. Comments only: no type, schema, + export, `describe()` text, conversion `summary` or runtime behaviour changes. +- a51920f: The ADR-0087 migration entries cite the commit that decided each retirement instead of a tracker number that no longer resolves + + Clause-②: no + + The source comments of the migration registry's retired-key, retired-def and semantic + entries named GitHub issues that no longer exist, so a reader could not tell a rule kept + on purpose from one nobody could explain. Each of those comments now names the commit + that made the decision and, where the number alone carried the meaning, says what was + decided. The compiled `@objectstack/spec/migrations` entry carries these comments, which + is why this is a release note at all. Comment text only: no entry id, retired key or def, + prescription, projected upgrade-guide text, schema, export or runtime behaviour changes. +- 0f6dcac: Provenance comments in the rest of `src/` were re-anchored + + The remaining comment and docblock lines in 21 files under `src/` (among + them `api/rest-server.zod.ts`, `system/i18n-resolver.ts`, + `system/operation-message.ts`, `shared/identifiers.zod.ts`, the root + `index.ts` and `data/driver/turso.zod.ts`) cited tracker numbers that no + longer resolve on GitHub. They now cite the commit in this repository's + history that decided the matter, or the ADR amendment that records the + ruling, and say in their own words what was decided. Comments only: no type, + schema, export, message-catalog string or runtime behaviour changes. +- 682873f: Provenance comments in `stack.zod.ts` and `data/analytics.zod.ts` were re-anchored + + Twelve comment and docblock lines in `src/stack.zod.ts` and + `src/data/analytics.zod.ts` cited tracker numbers that no longer resolve on + GitHub. They now cite the commit in this repository's history that decided + the matter: the `themes` carrier retirement, the lowered-handler array form of + `functions`, the closed `ManifestSchema`, the same-key action refusal in + `defineStack` and its cross-stack twin in `composeStacks`, and the + `analytics_cube` binding at the `/meta` write door. Comments only: no type, + schema, export, `describe()` text or runtime behaviour changes. +- 2123fcc: Provenance comments in `ui/` were re-anchored + + Comment and docblock lines under `src/ui/`, and in + `src/data/filter-subtree-provenance.ts` and + `src/meta-spelling/manifest-collection-spelling.ts`, that cited tracker numbers + which no longer resolve on GitHub now cite the commit in this repository's + history that decided the matter, or the ADR amendment they quote, and say in + their own words what was decided. Two references to objectui numbers now name + objectui on each number. Comments only: no type, schema, export or runtime + behaviour changes. + ## 17.5.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index cd2ebdad194..7df91746a50 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.5.0", + "version": "17.6.0", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index 61bfe96c2f9..297066d1828 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,149 @@ # @objectstack/trigger-api +## 17.6.0 + +### Patch Changes + +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 7400a8f728e..5dd26fd9602 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index de57d5aba8b..24cd1b90c12 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,155 @@ # @objectstack/plugin-trigger-record-change +## 17.6.0 + +### Patch Changes + +- 8acdae9: Provenance comments in `trigger-record-change` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + ## 17.5.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 24092f8cb2b..8a74d9e2e4e 100644 --- a/packages/triggers/trigger-record-change/package.json +++ b/packages/triggers/trigger-record-change/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-record-change", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Record-change flow trigger for ObjectStack — auto-launches flows on object insert/update/delete via ObjectQL lifecycle hooks (ADR-0018)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index 8849b4f3472..71b9ced647c 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,173 @@ # @objectstack/plugin-trigger-schedule +## 17.6.0 + +### Patch Changes + +- 91e8fa1: Provenance comments in `trigger-schedule` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no type, schema, export, log or refusal text, or runtime behaviour changes. +- a6866da: fix(core): a date or time in the years 0001..0099 is read as written, not as 1900..1999, wherever a UTC instant is built from year / month / day / time parts + + `Date.UTC(year, …)` and `new Date(year, …)` read a year from 0 to 99 as 1900 + year. Core built its instants from parts that way, so every day of the years 0001..0099 (inside the supported range 0001..9999) landed in the 1900s at the sites below, with no error. + + - `@objectstack/core`: **new export** `wallClockToUtcMs(parts)`, the epoch milliseconds of a `WallClockParts` read as UTC. It is `Date.UTC` without the two-digit-year remap: `month` is 1-12, omitted time components are 0, and every component rolls over past its end as `Date.UTC` rolls it (`month: 13` is next January, `day: 0` the previous month's last day, `hour: 24` the next midnight). A `NaN` component gives `NaN`. Every site below now builds through it: + - `zonedWallClockToUtcMs` and `zonedDateStartToUtcMs`, the wall clock and the zone-offset read. The offset read also takes the zone's era, so a wall clock early on 0001-01-01 in a zone west of UTC, whose offset probe lands in year 0, reads right. + - `bucketKeyToCalendarRange` (`0050` spans 0050-01-01..0051-01-01, not 1950..1951; `0050-01-01` as a `day` key is no longer `null`) and `bucketDateKey`'s ISO week (0050-01-01 is in week 52 of 0049, not of 1949). + - The date macros: `{1976_years_ago}` resolves to `0050-09-30`, not `1950-09-30`. A macro that lands in 0001..0999 is now spelled with a four-digit year, as the `date` storage form spells it (`0055-06-15`, not `55-06-15`, which names no day). + - `@objectstack/service-analytics`: the preview evaluator's `week` key and the `compareTo` bucket alignment build their days through `wallClockToUtcMs`. + - `@objectstack/trigger-schedule`: a time-relative window's day bounds build through `wallClockToUtcMs`. + + What an author sees: `POST /api/v1/data/:object/import` stores the `datetime` cell `0050-01-01 10:00` as `0050-01-01T10:00:00.000Z`, and in `Asia/Shanghai` as `0050-01-01T01:54:17.000Z` (the zone's local mean time for that year). Before, it stored `1950-01-01T10:00:00.000Z` and `1950-01-01T02:00:00.000Z` and reported the row `ok`. Measured through the import route and read back through `POST /api/v1/data/:object/query` on SQLite and PostgreSQL 16; the `2026-07-15 10:00` control is stored the same before and after. Every year from 0100 on builds exactly as before. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [3572916] +- Updated dependencies [3fbf3ca] +- Updated dependencies [24d521e] +- Updated dependencies [b785c3b] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [b9087d7] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [58a77db] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [d2bc644] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/spec@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/metadata-core@17.6.0 + - @objectstack/types@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index 0738e693aa6..6849de52717 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack \u2014 auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)", "main": "dist/index.js", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index af4db02ecdf..716ff4598f9 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,164 @@ # @objectstack/types +## 17.6.0 + +### Minor Changes + +- 8368f1c: feat(types): the data-error classification table (`mapDataError` and its judgements) is exported from `@objectstack/types` (#20919) + + The classification half of `@objectstack/rest`'s error boundary — `mapDataError` + (a thrown error → the `{ status, body }` ADR-0112 answer, without emitting it), + `declaredHttpStatus`, `declaredServerFaultAnswer`, `sandboxBusinessMessage`, + `boundedDeclaredUserMessage`, `boundedDeclaredRefusalMessage` and + `isEngineDuplicateRecordEnvelope` — moved here unchanged, beside the primitives it + composes, so the bulk-import runner in `@objectstack/core` judges a failed row with + the same table the REST door answers with. `@objectstack/rest` keeps the emitters + and re-exports every name it exported before. The module also exports the eight + helpers the REST emitters compose (`truncateClientMessage`, `thrownCodeFields`, + `withoutDeclaredCodePrefix`, `withDeclaredUserMessage`, `isSandboxOrigin`, + `isSandboxCrash`, `fiveXxArmDisplacesDeclared4xx`, `structuredCodeAnswer`). + +### Patch Changes + +- b9087d7: CLI help, warnings and refusals no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Several lines the CLI prints sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `os build` / `os validate`: the provider preflight step now reads "Checking that every required capability has a provider installable in this edition...", and the undeclared-key header reads "Undeclared authoring keys (N) — dropped at load; reported here, never refused". + - `os doctor`: the retired `referenceFilters` row now says the key was removed from FieldSchema as a key no runtime read; the `NODE_ENV` and config-load rows drop their citations. + - `os serve`: the no-auth refusal says anonymous data access is always denied with no setting that turns that off; the organizations remedies drop their citations. + - `os meta resync`, `os db clean` and the `os migrate duplicates` / `multi-value-columns` / `recorded-by` / `summary-nulls` descriptions, the `os dev --restart` flag help, the `os storage orphans` closing line and the storage-driver refusals each say what was decided instead of citing it. + - `os serve`'s unknown-hostname 404 page spells its three short grey colours in six hex digits; they render the same. + - `@objectstack/types`: the host importer's undeclared-package message says the fallback resolves from the caller once `fallbackImport` is passed, and drops the citation beside "Being merely REACHABLE is not enough". + + Text only: no exit code, error code, flag, field or control flow moves. A script that matches the old CLI text (for example the "Checking capability providers" step line) needs the new spelling. +- 00f045d: Provenance comments in `@objectstack/types` were re-anchored + + Comment and docblock lines under `src/` that cited tracker numbers which no + longer resolve on GitHub now cite the commit in this repository's history that + decided the matter, and say in their own words what was decided. Comments + only: no error code, refusal text, type, export or runtime behaviour changes. +- Updated dependencies [e5c7d07] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [24d521e] +- Updated dependencies [3a89d45] +- Updated dependencies [f379f57] +- Updated dependencies [05cb2bc] +- Updated dependencies [7510663] +- Updated dependencies [1a75e39] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [1ab9892] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [ace770d] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [63bfe69] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [c8111a5] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [f10d802] +- Updated dependencies [93e9e42] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [ceee88f] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [32d3b3c] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [a29a0ea] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [b3d7a70] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [27c0cf3] +- Updated dependencies [70dae53] +- Updated dependencies [665cab3] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [336e191] +- Updated dependencies [9bdc6d3] +- Updated dependencies [24c554d] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [1741c5d] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] + - @objectstack/spec@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index f73c29333ba..e1cfb8241ac 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "main": "dist/index.js", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index a49c9a0b451..a9a0516c73a 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,279 @@ # @objectstack/verify +## 17.6.0 + +### Patch Changes + +- 49d2a24: The `verify --rls` report and messages, the dev plugin's tenancy and no-auth messages and the Hono server's no-API warning no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Strings these three packages show to operators, and print in verification reports, sent the reader to an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/verify`, the `objectstack verify --rls` report: the header reads `=== objectstack verify (RLS / cross-owner by-id-write invariant) — ===`, and the position-persona line reads `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed`. + - `@objectstack/verify`, an `rls-hole` verdict's detail: it says the by-id write bypassed RLS, and that a caller that cannot read a record must not be able to write it. + - `@objectstack/verify`, the refusal when the RLS probe persona cannot be provisioned (no ObjectQL engine): it says a by-id write that bypasses RLS is what becomes unreachable. The matching position-persona refusal drops its citation. + - `@objectstack/verify`, the records the probe writes: the probe permission set's row-level-security policy description, the probe `sys_permission_set` row's description and the position persona's `sys_user_position` reason drop their citations. Each already said what it is for. + - `@objectstack/verify`, the `bootStack` refusal for `multiTenant: true` when the app does not declare `@objectstack/organizations`: the citation beside "a package merely reachable through NODE_PATH or a hoisted workspace store is not accepted" goes. + - `@objectstack/plugin-dev`, the `REST API NOT enabled` warning for a stack that mounts no auth: it says anonymous access to object data is always denied, with no setting that turns that off. + - `@objectstack/plugin-dev`, the two refusals for an `OrganizationsPlugin` that refused to be constructed or failed to initialize: they drop their citations. Each already says `OS_ALLOW_DEGRADED_TENANCY` covers only an absent multi-org runtime, not a present one that declined. + - `@objectstack/plugin-hono-server`, the boot warning for a server with no data or discovery API mounted: it drops its citation. It already says the plugin is a transport adapter that serves neither. + + Text only: no status, error code, exit code, route, field, export, verdict or count moves. A log filter or script that matched the old text (for example the report header's `RLS / #NNNN` spelling) needs the new spelling. +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [e5c7d07] +- Updated dependencies [6f1f1c1] +- Updated dependencies [addbbf0] +- Updated dependencies [93d4e0e] +- Updated dependencies [88b484e] +- Updated dependencies [9905e61] +- Updated dependencies [fa0a4b6] +- Updated dependencies [f11b5f2] +- Updated dependencies [0cb72cf] +- Updated dependencies [c1d8051] +- Updated dependencies [a918fe7] +- Updated dependencies [41dcf11] +- Updated dependencies [c46279f] +- Updated dependencies [688ddef] +- Updated dependencies [b1aab1e] +- Updated dependencies [274e162] +- Updated dependencies [05a7547] +- Updated dependencies [0efbdc3] +- Updated dependencies [c8dd8dd] +- Updated dependencies [03cdb9a] +- Updated dependencies [15b586d] +- Updated dependencies [542670d] +- Updated dependencies [e73ee2d] +- Updated dependencies [92fe081] +- Updated dependencies [c4c68ca] +- Updated dependencies [d78a0bd] +- Updated dependencies [5363e2d] +- Updated dependencies [7001918] +- Updated dependencies [c876a74] +- Updated dependencies [f1e921a] +- Updated dependencies [7a1faf1] +- Updated dependencies [c9d234c] +- Updated dependencies [5a23096] +- Updated dependencies [a94f3ba] +- Updated dependencies [3fbf3ca] +- Updated dependencies [eb4b17c] +- Updated dependencies [24d521e] +- Updated dependencies [f4ce10c] +- Updated dependencies [b785c3b] +- Updated dependencies [97005ae] +- Updated dependencies [2473e26] +- Updated dependencies [3a89d45] +- Updated dependencies [c96beb2] +- Updated dependencies [6e3aa75] +- Updated dependencies [f379f57] +- Updated dependencies [889139c] +- Updated dependencies [05cb2bc] +- Updated dependencies [3f45b6c] +- Updated dependencies [14f80e2] +- Updated dependencies [7510663] +- Updated dependencies [4bf4e7e] +- Updated dependencies [4d04b6b] +- Updated dependencies [9a4b2bb] +- Updated dependencies [b80ab57] +- Updated dependencies [d282087] +- Updated dependencies [73155fe] +- Updated dependencies [0e9ad74] +- Updated dependencies [bbe03f4] +- Updated dependencies [a6866da] +- Updated dependencies [1a75e39] +- Updated dependencies [67c1b11] +- Updated dependencies [72f8c38] +- Updated dependencies [cd901d7] +- Updated dependencies [d7631d5] +- Updated dependencies [d830d71] +- Updated dependencies [89801cd] +- Updated dependencies [1ab9892] +- Updated dependencies [10c36cc] +- Updated dependencies [fbec216] +- Updated dependencies [35587f7] +- Updated dependencies [cd6d8a5] +- Updated dependencies [ace770d] +- Updated dependencies [7184436] +- Updated dependencies [ed54768] +- Updated dependencies [99786f9] +- Updated dependencies [5757463] +- Updated dependencies [63bfe69] +- Updated dependencies [96e7244] +- Updated dependencies [defc7f7] +- Updated dependencies [36d043b] +- Updated dependencies [679f95e] +- Updated dependencies [4b45afa] +- Updated dependencies [1940afd] +- Updated dependencies [4f83db5] +- Updated dependencies [f5c7b2c] +- Updated dependencies [6afccda] +- Updated dependencies [671d4c1] +- Updated dependencies [bbcd20c] +- Updated dependencies [165c1d4] +- Updated dependencies [d7b9817] +- Updated dependencies [f80e2a6] +- Updated dependencies [22e584c] +- Updated dependencies [0d9349f] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [c8111a5] +- Updated dependencies [7afdc5c] +- Updated dependencies [9ad6544] +- Updated dependencies [c9c182e] +- Updated dependencies [4b4ee88] +- Updated dependencies [4b4ee88] +- Updated dependencies [e47355b] +- Updated dependencies [b9087d7] +- Updated dependencies [f115b1f] +- Updated dependencies [7c5a311] +- Updated dependencies [49d2a24] +- Updated dependencies [f10d802] +- Updated dependencies [856321f] +- Updated dependencies [76bd58f] +- Updated dependencies [6b004c0] +- Updated dependencies [93e9e42] +- Updated dependencies [157baa7] +- Updated dependencies [ca5408c] +- Updated dependencies [ca5408c] +- Updated dependencies [b280546] +- Updated dependencies [00a92e1] +- Updated dependencies [975b248] +- Updated dependencies [ebb66aa] +- Updated dependencies [793fb83] +- Updated dependencies [ceee88f] +- Updated dependencies [8460592] +- Updated dependencies [e18fea6] +- Updated dependencies [f750119] +- Updated dependencies [660a9b2] +- Updated dependencies [dcd3309] +- Updated dependencies [f6ccca4] +- Updated dependencies [26437ae] +- Updated dependencies [33b6e8b] +- Updated dependencies [cb4c31d] +- Updated dependencies [27bf358] +- Updated dependencies [525b813] +- Updated dependencies [05be352] +- Updated dependencies [d67b942] +- Updated dependencies [8d329f0] +- Updated dependencies [d1633f3] +- Updated dependencies [32d3b3c] +- Updated dependencies [8f78495] +- Updated dependencies [c6b3a01] +- Updated dependencies [bee75ce] +- Updated dependencies [2742e53] +- Updated dependencies [bb2eccf] +- Updated dependencies [75519e1] +- Updated dependencies [a75311d] +- Updated dependencies [d98bf24] +- Updated dependencies [1571aed] +- Updated dependencies [5dbeb7d] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [8368f1c] +- Updated dependencies [31c3996] +- Updated dependencies [95555e7] +- Updated dependencies [657b6b7] +- Updated dependencies [a29a0ea] +- Updated dependencies [de8cd58] +- Updated dependencies [5f6b63a] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [83480c6] +- Updated dependencies [013f97d] +- Updated dependencies [5d5e679] +- Updated dependencies [e07566b] +- Updated dependencies [11d28c1] +- Updated dependencies [399e3aa] +- Updated dependencies [e161ad3] +- Updated dependencies [ae1e950] +- Updated dependencies [ba03198] +- Updated dependencies [94608a7] +- Updated dependencies [c35436c] +- Updated dependencies [9b81314] +- Updated dependencies [58a77db] +- Updated dependencies [a9d36d5] +- Updated dependencies [b3d7a70] +- Updated dependencies [514001a] +- Updated dependencies [b3917d9] +- Updated dependencies [c27404f] +- Updated dependencies [a11faee] +- Updated dependencies [2c1cef3] +- Updated dependencies [27c0cf3] +- Updated dependencies [097ef80] +- Updated dependencies [39ab294] +- Updated dependencies [70dae53] +- Updated dependencies [f20f669] +- Updated dependencies [c6954d6] +- Updated dependencies [2bddb19] +- Updated dependencies [bafb8c9] +- Updated dependencies [9c8b65a] +- Updated dependencies [665cab3] +- Updated dependencies [665cab3] +- Updated dependencies [682873d] +- Updated dependencies [1bd14c9] +- Updated dependencies [432c8ab] +- Updated dependencies [62b90d7] +- Updated dependencies [cb45469] +- Updated dependencies [7a606a9] +- Updated dependencies [f3b16fc] +- Updated dependencies [d6d6e87] +- Updated dependencies [df1feae] +- Updated dependencies [ef96c9e] +- Updated dependencies [336e191] +- Updated dependencies [336e191] +- Updated dependencies [454bbb6] +- Updated dependencies [9bdc6d3] +- Updated dependencies [3a7b6eb] +- Updated dependencies [24c554d] +- Updated dependencies [0b12b9e] +- Updated dependencies [55012df] +- Updated dependencies [ce4e205] +- Updated dependencies [3dc33b2] +- Updated dependencies [9969228] +- Updated dependencies [95e24b0] +- Updated dependencies [1a4c7f8] +- Updated dependencies [c7396f1] +- Updated dependencies [434c6c7] +- Updated dependencies [4b59a38] +- Updated dependencies [4727fcb] +- Updated dependencies [d2bc644] +- Updated dependencies [2791138] +- Updated dependencies [cfa9315] +- Updated dependencies [0803a8b] +- Updated dependencies [0d42104] +- Updated dependencies [a3d7588] +- Updated dependencies [b8191f7] +- Updated dependencies [315888d] +- Updated dependencies [01e78dc] +- Updated dependencies [1741c5d] +- Updated dependencies [04b202e] +- Updated dependencies [a186aea] +- Updated dependencies [3711e0b] +- Updated dependencies [a8acee2] +- Updated dependencies [a51920f] +- Updated dependencies [0f6dcac] +- Updated dependencies [682873f] +- Updated dependencies [2123fcc] +- Updated dependencies [00f045d] + - @objectstack/rest@17.6.0 + - @objectstack/plugin-security@17.6.0 + - @objectstack/spec@17.6.0 + - @objectstack/platform-objects@17.6.0 + - @objectstack/objectql@17.6.0 + - @objectstack/core@17.6.0 + - @objectstack/service-analytics@17.6.0 + - @objectstack/service-automation@17.6.0 + - @objectstack/runtime@17.6.0 + - @objectstack/plugin-auth@17.6.0 + - @objectstack/plugin-sharing@17.6.0 + - @objectstack/service-datasource@17.6.0 + - @objectstack/service-settings@17.6.0 + - @objectstack/types@17.6.0 + - @objectstack/plugin-hono-server@17.6.0 + ## 17.5.0 ### Minor Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index 073a0ca7950..f0e77c004b0 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.5.0", + "version": "17.6.0", "license": "Apache-2.0", "description": "Boot any ObjectStack app in-process and verify it through the real HTTP stack — auto-derived CRUD round-trip fidelity plus the cross-owner RLS invariant. Catches runtime regressions that static checks miss.", "type": "module",