diff --git a/.changeset/27193-recompute-home-amounts.md b/.changeset/27193-recompute-home-amounts.md deleted file mode 100644 index c68142aa0a7..00000000000 --- a/.changeset/27193-recompute-home-amounts.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/cli': patch ---- - -feat(objectql,cli): `os migrate recompute-home-amounts` — recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/objectql`: `recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })`**, with `assertHomeAmountRecomputeSince` (the pure `since` check a door can run before it opens a database), `formatHomeAmountRecomputeReport`, `homeAmountRecomputeComplete` and `HOME_AMOUNT_SKIP_REASONS`. It walks every stored row of an object whose `record`-mode amounts declare `currencyConfig.homeAmount` and projects each through the write-time projection itself (the same rate row, division, single rounding, same-currency rule and organization-timezone day), in the row's own organization, so each organization converts into its own home currency at its own rates and is counted apart. A dry run (the default) reports what would change and writes nothing; `apply` writes only the rows that differ, through `engine.update` with the home field in the payload, so the value stored is the one the write itself computes, and only the home and rate fields move (`updated_at` is kept, so a consumer syncing by `updated_at` does not see the recompute). Being a write, it moves what reads the home field as a fresh write would: stored formulas, parent summaries, and the per-row audit rows where audit capture is installed. Rows that cannot be converted are listed by reason and never written: `rate_missing` (with the currency and date), `code_blank` (an amount with no currency code; the remedy is `os migrate record-currency-codes`), `document_date_blank`, `home_currency_unset` and `unconvertible`. `since` filters on the document date (`homeAmount.rateDate`). One run-level `sys_audit_log` entry records each applied run (counts per organization and reason, and the skipped rows), also when an unclassified error stops the run partway (marked `aborted`, then the error is thrown on). A walk cut short by `maxRecords` is `truncated` and not complete. Refused before any row is read: an unknown object (`OBJECT_NOT_FOUND`), an object or `field` declaring no home amount (`INVALID_FIELD`), a `since` that is not `YYYY-MM-DD` (`VALIDATION_FAILED`). -- **`@objectstack/objectql`: `ObjectQL.homeAmountReadsFor(context?)`**, the reads the write-time projection makes (its serving, and the organization a write converts a row under), for a caller that projects a stored row as a write would. -- **`@objectstack/cli`: `os migrate recompute-home-amounts --object [--field ] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`**, the shell of `os migrate record-currency-codes`, booting the settings service (each organization's `localization.currency` and `.timezone`) and the audit ledger. It exits 1 while any row is listed and skipped, on a dry run as on an apply, because a total over the home field silently leaves such a row out, and when `--max-records` cut the walk short. diff --git a/.changeset/27195-record-currency-door-names-home-field.md b/.changeset/27195-record-currency-door-names-home-field.md deleted file mode 100644 index 9997dcd83ab..00000000000 --- a/.changeset/27195-record-currency-door-names-home-field.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -fix(service-analytics): the refusal of an ungrouped aggregate over a record-currency amount names the home field as the remedy for a consolidated total (ADR-0144 D6) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Now.** A `sum`, `avg`, `min` or `max` over a `currencyMode: 'record'` amount that does not group by its code field is still refused `INVALID_FIELD` / 400 before anything is read, with the same code, status, member, field and object. Its message now names both remedies. For one total in the home currency, point the measure at the amount's home field: by name (`homeAmount.field`) when the host relays it, or else as "the field its `currencyConfig.homeAmount.field` names", with the declaration to add if it has none. For one row per currency, add the dimension over the code field. Before, the message named only the grouping. -- **Unchanged.** A measure over the home field is no record-mode amount, so it is served unchanged: ungrouped, grouped by the code field, and as a `totals` grand total, labelled with the home currency. -- **The widening.** `AnalyticsServiceConfig.sourceFieldMeta` may answer one more optional key, `homeAmountField`: the amount's `currencyConfig.homeAmount.field`, beside a `currencyField` only. `AnalyticsServicePlugin` relays it. A host that does not answer it gets the message without the field's name. diff --git a/.changeset/29041-push-notification-channel.md b/.changeset/29041-push-notification-channel.md deleted file mode 100644 index 67fc4e16a93..00000000000 --- a/.changeset/29041-push-notification-channel.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/service-push': patch -'@objectstack/service-messaging': patch -'@objectstack/spec': patch ---- - -feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM - -Clause-②: yes (widening) -Metadata-protocol: none - -- **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). -- **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. -- **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. diff --git a/.changeset/29255-installer-client-source-skill.md b/.changeset/29255-installer-client-source-skill.md deleted file mode 100644 index dd942483594..00000000000 --- a/.changeset/29255-installer-client-source-skill.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the automation skill's OAuth recipe teaches the installer-supplied client - -Clause-②: no -Metadata-protocol: none - -Recipe 6 of `objectstack-automation/references/integration-recipes.md` now shows `clientSource: 'installer'` beside a declared `clientId` + `credentialRef`, with when to use which: a declared client for an app its publisher deploys, the installer form for one installed on deployments the publisher never sees. The administrator registers a client with the vendor against the `redirectUri` the connection's status read answers, connects with `{ clientId, clientSecret }` (no secret for a public client; a reconnect naming no client keeps the stored one), and the connection reads back `client: { source: 'installer', clientId, hasSecret }`, never the secret. diff --git a/.changeset/29395-skills-search-auto-default.md b/.changeset/29395-skills-search-auto-default.md deleted file mode 100644 index b669604a8e6..00000000000 --- a/.changeset/29395-skills-search-auto-default.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): with no `searchableFields` declared, a field marked `searchable: true` joins the default search set - -Clause-②: no -Metadata-protocol: none - -- **Before.** `objectstack-data`, `objectstack-query` and the `objectstack-ui` list-views rule taught the record-search auto-default as type-only: the name field plus the text-like and `select` columns. A field marked `searchable: true` was described as doing nothing, and a `$searchFields` override naming one as refused. -- **Now.** Each says what the platform resolves: with no `searchableFields` declared, the set `search` scans, and the set a `$searchFields` override may name, is the type-based default plus every field marked `searchable: true`, of any type except `secret`, `password`, `vector` or a virtual `formula`. Each also says what does not change: `searchable: false`, the default, never removes a field the type default scans, and declaring `searchableFields` chooses the set. The enum half of the type default is spelled `select` alone, as the resolver has it. -- No schema, key or behaviour changes. diff --git a/.changeset/29411-platform-skill-package-contributions.md b/.changeset/29411-platform-skill-package-contributions.md deleted file mode 100644 index c6bb512c0b2..00000000000 --- a/.changeset/29411-platform-skill-package-contributions.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the platform skill teaches the `contributions` member of the package customizations read - -Clause-②: no -Metadata-protocol: none - -`skills/objectstack-platform/SKILL.md` now names `contributions` beside the overrides, extensions and switched-off items that `GET /packages//customizations` lists: one entry per item another package adds to the base, stamped with its `packageId`; a `field` or `flow` entry names the base's `object` it is on, a `nav` entry names the base's `app`, its `group` when it targets one, and the item id as `name`; `counts.contributions` counts items, not packages. The text is paid for by deleting three restatements in the same file, so the token ceiling is unchanged. diff --git a/.changeset/29418-record-node-operand-value-docs.md b/.changeset/29418-record-node-operand-value-docs.md deleted file mode 100644 index 613aec2d65b..00000000000 --- a/.changeset/29418-record-node-operand-value-docs.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-automation says a flow's record nodes treat a computed filter operand as a value, never as a query operator - -Clause-②: no -Metadata-protocol: none - -- **Now.** § Filter operands states the rule from the service-automation change: a computed operand is a string, number, bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list of them with no empty entry. Anything else fails the node as a guard before the query runs. The fix: make the expression evaluate to the compared value (for a record, its `id`), and write conditions in the filter itself. The table of failures a `fault` edge does not route now names that refusal beside the `null` operand. -- `SKILL.md` stays under its token ceiling (no ceiling moves). No schema, key, command or behaviour changes. diff --git a/.changeset/29461-d11-added-locale-keeps-write.md b/.changeset/29461-d11-added-locale-keeps-write.md deleted file mode 100644 index 87069733985..00000000000 --- a/.changeset/29461-d11-added-locale-keeps-write.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-platform says the D11 close refuses a package-less write only where the row moves as a declared override, and an added email-template locale keeps that write - -Clause-②: no -Metadata-protocol: none - -- **Before.** The ADR-0146 customization paragraph said a pre-D11 unbound Setup row's package-less save, reset and rollback are refused once it is movable. That is false for a row of an email-template locale the base does not ship: `os migrate packageless-overlays` moves it as the pair's own member (`addedMember`, no `overrides`), and the close judges only the declared-override move, so that row's package-less write stays open before and after its move. The clause also left out publish, which the close refuses too. -- **Now.** The clause says the close covers a row movable as a declared override, lists save, publish, reset and rollback, and says an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The file stays at its token ceiling: a duplicate "CLI Commands" pointer section is cut, its home the Part 3 pointer to `references/operations.md`. No rule is removed; no schema or behaviour changes. diff --git a/.changeset/29506-title-format-refused-teaching.md b/.changeset/29506-title-format-refused-teaching.md deleted file mode 100644 index 864fa67058d..00000000000 --- a/.changeset/29506-title-format-refused-teaching.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): teach that a save and `os validate` refuse `titleFormat` - -Clause-②: no -Metadata-protocol: none - -- `objectstack-data` (object properties table) and `objectstack-formula` (template carriers) no longer call `titleFormat` deprecated: since #29499 the metadata save gate and `os validate` refuse an object that declares it (`title-format-retired`), and it moves to `nameField`. The formula skill drops the sentence that restated the template grammar cell beside it. -- ADR-0079 D3 gains a dated amendment note citing ruling 6574393684 and #29499; the decision is unchanged. diff --git a/.changeset/29512-approval-decision-outputs-skill.md b/.changeset/29512-approval-decision-outputs-skill.md deleted file mode 100644 index 351005244e4..00000000000 --- a/.changeset/29512-approval-decision-outputs-skill.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the automation skill teaches that an approval's decision outputs are enforced at the decision - -Clause-②: no -Metadata-protocol: none - -- **Before.** `skills/objectstack-automation/references/state-machines-and-approvals.md` said only that a typed `decisionOutputs` entry renders the matching record picker in the decision dialog (`multiple` collects an id array). An agent writing an approval flow from it could read `type` and `multiple` as widget hints and not know that a value the next step cannot use is refused. -- **Now.** The *Recording a decision* section says what the approvals service enforces, on approve and reject alike and before any write: a picker value must resolve through the `expression` approver's resolvers (`user` by any account's record id, `department` by record id in the request's organization, `team` by record id not owned by another organization, `position` by the machine name of a position someone in that organization holds); `multiple: true` takes a list of distinct values for every kind, bare keys included, and without it one value; any other non-blank value is refused with `400 VALIDATION_FAILED` naming `outputs.KEY`. The Console sends a list for a `multiple: true` text output. The existing `lead_review` node is the example. The file stays within its token ceiling (5724 of 5730). diff --git a/.changeset/29801-related-list-placement-skill.md b/.changeset/29801-related-list-placement-skill.md deleted file mode 100644 index b43fb73bcbe..00000000000 --- a/.changeset/29801-related-list-placement-skill.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): objectstack-data teaches `relatedList` placement by each value's role, not by record-page tabs or columns - -Clause-②: no -Metadata-protocol: none - -- **Before.** `rules/relationships.md` said `relatedList: 'primary'` draws "a full-width table in the details column after the first field section (in the Details tab, no tab of its own)" and that `true` / omitted is "a side card (the shared **Related** tab)". The spec's `relatedList` describe no longer names any tab or column, so the skill taught a page layout the record page does not draw. -- **Now.** The three bullets follow the spec describe: `'primary'` is part of the record's main content, a full-width table in the record's detail body (still the default for an inline-edited `master_detail` FK); `false` means the child object is not shown on the parent record page; `true` or omitted is an auxiliary panel on the record page. Layout stays ADR-0085's to define. -- The file gets smaller and stays under its token ceiling; no ceiling moves, no schema, key or behaviour changes. diff --git a/.changeset/29820-push-capability-serve.md b/.changeset/29820-push-capability-serve.md deleted file mode 100644 index b5b11c7950c..00000000000 --- a/.changeset/29820-push-capability-serve.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor -'@objectstack/service-push': minor -'@objectstack/cli': minor -'@objectstack/verify': minor ---- - -feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot - -Clause-②: yes (widening) -Metadata-protocol: added -Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` - -A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. - -- **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. -- **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. -- **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. -- **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. - -To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: - -```ts -push: { - transport: 'direct', - apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, - fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, -}, -``` - -In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. diff --git a/.changeset/29872-export-carries-manifest-object-extensions.md b/.changeset/29872-export-carries-manifest-object-extensions.md deleted file mode 100644 index f4d7d137b50..00000000000 --- a/.changeset/29872-export-carries-manifest-object-extensions.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/metadata-protocol': patch -'@objectstack/runtime': patch ---- - -fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not - -Clause-②: yes (widening) -Metadata-protocol: none - -A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. - -- **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. -- **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. -- **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. - -`SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. - -Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. diff --git a/.changeset/29873-install-prints-withheld-maps.md b/.changeset/29873-install-prints-withheld-maps.md deleted file mode 100644 index 77bc00adb92..00000000000 --- a/.changeset/29873-install-prints-withheld-maps.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os package install` prints the connector maps the package export withheld (#29873) - -Clause-②: no -Metadata-protocol: none - -An exported package names each connector header or environment map the export withheld whole in its `withheldMaps`, and both install modes of the runtime's install-local endpoint name each map in their answer. The command dropped that list, so an operator who installed an export the documented way (`os package install DIR/NN-ID/objectstack.json`) was never told that the connector sends none of those headers or environment variables. - -The command now prints one warning, one line per map (`connector 'NAME' PATH`, for example `connector 'erp' providerConfig.headers`), and the remedy: re-enter each map where the connector is authored. It does this for a managed install (hot-loaded or not) and for an unmanaged one, from a catalog id or an artifact file. An answer without the list prints nothing new, and the exit code is unchanged. The command has no `--json` output. diff --git a/.changeset/29879-datasource-update-kept-pool-stores-nothing.md b/.changeset/29879-datasource-update-kept-pool-stores-nothing.md deleted file mode 100644 index 79bba6191d6..00000000000 --- a/.changeset/29879-datasource-update-kept-pool-stores-nothing.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource)!: a datasource update whose new configuration fails to connect stores none of it, and a whitespace-only `secret` is refused like an empty one - -Clause-②: yes (narrowing) — `PATCH /api/v1/datasources/:name` and `POST /api/v1/datasources` refuse one more body that used to answer 200 / 201: a `secret` whose value is whitespace only (`" "`, a tab and a newline), now `400 DATASOURCE_ADMIN_ERROR` with the empty-secret answer. The widening half: the `reregisterPool` member of the exported `DatasourceAdminServiceConfig` may now answer `{ previousPoolKept: boolean }` (still optional; a seam that answers nothing reads as before). -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`): a whitespace-only `secret` is refused. - -- **Before.** A `PATCH` whose new configuration failed to connect (a wrong password, a blank `secret`, a changed `config` that does not reach the database) answered `200` with `status: "error"` and a reason saying the previous pool was kept and the new configuration was NOT in force. It had already stored the new record, repointed `external.credentialsRef` at a new `sys_secret` row and removed the working one, so the next restart connected the configuration that failed. A whitespace-only `secret` passed the empty-secret guard and reached this path. -- **Now.** The update rebuilds the pool before it stores anything. When the previous pool is kept, nothing of the patch is stored: the record, its `credentialsRef` and every other key of the patch stay as they were, and the `sys_secret` row sealed for the patch is removed again. The answer is unchanged (`200`, `status: "error"`, "the new configuration is NOT in force"), and now true: a restart connects the configuration that was serving. A configuration that connects is stored and in force, and the replaced secret is retired, as before. A whitespace-only `secret` is refused before anything is written, by the same guard and with the same message as the empty string; a value with whitespace around other characters is still sealed as sent. -- **Who moves.** The console sends `secret` only when the field holds a value. A script or SDK call that sends a whitespace-only `secret` gets the 400. A host that wires its own `reregisterPool` seam keeps today's behaviour until it answers `{ previousPoolKept: true }` for a rebuild that kept the previous pool. - -**The remedy.** To keep the stored credential, leave `secret` out of the request (or send `null`). To replace it, send the new credential. diff --git a/.changeset/29891-email-send-display-name-400.md b/.changeset/29891-email-send-display-name-400.md deleted file mode 100644 index af6cd8bee88..00000000000 --- a/.changeset/29891-email-send-display-name-400.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-email': patch ---- - -fix(plugin-email): a non-string recipient display name answers the send door's 400, not 500 - -Clause-②: no -Metadata-protocol: none - -`POST /api/v1/email/send` with a recipient such as `{ "address": "ok@example.com", "name": 42 }` in `to`, `cc`, `bcc` or `replyTo` (or as one entry of a `to` / `cc` / `bcc` list) now answers `400 VALIDATION_FAILED` with "invalid recipient address in `to`: the display name of ok@example.com must be a string" (`reply-to address` for `replyTo`). It used to answer `500 EMAIL_SEND_FAILED` with `a.name?.trim is not a function`. - -The refusal itself was already right. The 500 came from the code that writes the `sys_email` rejection row after the refusal: it called `.trim()` on the caller's raw display name. That writer now reads the name as unknown input and leaves a non-string name out of the row (the row's `error` column names it), so the caller gets the 400 and the rejection is still recorded. A string name, an absent name and a `null` name behave as before. diff --git a/.changeset/29894-batch-row-hook-aborted.md b/.changeset/29894-batch-row-hook-aborted.md deleted file mode 100644 index 6e0011144c1..00000000000 --- a/.changeset/29894-batch-row-hook-aborted.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): a hook that aborts with free text answers `HOOK_ABORTED` on the per-object batch row too, not `VALIDATION_ERROR` - -Clause-②: no -Metadata-protocol: none - -- **Before.** Since #27597 (and #27672) a sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany` and `/api/v1/batch` with `400 HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/batch` answered the same abort as a row `{ code: 'VALIDATION_ERROR', message: }`: the row mapper (`toRowApiError`) derived the code from the `400` the doors serve, so a bulk client branching on the code read a business refusal as malformed input. -- **Now.** The row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read). The value moves, from `VALIDATION_ERROR` to `HOOK_ABORTED`, on every row that shares the mapper: `POST /api/v1/data/:object/batch` with `operation` `create`, `update`, `upsert` or `delete`, in every mode (plain, `atomic`, `continueOnError`), and the per-row results of `updateManyData` / `deleteManyData`. -- **Unchanged.** A code the hook declared still wins (a registered code is its own row code; an unregistered one still gets the code its status derives). The row's `message` is still the hook's sentence and `httpStatus` is still absent when the body declared no status. A crashed body and a non-sandbox producer answer as before. No error code is added; `HOOK_ABORTED` is the one #27597 registered. diff --git a/.changeset/29895-user-locale-case-fold.md b/.changeset/29895-user-locale-case-fold.md deleted file mode 100644 index d078288bb06..00000000000 --- a/.changeset/29895-user-locale-case-fold.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix: a user's language preference is stored in its BCP-47 case, so `zh-cn` and `ZH-CN` are saved as `zh-CN` and the console renders that user's language (#29895) - -Clause-②: no -Metadata-protocol: none - -`sys_user.locale` used to store a tag as written. The column checks only the shape of a tag, so `zh-cn` was accepted, and the console, which picks its UI language by exact match, then rendered English for that user with no error. Every write of the column is now stored in BCP-47 case: language lower case, region upper case, script title case (`ZH-HANS-cn` → `zh-Hans-CN`). This applies to the data API (`PATCH /api/v1/data/sys_user/:id` and its bulk form), the admin user import and the identity-provider locale sync. The fold is the one `LocaleSchema` applies to translation locales. Nothing new is refused: `zh_CN` is still refused with 400, and an empty value still clears the preference. Rows stored mis-cased before this release are not rewritten. Such a row reads back as stored until the column is written again. diff --git a/.changeset/29896-field-sortable-false-honoured.md b/.changeset/29896-field-sortable-false-honoured.md deleted file mode 100644 index ee304c4221e..00000000000 --- a/.changeset/29896-field-sortable-false-honoured.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/mcp': minor ---- - -fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) - -Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. - -- **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. -- **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. -- **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. -- **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. - -**The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. diff --git a/.changeset/29897-properties-panel-readonly-reason.md b/.changeset/29897-properties-panel-readonly-reason.md deleted file mode 100644 index c2425b3e4b1..00000000000 --- a/.changeset/29897-properties-panel-readonly-reason.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): on a read-only item, the Properties panel shown with nothing selected says why its controls are disabled - -Clause-②: no -Metadata-protocol: none - -On the metadata editor page (Studio's All metadata item page, Setup's metadata pages), a locked item, a read-only package, a caller without `manage_metadata`, a code-shipped artifact or a walled catalog type disables every control in the Properties panel. The scoped inspector (a selected flow node or edge) already said why; the panel shown with nothing selected (an object's or view's default inspector, or the generic form a flow shows) greyed its controls silently, and Tab never reached them. The page now wraps that panel, and the generic form on a page without a canvas, in the same reason wrapper the scoped inspectors use: hovering a disabled control or tabbing to the panel opens a tooltip with the reason, and the panel's accessible description is that reason. A writable item renders the panel exactly as before. No registered default inspector changes: the page draws the reason around it. diff --git a/.changeset/29898-picklist-binding-unknown-refused.md b/.changeset/29898-picklist-binding-unknown-refused.md deleted file mode 100644 index 07e2bb8fb19..00000000000 --- a/.changeset/29898-picklist-binding-unknown-refused.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/spec': minor -'@objectstack/cli': minor ---- - -fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) - -Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. - -- **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. -- **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. -- **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. -- **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). -- **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. - -**FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. diff --git a/.changeset/29899-publish-sheet-picklist-options.md b/.changeset/29899-publish-sheet-picklist-options.md deleted file mode 100644 index 15dc3f16e94..00000000000 --- a/.changeset/29899-publish-sheet-picklist-options.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): Studio's publish review sheet no longer lists an `Options` change on a picklist-bound field nobody edited (#29899) - -Clause-②: no -Metadata-protocol: none - -On an object with a field bound to a package picklist (`{ type: 'select', picklist: 'NAME' }`), the publish review sheet listed `~ FIELD · Options` on every publish, beside the change the author actually made. The draft is stored without the field's `options`, while the live read serves the options the runtime resolved from the picklist, and the sheet compared the two as read. - -The sheet now compares both versions of an object in the form the publish door stores: a field that names a picklist is read without `options` on both sides. A field with its own inline `options` is untouched, so a real options change there is still listed, and a field moved between a picklist and inline options still lists both keys. diff --git a/.changeset/29900-report-runtime-filter-field-unknown.md b/.changeset/29900-report-runtime-filter-field-unknown.md deleted file mode 100644 index c16ebb24d95..00000000000 --- a/.changeset/29900-report-runtime-filter-field-unknown.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -fix(lint)!: a report whose `runtimeFilter` names a field its dataset's object lacks is refused at publish (`report-filter-field-unknown`), as the dashboard twin already is (#29900) - -Clause-②: yes (narrowing) — the report publish door, `os validate` and `os build` refuse a report `runtimeFilter` key they accepted before; `@objectstack/lint` also exports `validateReportRuntimeFilters`, `REPORT_FILTER_FIELD_UNKNOWN` and the `ReportRuntimeFilterFinding` type. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` and `content/docs/**` teach `runtimeFilter` keys that are fields of the dataset's object, which this rule accepts; no page teaches a key the object lacks. The reasoning ships as the `os explain report-filter-field-unknown` entry. - - - -**BREAKING** accept-set narrowing at the report publish door (`PUT /api/v1/meta/report/NAME`, `POST /api/v1/meta/report/NAME/publish`, package draft publish; Studio, REST and MCP authors) and in `os validate` / `os build`. - -- **Before.** A report with `runtimeFilter: { no_such_field: 'x' }` over a dataset whose object has no `no_such_field` published `200`, and every reader of the report saw a "Dataset query failed: 400 Bad Request — Filter member no_such_field …" panel instead of numbers. The same key on a dashboard widget's `filter` was already refused `422` (`widget-filter-field-unknown`). -- **Now.** It is refused `422 INVALID_METADATA` with `report-filter-field-unknown` at `reports[0].runtimeFilter.no_such_field`, and the verdict names the object and the nearest field ("Did you mean …"). A `joined` report's block filter is judged against the block's dataset, and the report-level `runtimeFilter` (merged into every block's query) against each block's dataset. -- **Accepted, as on a widget.** A field of the dataset's object, a registry-injected system column (`created_at`, `owner_id` where injected), a dotted relationship path, and every `$` operator and combinator. One more key is accepted on a report: a bare key naming a dimension of the bound dataset, which the dataset query binds through that dimension. -- **Not judged.** A `dataset` that resolves to nothing (`chart-dataset-unknown` reports it once), an object the stack (or the publish snapshot) does not carry, an object with no readable field map, and a dotted key whose prefix the dataset's `include` does not declare. -- **`os explain report-filter-field-unknown`** prints the reasoning; `RULE_EXPLANATIONS` gains the entry. - -**What an author does.** Nothing for a report whose filter keys name real fields or dimensions. A refused key never served the report (every reader saw the error panel), so there is no rewrite that keeps what the author meant: name a field of the dataset's object, a dimension of the dataset, or a `relationship.field` path whose prefix the dataset's `include` declares. The refusal names the key and the nearest field. diff --git a/.changeset/29901-save-status-held.md b/.changeset/29901-save-status-held.md deleted file mode 100644 index 527a413be40..00000000000 --- a/.changeset/29901-save-status-held.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): the metadata editor's save-status chip says "Not saved" while a save is held, instead of promising an auto-save that never comes (#29901) - -Clause-②: no -Metadata-protocol: none - -On the metadata editor page (Studio's All metadata item page, Setup's metadata pages) with auto-save on, a draft the server refused with `422 INVALID_METADATA` stays dirty, and the chip beside Save kept reading "Auto-saving…" although the auto-save is held until the refused part changes and no further save is sent. The same happened while the inspector holds a fault it reports (a CEL predicate that does not parse), which holds the auto-save the same way. - -The chip now has a held state: while either hold stands on a dirty draft it reads "Not saved — fix the errors to save" in the destructive tone, with the Save button's own explanation as its tooltip. It returns to the usual states as soon as the hold lifts: editing the part the server named, or fixing the inspector fault, lets the auto-save run and the chip reads "Auto-saving…", then "Saved". Nothing about when a save is sent or held changes. New string: `engine.edit.notSavedHeld` (en, zh). diff --git a/.changeset/29903-base-export-without-pair-extension.md b/.changeset/29903-base-export-without-pair-extension.md deleted file mode 100644 index 62cee36a241..00000000000 --- a/.changeset/29903-base-export-without-pair-extension.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/metadata-protocol': patch ---- - -fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension - -Clause-②: yes (widening) -Metadata-protocol: none - -After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages//export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged. - -- **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension. -- **The pair's export** is unchanged: it carries the extension in `objectExtensions`. -- **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in. - -An extension contributed by any other installed package is still folded into the base's object in the base's export, as before. - -`SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it. diff --git a/.changeset/29904-type-mismatch-fail-closed.md b/.changeset/29904-type-mismatch-fail-closed.md deleted file mode 100644 index 8d182206707..00000000000 --- a/.changeset/29904-type-mismatch-fail-closed.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/formula': minor -'@objectstack/lint': minor -'@objectstack/mcp': minor -'@objectstack/spec': minor ---- - -fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) - -Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. -Metadata-protocol: narrowed - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. - -- **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". -- **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. -- **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. -- **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. -- **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). -- **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. - -**FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. diff --git a/.changeset/29909-revoke-sessions-on-password-reset-retired.md b/.changeset/29909-revoke-sessions-on-password-reset-retired.md deleted file mode 100644 index 54236248a92..00000000000 --- a/.changeset/29909-revoke-sessions-on-password-reset-retired.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-auth': minor ---- - -feat(spec)!: retire `emailAndPassword.revokeSessionsOnPasswordReset` — a completed password reset always ends every session the account holds, and there is no setting (#29909) - -Clause-②: no (narrowing) — `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` becomes a `retiredKey()` tombstone, so the schema parse and `tsc` refuse it at any value, and plugin-auth's entry guard refuses it at any value too, `true` included, which it accepted before. Nothing is widened. -Metadata-protocol: narrowed - - - -**BREAKING** for a configuration that still writes `emailAndPassword.revokeSessionsOnPasswordReset`, at any value. - -- **`@objectstack/spec`.** `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` is retired (ADR-0049 enforce-or-remove). A completed self-service password reset already ended every session the account holds, on every reset door, and an explicit `false` was already refused at boot, so the key could hold one value only. It is now a tombstone: a typed configuration that writes it fails `tsc`, and `AuthConfigSchema` / `EmailAndPasswordConfigSchema` refuse it with the prescription. The authorable-surface row reads `[RETIRED]`, `RETIRED_KEYS_BY_MAJOR[18]` lists `system/EmailAndPasswordConfig:revokeSessionsOnPasswordReset`, and the upgrade guide carries the semantic entry `email-and-password-revoke-sessions-on-reset-retired`. The prescription is exported as `REVOKE_SESSIONS_ON_PASSWORD_RESET_RETIRED` (`@objectstack/spec/system`). No ADR-0087 conversion: the key lives in the configuration a host hands the auth plugin, never in a stack or a stored row, so `os migrate meta` has nothing to rewrite. -- **`@objectstack/plugin-auth`.** The auth manager takes its configuration unparsed, so it refuses the retired key itself, where the configuration enters: at construction, which stops the boot, and on a configuration patch, where the standing configuration keeps ruling. It now refuses `true` as well as `false`, with the tombstone's own prescription. `true` asked for what the platform does anyway; the key is to be deleted either way. The auth instance still sets better-auth's own option of that name itself, so a completed reset ends the account's sessions exactly as before. -- **Unchanged.** What a password reset does: every self-service reset door ends every session the account holds, and each ended session is stamped `revoke_reason: password_reset`. An absent key is accepted, as before. - -**The remedy.** Delete `revokeSessionsOnPasswordReset` from the `emailAndPassword` block of the auth configuration, whatever it holds; nothing replaces it. diff --git a/.changeset/29910-no-submitter-returned-docs.md b/.changeset/29910-no-submitter-returned-docs.md deleted file mode 100644 index 80cef7b0adf..00000000000 --- a/.changeset/29910-no-submitter-returned-docs.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(approvals): say how a request with no submitter leaves the `returned` state - -Clause-②: no -Metadata-protocol: none - -- **`@objectstack/spec`: comment text only.** The `IApprovalService` docblocks on `recall`, `sendBack` and `resubmit` now state the path for a request with no submitter (its run's trigger resolved no user): nobody can recall it once it is `returned`, a system-context `resubmit` is its way out, and the next round opens `pending` with no submitter, where the system or the override actor may recall it. The `resubmit` docblock now names the system context it already admits. No key, type, schema or behaviour changes. diff --git a/.changeset/29916-connection-error-no-invented-503.md b/.changeset/29916-connection-error-no-invented-503.md deleted file mode 100644 index 4a1e90b7705..00000000000 --- a/.changeset/29916-connection-error-no-invented-503.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/data-objectstack': patch ---- - -Clause-②: no -Metadata-protocol: none - -A `ConnectionError` for a request nothing answered (an unreachable host, a rejected `fetch`) no longer reads as a server that answered with a 503 (#29916). - -- The `ConnectionError` constructor leaves `statusCode` undefined when the caller passes none. Before, it stamped `503`. `connect()` builds it that way when discovery's `fetch` rejects, every data method opens with `await this.connect()`, and the post-connect `error` connection state builds it the same way, so a read against a down backend was taken for the server's own answer. `answeredLoadFailure` now returns `null` for it and `classifyLoadError` reads `network`, so a failed-read state keeps its "check your connection" sentence. -- A discovery read the server answered with an error status (`!res.ok`) now carries that status (a 401 reads as `unauthorized`), instead of the invented 503. -- An answered 503 or 504 through `createErrorFromResponse` keeps its status, as before. diff --git a/.changeset/29918-lifecycle-first-sweep-fresh-store.md b/.changeset/29918-lifecycle-first-sweep-fresh-store.md deleted file mode 100644 index ca0b038457f..00000000000 --- a/.changeset/29918-lifecycle-first-sweep-fresh-store.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): the lifecycle sweep no longer runs 60 s after a boot that created its datastore from empty; its first sweep runs one interval later - -Clause-②: yes (widening) — the exported `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean` (an engine without it keeps the old schedule); no accept set changes. -Metadata-protocol: none - -- `LifecycleService.start()` asks the engine, at the initial delay, whether this process created the datastore from empty (`ObjectQL.wasDatastoreCreatedFromEmpty()`, answered from every driver's schema-sync stats without a statement). When it did, the scheduled first sweep is skipped and the sweep runs on the interval, one interval later. Such a store holds no row older than the boot, and no declared window is shorter than an hour, so the skipped sweep had nothing to reap. It still read every declared object, the `lifecycle` settings and every lookup the reference audit walks: 130 statements in the first minute of a hosted tenant kernel's first boot. -- A store that existed before the boot, or an engine that cannot say, sweeps at the initial delay as before. A direct `sweep()` call always sweeps. `OS_LIFECYCLE_DISABLED`, `initialDelayMs` and `sweepIntervalMs` keep their meaning. -- `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean`. An engine double that omits it keeps the old schedule. -- A re-boot on an existing store still sweeps at the initial delay. Persisting a last-swept watermark for that case is separate work. diff --git a/.changeset/29919-dispatch-loop-next-due-wake.md b/.changeset/29919-dispatch-loop-next-due-wake.md deleted file mode 100644 index e93288f72b4..00000000000 --- a/.changeset/29919-dispatch-loop-next-due-wake.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/service-messaging': patch -'@objectstack/service-queue': patch ---- - -fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. -- **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). -- **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. -- **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. - -Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. diff --git a/.changeset/29926-type-mismatch-outcome-per-role.md b/.changeset/29926-type-mismatch-outcome-per-role.md deleted file mode 100644 index fcdd474ae02..00000000000 --- a/.changeset/29926-type-mismatch-outcome-per-role.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/formula': minor -'@objectstack/lint': minor -'@objectstack/mcp': minor -'@objectstack/spec': minor ---- - -fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) - -Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. -Metadata-protocol: narrowed -Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. - -- **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. -- **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. -- **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. -- **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. -- **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. - -**FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. - -**The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. diff --git a/.changeset/29934-dev-start-one-data-key-file.md b/.changeset/29934-dev-start-one-data-key-file.md deleted file mode 100644 index 5821789659f..00000000000 --- a/.changeset/29934-dev-start-one-data-key-file.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-settings': patch ---- - -fix(cli): `os dev` and `os start` share one data-key file, so a secret saved under `npm run dev` reads back under `os start` - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/cli`.** Both commands open the same project database, but `os start` used to mint its own data key under its own home (`PROJECT/.objectstack/dev-crypto-key` in project mode) while `os dev` kept the key at `$HOME/.objectstack/dev-crypto-key` (or `$OS_HOME`'s). Every secret written during development then read back as unset under `os start`. Both commands now resolve the key file the way `os dev` always has (`os start --home` counts as `OS_HOME`) and hand it to their `serve` child on the internal `OS_INTERNAL_CRYPTO_KEY_FILE` channel. The project-local file an earlier `os start` minted is not orphaned: when it alone holds a key it is still read, and the boot says which file; when it and the shared file hold different keys, the shared one is used and a warning names both paths. An explicit `OS_SECRET_KEY` / `OS_DEV_CRYPTO_KEY` still wins, unchanged. -- **`@objectstack/service-settings`.** New exports `resolvePersistedKeyFile` (the one resolution both commands take, beside the provider's own key-file path) and `KEY_FILE_CHANNEL_ENV`; the local crypto provider's file tier reads the file that channel names, ahead of `OS_HOME`. diff --git a/.changeset/29935-update-action-refreshes-record.md b/.changeset/29935-update-action-refreshes-record.md deleted file mode 100644 index 7cb3daa6964..00000000000 --- a/.changeset/29935-update-action-refreshes-record.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/core': patch ---- - -fix(core): a successful `operation: 'update'` action now refreshes the record it wrote, with no `refreshAfter: true` (#29935) - -Clause-②: no -Metadata-protocol: none - -The spec fills in `refreshAfter: false` on an action authored without it, and that parsed shape is what the Console is served. The server-action dispatch (`createServerActionHandler`) refreshed only when `refreshAfter !== false`, so a declared update never refreshed: the route wrote the record and answered 200, the toast reported success, and the record page kept the old values and the action button whose `visible` predicate had turned false. The list the record came from kept it under a filter it no longer matched. - -A declared `operation: 'update'` writes the record it addresses by definition, so its success is now treated as a record change whatever `refreshAfter` says, the same way a client-side `dataSource.update` already reaches every reader. The record-scoped invalidation re-reads the record page (drawer and full page) and refetches every list of that object. `refreshAfter` keeps governing every other server action: a non-update action served with the default `refreshAfter: false` still sends no re-read. The spec default is unchanged. diff --git a/.changeset/29942-related-list-title-translation.md b/.changeset/29942-related-list-title-translation.md deleted file mode 100644 index 5ab69e6ea05..00000000000 --- a/.changeset/29942-related-list-title-translation.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/cli': minor -'@objectstack/metadata-protocol': minor ---- - -feat(spec): a lookup or master-detail field's related-list title is translatable - -Clause-②: yes -Metadata-protocol: added - -A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. - -- `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. -- It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. -- `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. -- The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. diff --git a/.changeset/29943-body-table-totals-additive-columns.md b/.changeset/29943-body-table-totals-additive-columns.md deleted file mode 100644 index 661e3874efb..00000000000 --- a/.changeset/29943-body-table-totals-additive-columns.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): a record page's body tables total only the columns declared additive — a version number or an instalment number is no longer summed - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** The 「Totals (this page)」 line under a body table of the default record page (a related list drawn full width in the details column) used to add up every `currency`, `number`, `integer` and `decimal` column. Ordinals were summed with the quantities: a contract's versions v1–v4 read 「版本号 10」, five instalments 「期次 15」. The line now totals: - -- a `currency` column, when every counted row is in one known currency, formatted in it. Each row's currency is the one its cell displays (`resolveFieldCurrency`): the field's code under `currencyMode: 'fixed'`, the tenant default under `dynamic`, the row's own code field under `record`. Rows in different currencies, or a row whose currency is unknown (a `record`-mode row with a blank code, a `dynamic` amount with no tenant default), give that column no total rather than a bare or wrong-currency sum. A `currencyConfig` without `currencyMode: 'fixed'` no longer formats in its `defaultCurrency`, which the spec does not read outside `fixed`; it shows the tenant default, as the column's cells do; -- a `number`, `integer` or `decimal` column only when the record's own object declares a `summary` field whose `summaryOperations` sums that child field (`function: 'sum'`, `object` naming the child). The roll-up is the author's statement that the column adds up. Without one, as for an amount stored as `number` with its currency on the parent, the column gets no total; -- nothing else. When no column qualifies the line is not drawn. diff --git a/.changeset/29945-body-table-overflow-cues.md b/.changeset/29945-body-table-overflow-cues.md deleted file mode 100644 index 80e7b179de9..00000000000 --- a/.changeset/29945-body-table-overflow-cues.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/components': patch -'@object-ui/i18n': patch ---- - -fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 - -Clause-②: no -Metadata-protocol: none - -A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: - -- **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). -- **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. -- **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. - -A body table that fits shows none of these: no pinned column and the icon-only ⤢. - -`@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. diff --git a/.changeset/29946-record-page-user-field-related-list.md b/.changeset/29946-record-page-user-field-related-list.md deleted file mode 100644 index 9e16bf8fe8d..00000000000 --- a/.changeset/29946-record-page-user-field-related-list.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the one-request record page accepts a related list keyed by a `user` field again - -Clause-②: no -Metadata-protocol: none - -- **Regression in 18.3.0 (#29946, duly#185).** `POST /data/:object/:id/page` (#27713) checked each related list's field against a private `lookup`/`master_detail` list and read `reference` raw, so a related list on `sys_user` keyed by a `user` field (an owner, an assignee) answered `400 VALIDATION_FAILED … is not a lookup or master-detail field that references 'sys_user'`, and the console's full-page record view showed "could not load this record". The related list's own row query (`{ [field]: id }`) follows that field, and 18.2.0 read each related list through that list read, with no type check. -- The check now asks the spec's one arbiter of a field's target, `referenceTargetOf` (`lookup`, `master_detail`, `user`, `tree`; a `user` field's target is fixed to `sys_user` by its type, whether or not its `reference` spells it), the same judgement the engine's `$expand` and delete walk read. A field that does not reference the page's object is still a located `400 invalid_relationship`: a `user` field on any other object's page, a `lookup` to another object, a `tree` on another object, an unreadable `reference` carrier. The message now reads `… is not a lookup, master-detail, user or tree field that references ''.` diff --git a/.changeset/29951-page-header-placed-actions-reader.md b/.changeset/29951-page-header-placed-actions-reader.md deleted file mode 100644 index 156bc3c88b3..00000000000 --- a/.changeset/29951-page-header-placed-actions-reader.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/components': patch -'@object-ui/types': patch -'@object-ui/app-shell': patch -'@objectstack/spec': patch -'@objectstack/platform-objects': patch -'@objectstack/plugin-approvals': patch ---- - -fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) - -Clause-②: no -Metadata-protocol: none - -- **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. -- **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. -- **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. -- `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. diff --git a/.changeset/29951-skills-placed-actions-drawn.md b/.changeset/29951-skills-placed-actions-drawn.md deleted file mode 100644 index 49fd6156d74..00000000000 --- a/.changeset/29951-skills-placed-actions-drawn.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): `objectstack-ui` no longer says `page:header`'s `placedActions: true` is not drawn, and an eval exercises it (#29951) - -Clause-②: no -Metadata-protocol: none - -- `rules/pages.md`: the component catalogue's `page:header` row says `placedActions: true` draws every placed action, read live. The console's header renderer reads the key since #29951's ui half, so the "not drawn yet" qualifier #30018 wrote is dropped. -- `evals/record-page.json`: a third case asks for a custom full record page whose header shows every action packages place on the object; the expected answer keeps the page and its tab strip and sets `placedActions: true` on the `page:header` node, with no `actions` id list beside it. diff --git a/.changeset/29954-disabled-reason-tooltip-width.md b/.changeset/29954-disabled-reason-tooltip-width.md deleted file mode 100644 index 0547003023a..00000000000 --- a/.changeset/29954-disabled-reason-tooltip-width.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/components': patch -'@object-ui/plugin-detail': patch ---- - -fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) - -Clause-②: no -Metadata-protocol: none - -The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. diff --git a/.changeset/29959-flow-condition-type-mismatch-every-site.md b/.changeset/29959-flow-condition-type-mismatch-every-site.md deleted file mode 100644 index a228bec2162..00000000000 --- a/.changeset/29959-flow-condition-type-mismatch-every-site.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/formula': minor -'@objectstack/lint': minor -'@objectstack/mcp': minor -'@objectstack/spec': minor ---- - -fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) - -Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). -Metadata-protocol: narrowed -Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. - - - -**BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. - -- **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. -- **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. -- **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. -- **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. -- **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. -- **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. -- **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). - -**FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). diff --git a/.changeset/29961-stored-expression-backfill.md b/.changeset/29961-stored-expression-backfill.md deleted file mode 100644 index 259f34c5fe0..00000000000 --- a/.changeset/29961-stored-expression-backfill.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/cli': patch ---- - -fix(objectql,cli): `os migrate stored-expressions` fills a stored computed field on rows stored before it was declared, and the boot names the command when a sample of rows needs it - -Clause-②: yes (widening) -Metadata-protocol: none - -A stored computed field (a non-`formula` field declaring `expression`) is computed by the engine on every insert and update and on nothing else, so declaring one on an object that already holds rows left the new column NULL on every existing row until each was next written; a filter, sort, roll-up or analytics anchor over it read NULL where the expression gives a value, with no error. A fresh database was right and an upgraded one was not. - -- **`@objectstack/cli`: `os migrate stored-expressions [--object ]… [--field ] [--changed] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`.** A dry run (the default) reports, per field, the rows holding a NULL the expression gives a value; `--apply` writes them. Each value is the write's own computation over the stored row (option labels, the one reference hop, the `can()` map), and the apply writes through `engine.update` with `updated_at` kept, so hooks, roll-ups over the column and audit capture see an ordinary write. A NULL the expression itself answers (an empty reference, a null input) is left alone. A value that differs from the expression — the expression changed after the row was written — is counted on every run and rewritten only with `--changed`; an expression reading the clock always differs there. A row on which an expression does not evaluate while it holds a value is listed and not written, because the write would store NULL over it. The command exits 1 while a row is skipped or could not be written, or when `--max-records` cut the walk short. Idempotent: a re-run finds nothing to fill. -- **Boot advisory.** At `kernel:bootstrapped` the engine plugin reads the first 100 rows of each object declaring a stored computed field (nothing for an object declaring none) and, when that sample holds such a NULL, warns once naming each `object.field` and `os migrate stored-expressions`. It writes nothing and never fails a boot. -- **`@objectstack/objectql`:** `backfillStoredExpressions`, `formatStoredExpressionBackfillReport`, `storedExpressionBackfillComplete`, `announceStoredExpressionHoles`, `sampleStoredExpressionHoles`, and `ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)` — a stored computed field's values over rows already stored, computed as a write computes them. diff --git a/.changeset/29969-platform-push-eval.md b/.changeset/29969-platform-push-eval.md deleted file mode 100644 index 39183279c9f..00000000000 --- a/.changeset/29969-platform-push-eval.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -test(skills): the platform skill's evals ask for push notifications on an `os serve` deployment - -Clause-②: no -Metadata-protocol: none - -`skills/objectstack-platform/evals/config-plugins-ops.json` gains case 12: the prompt asks to turn on push notifications for an `os serve` deployment, and the expected answer declares `requires: ['push']` with a top-level `push:` key on the stack definition and writes no secret inline (the APNs signing key and the Firebase service-account JSON are named by reference). The assertions stay with what `objectstack-platform/SKILL.md` teaches today, the `push` token and the top-level key; the push block's transport and vendor shape are not asserted until the skill teaches them. diff --git a/.changeset/29971-sign-in-mobile-handoff.md b/.changeset/29971-sign-in-mobile-handoff.md deleted file mode 100644 index c4678af1a07..00000000000 --- a/.changeset/29971-sign-in-mobile-handoff.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/console': patch ---- - -fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page - -Clause-②: no -Metadata-protocol: none - -The mobile app opens the Console's sign-in page in the system browser with `?redirect=` naming the hand-off route (`/api/v1/auth/one-time-token/handoff?...`), so that a user whose identity provider refuses embedded WebViews can still sign in. After sign-in the page sent every safe redirect through the Console mount, so the browser landed on `/_console/apps/.../api/v1/auth/one-time-token/handoff?...` and the page said it does not exist. Now: - -- A `redirect` whose path is exactly the hand-off route under the auth base the Console talks to is navigated to unprefixed, by script from the sign-in page. Every other redirect, `/api/v1/data/x` included, stays a Console route as before; a lookalike path, another base path or an absolute URL is never followed out of the mount. -- The hand-off comes before the organization steps: a user with several organizations is no longer sent to the picker first, and one with no active organization does not wait for it to be chosen. -- A session created more than five minutes ago, which the hand-off refuses, keeps the sign-in form on screen instead of forwarding to a JSON refusal. Signing in on that form makes a new session and forwards. -- The sign-up page does the same after a sign-up. diff --git a/.changeset/29972-serve-trusted-app-schemes-env.md b/.changeset/29972-serve-trusted-app-schemes-env.md deleted file mode 100644 index 3b6d9b63973..00000000000 --- a/.changeset/29972-serve-trusted-app-schemes-env.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os serve` reads `OS_TRUSTED_APP_SCHEMES` into AuthPlugin's `trustedAppSchemes`, so a deployment can register its mobile app for the sign-in hand-off - -Clause-②: no -Metadata-protocol: none - -- **Before.** `serve` constructed `AuthPlugin` with no `trustedAppSchemes`, and no setting a deployment could make reached the key. The `one-time-token` plugin never mounted and `GET /api/v1/auth/one-time-token/handoff` answered `400 … is not a registered app scheme` for every app, whatever the operator set. -- **Now.** `OS_TRUSTED_APP_SCHEMES` is read the way `OS_TRUSTED_ORIGINS` is: comma-separated, each entry trimmed, empty entries dropped, repeats folded. The list is passed as `trustedAppSchemes`. `os dev` and `os start` run `serve` as a child with the inherited environment, so they read it through the same line. -- **Unset stays unset.** No default scheme is registered: unset, empty or only separators, the plugin stays unmounted and the hand-off refuses every target, as before. -- **A bad entry still fails the boot.** Entries are not validated, lower-cased or dropped by the reader; plugin-auth's `assertTrustedAppSchemes` refuses a bad one at construction, naming it. -- Environment variable only: no stack key is added. The hosted runtime's own `AuthPlugin` is not changed. Listed in `content/docs/deployment/environment-variables.mdx` beside `OS_TRUSTED_ORIGINS`. diff --git a/.changeset/29978-import-row-hook-aborted.md b/.changeset/29978-import-row-hook-aborted.md deleted file mode 100644 index 18f1bfa988b..00000000000 --- a/.changeset/29978-import-row-hook-aborted.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/core': patch ---- - -fix(core): a hook that aborts with free text answers `HOOK_ABORTED` on the import row too, not `IMPORT_ROW_FAILED` - -Clause-②: no -Metadata-protocol: none - -- **Before.** A sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany`, `/api/v1/batch` and every `POST /api/v1/data/:object/batch` row with `HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/import` and the async import job answered the same abort as a row `{ ok: false, code: 'IMPORT_ROW_FAILED', error: }`, so an import client branching on the row's `code` could not tell a business rule's refusal from any other failed row. -- **Now.** The import row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read), judged by `resolveThrownHttpError`, the resolver that declares that code. The value moves, from `IMPORT_ROW_FAILED` to `HOOK_ABORTED`, on every row the core import runner builds from a write failure: the synchronous import, the async import job's stored results, the create and update halves of an upsert, and every write path (single-row, batched, partial-success). -- **Unchanged.** A code the hook declared still wins, registered or not. The row's `error` is still the hook's sentence. A body that crashed, a refusal that declared a 5xx status, a driver fault and every other row failure keep the code they had. The row's `code` is an open string in `ImportRowResultSchema`, so no schema moves, and no error code is added: `HOOK_ABORTED` is the one the ledger already registers. diff --git a/.changeset/29983-api-trigger-signature.md b/.changeset/29983-api-trigger-signature.md deleted file mode 100644 index 9c419cdf88c..00000000000 --- a/.changeset/29983-api-trigger-signature.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): an `api` flow's start node declares a third-party sender's signature scheme (`config.signature`), keyed by `config.secretRef` (#29983) - -Clause-②: yes (widening) -Metadata-protocol: added - - - -An inbound `api` hook admitted a post only by ObjectStack's own `x-objectstack-signature`, so a vendor that signs its callbacks its own way (DocuSign Connect, a Stripe-style `t=…,v1=…` header, a GitHub-style `sha256=` header) could not be admitted at all. The start node's `config` now carries `signature`, which declares how such a sender signs: - -- `signs: 'body'`: the raw body alone, the whole header value (after an optional `prefix`) is the digest. DocuSign Connect: `{ signs: 'body', header: 'X-DocuSign-Signature-1', algorithm: 'hmac-sha256', encoding: 'base64' }`. -- `signs: 'timestamp.body'`: `T.RAW_BODY`, the header being `NAME=value` elements whose `timestampElement` carries `T` and whose `digestElement` carries the digest. Stripe-style: `{ signs: 'timestamp.body', header: 'Stripe-Signature', algorithm: 'hmac-sha256', encoding: 'hex', timestampElement: 't', digestElement: 'v1' }`. - -The key is the start node's `config.secretRef`, as for an ObjectStack-signed hook. Declared, the scheme REPLACES the `x-objectstack-signature` check for that hook (one verifier per hook, never optional). The flow parse refuses a `signature` with no `secretRef`, one beside a literal `secret`, an algorithm other than `hmac-sha256`, an encoding other than `hex` or `base64`, a `timestamp.body` scheme with no `timestampElement` (or no `digestElement`, or the same name for both), a body-only scheme that names a timestamp element, a `header` naming `x-objectstack-signature` or another reserved header (`authorization`, `proxy-authorization`, `cookie`, `host`, `content-length`, `transfer-encoding`, `x-idempotency-key`) in any case, and one on a flow that binds no `api` trigger. - -New exports from `@objectstack/spec/automation`: `ApiTriggerSignatureSchema`, `ApiTriggerSignature`, its two arms `ApiTriggerBodySignatureSchema` / `ApiTriggerBodySignature` and `ApiTriggerTimestampedSignatureSchema` / `ApiTriggerTimestampedSignature`, `API_TRIGGER_SIGNATURE_KEY`, `API_TRIGGER_SIGNATURE_RESERVED_HEADERS`, `apiTriggerVerifierOf` (the one reader of which verifier a hook has: `objectstack-signature`, `declared` or `unusable`), `ApiTriggerVerifier`, `apiTriggerSignatureRefusals` and `ApiTriggerSignatureRefusal`. The `ITriggerApiService.handleInboundHook` contract text states what the implementer owes: a constant-time compare of the decoded 32 bytes, the same 300-second window as `x-objectstack-signature` for a scheme that signs a timestamp, no replay window for a body-only scheme, a repeated header read as its one comma-joined value, and the one `401 INVALID_SIGNATURE` for every failure, an unusable declaration included. - -This release declares the key; the `api` trigger does not verify it yet. Until it does, a hook that declares `signature` is still verified by `x-objectstack-signature` alone, so every post its vendor sends is refused `401`, and none is admitted unverified. diff --git a/.changeset/29988-studio-interfaces-nav-rail-module.md b/.changeset/29988-studio-interfaces-nav-rail-module.md deleted file mode 100644 index 5100c5f70b4..00000000000 --- a/.changeset/29988-studio-interfaces-nav-rail-module.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — the Studio Interfaces pillar's navigation rail moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 3) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. The rail Studio's Interfaces pillar draws for an app's navigation (the nav tree and its groups, the rows a customization package contributes, the off-nav sections for pages, dashboards, reports and views, the two rail contexts, the kind names and kind icons) now lives in `views/studio-design/InterfacesNavRail.tsx`. The moved code is byte-identical apart from its imports and eleven `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. diff --git a/.changeset/29988-studio-nav-item-inspector-module.md b/.changeset/29988-studio-nav-item-inspector-module.md deleted file mode 100644 index a63998c5985..00000000000 --- a/.changeset/29988-studio-nav-item-inspector-module.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — Studio's nav item inspector moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 1) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. The menu-entry inspector Studio shows while you edit an app's navigation, with the nav target pickers and the select it draws with, now lives in `views/studio-design/StudioNavItemInspector.tsx`. The moved code is byte-identical apart from its imports and one `export`; `StudioDesignSurface.tsx` imports it from there and re-exports the inspector, so every import of it keeps working. The package's public exports are unchanged. diff --git a/.changeset/29988-studio-package-switcher-module.md b/.changeset/29988-studio-package-switcher-module.md deleted file mode 100644 index dc6ecad0d7f..00000000000 --- a/.changeset/29988-studio-package-switcher-module.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — Studio's top-bar package switcher moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 2) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. The package switcher in Studio's top bar (the package list, New package, the package detail sheet and the deleted-package eviction), with the package-list toast id and the Studio landing path it shares with the surface, now lives in `views/studio-design/PackageSwitcher.tsx`. The moved code is byte-identical apart from its imports and three `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. diff --git a/.changeset/29988-studio-pillar-helpers-modules.md b/.changeset/29988-studio-pillar-helpers-modules.md deleted file mode 100644 index 942613c0f3f..00000000000 --- a/.changeset/29988-studio-pillar-helpers-modules.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -internal: move-only — Studio's flow rail helpers, pillar kit and Data pillar helpers move out of `StudioDesignSurface.tsx` into three modules, taking it under 10,000 lines (#29988, slice 4) - -Clause-②: no -Metadata-protocol: none - -No behaviour change. Three groups of helpers now live in their own modules under `views/studio-design/`. `AutomationsFlowRail.tsx` holds the Automations rail's flow status dot, its glyph tables, the flow reads and the new-flow trigger helpers. `StudioPillarKit.tsx` holds what every pillar shares: the held-edit notice, the unsaved-work and taken-identifier checks, the "Show me" focus helpers, the draft-saved announcement and the read-only link. `DataPillarHelpers.tsx` holds the Data pillar's field-name, CEL-stamp and form-preview helpers. The moved code is byte-identical apart from its imports and `export`s. `StudioDesignSurface.tsx` imports it from there and re-exports the five names it exported before, so every import of them keeps working. The package's public exports are unchanged. diff --git a/.changeset/29990-i18n-related-list-title.md b/.changeset/29990-i18n-related-list-title.md deleted file mode 100644 index c1d665d2e48..00000000000 --- a/.changeset/29990-i18n-related-list-title.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the i18n skill lists a lookup or master-detail field's translatable `relatedListTitle` - -Clause-②: no -Metadata-protocol: none - -- **Before.** The `objectstack-i18n` object-level table listed `label`, `help`, `placeholder`, `options` and `inlineTitle` under `fields.{field_name}`, so an author had no word that the related-list title on a parent's record page is translatable. -- **Now.** The row adds `relatedListTitle`: the title of the related list a `lookup` or `master_detail` field draws on its parent's record page. The key sits on the child object's field, and an untranslated title keeps its authored text, never the child's label. -- The paragraph under *Diff & Coverage Schemas* saying `getCoverage()` / `suggestTranslations()` have no shipped implementation is cut; *Service Contract* in the same file says the same, and the section's opening sentence now names the two schemas as those methods' return shapes. No schema, key or behaviour changes. diff --git a/.changeset/29997-handoff-signin-entry.md b/.changeset/29997-handoff-signin-entry.md deleted file mode 100644 index 44f2a33ba85..00000000000 --- a/.changeset/29997-handoff-signin-entry.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/spec': patch -'@object-ui/console': patch ---- - -feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off - -Clause-②: no -Metadata-protocol: none - -`GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: - -- A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. -- Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. -- A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. -- A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. - -An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. diff --git a/.changeset/30000-packageless-overlay-move-heals-registry.md b/.changeset/30000-packageless-overlay-move-heals-registry.md deleted file mode 100644 index 3529fb99412..00000000000 --- a/.changeset/30000-packageless-overlay-move-heals-registry.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): `os migrate packageless-overlays --apply` heals the registry after it deletes the unbound row, as the delete door does - -Clause-②: no -Metadata-protocol: none - -- **Now.** After the ceremony saves a row into its base's `.custom` pair and reads it back, it deletes the unbound row through the protocol's one repository and then runs the delete door's registry heal for an active row. The unbound row's registration leaves with the row, and the pair's row is registered again, so the process serves what a restart serves. Before, the delete went through a second repository and no heal followed, so the unbound row stayed registered beside the base's entry until the next restart. The five reads of the item already served the pair's row, so no read answered differently. -- **Unchanged.** A draft row's removal heals nothing, as a draft discard does not. Outcomes, receipts and the rows written are the same. diff --git a/.changeset/30001-platform-locale-table.md b/.changeset/30001-platform-locale-table.md deleted file mode 100644 index d19331c2bcb..00000000000 --- a/.changeset/30001-platform-locale-table.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec/system): export the platform locale table `PLATFORM_LOCALES` and its read aliases - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`PLATFORM_LOCALES`** (from `@objectstack/spec/system`): the one list of languages the platform supports, as frozen rows `{ tag, label, rtl }` in menu order: `en`, `zh-Hans`, `zh-Hant`, `es`, `ar` (right to left), `fr`, `pt-BR`, `ru`, `de`, `ja`, `ko`, each labelled with its own native name. `PLATFORM_LOCALE_TAGS` is the tag list and `PlatformLocaleTag` the literal union of the tags. -- **`PLATFORM_LOCALE_READ_ALIASES`** (typed on its ten literal keys, `PlatformLocaleReadAlias`) and **`platformLocaleTagOf(code)`**: the codes stored values already carry, mapped to the table tag they mean (`zh`, `zh-CN`, `zh-SG` to `zh-Hans`; `zh-TW`, `zh-HK`, `zh-MO` to `zh-Hant`; `ja-JP` to `ja`; `es-ES` to `es`; `pt` to `pt-BR`; `en-US` to `en`). A table tag answers itself and the match ignores case (`zh-cn` reads as `zh-Hans`). Any other code answers `undefined`; no region fallback is guessed. -- The table is the target every surface is backfilled to; today the Console ships ten packs without `zh-Hant`, and platform metadata and the built-in emails ship four locales. Nothing reads the table yet and no schema refuses a tag that is not on it: every locale value that parsed before still parses. The protocol page `i18n-standard` gains the table, the read aliases and the rule (same-PR follow; a language is added or removed by changing the table and backfilling once). diff --git a/.changeset/30009-api-trigger-declared-signature.md b/.changeset/30009-api-trigger-declared-signature.md deleted file mode 100644 index 8caad3b0f1a..00000000000 --- a/.changeset/30009-api-trigger-declared-signature.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/trigger-api': patch -'@objectstack/lint': patch -'@objectstack/skills': patch ---- - -feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. -- **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. -- **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. diff --git a/.changeset/30011-environment-scoped-rest-providers.md b/.changeset/30011-environment-scoped-rest-providers.md deleted file mode 100644 index b65c9d89f2f..00000000000 --- a/.changeset/30011-environment-scoped-rest-providers.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the approvals, email, sharing, sharing-rule and analytics routes answer from the request's environment kernel when one REST plugin fronts many environments - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the five provider seams the plugin hands `RestServer` for `approvals`, `email`, `sharing`, `sharingRules` and `analytics` read the HOST kernel's service whatever environment they were asked for, and the unscoped (hostname-routed) mount asked them for no environment at all. The environment's own services were never reached: `GET /api/v1/approvals/requests` answered `{ data: [] }`, `GET /api/v1/approvals/requests/:id` answered 501, and the sharing, sharing-rule and email doors answered 501, on every hosted environment. The record page's approvals part answered no requests. -- **What changes.** Given an environment id and a registered `kernel-manager`, each of the five providers now reads its slot on that environment's kernel (`kernelManager.getOrCreate(id)`), the same acquisition the data routes make. The routes pass the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. There is no fallback to the host's occupant: an environment whose kernel carries no such service gets the door's own "not configured" answer. -- **Analytics.** `POST /analytics/dataset/query` already read the environment's service first, and now reads it alone: when the environment's kernel carries no analytics service, or cannot be acquired, the answer is the route's 501 rather than the host's service. -- **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads all five services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. -- **No migration.** No configuration, route, response shape or export changes. diff --git a/.changeset/30016-flow-date-arithmetic-run-fails.md b/.changeset/30016-flow-date-arithmetic-run-fails.md deleted file mode 100644 index b8c28da5150..00000000000 --- a/.changeset/30016-flow-date-arithmetic-run-fails.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/formula': patch -'@objectstack/lint': patch ---- - -fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`@objectstack/formula`: four codes join `ExpressionRefusalCode`**: `date-arithmetic-run-fails`, `date-arithmetic-run-may-fail`, `date-text-arithmetic-run-fails` and `date-text-arithmetic-run-may-fail`, with the same params as `date-arithmetic` / `date-text-arithmetic`. A flow condition (a `'flattened'` site) that does arithmetic on a date field used to be told the field "silently evaluates to null", but the engine's `evaluateCondition` throws on the fault on every flow, so the run fails at the condition. Where the caller declares the run proven to hold the field (`faultOutcome: 'run-fails'`, with `bareNamesUnproven` unset) the verdict is served under `-run-fails` and says the run fails; anywhere else in a flow it is served under `-run-may-fail` and says the run fails wherever the name holds the field's date when the condition runs (a flow started by an API call, an action or a parent flow binds what its entrance hands over, which can be a number). The grade is unchanged: an error at every site. A `record`-scoped site (a formula field, a validation rule, a display predicate) keeps `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a flow condition re-keys to the new ones. -- **`@objectstack/lint`**: the fault-outcome ledger's docblock names the date verdicts' flow codes; the routing is the existing `faultOutcome` / `bareNamesUnproven` hint, unchanged. diff --git a/.changeset/30018-page-header-placed-actions.md b/.changeset/30018-page-header-placed-actions.md deleted file mode 100644 index 2e8105d8465..00000000000 --- a/.changeset/30018-page-header-placed-actions.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/skills': minor ---- - -feat(spec): a `page:header` declares `placedActions: true`, the explicit opt-in to draw every action of the bound object placed at `record_header` / `record_more`, read live (#30018) - -Clause-②: yes (widening) -Metadata-protocol: added - -- **`placedActions`** on a `page:header` component's `properties` (`PageHeaderProps`, the `ComponentPropsMap['page:header']` row): `true` asks the header to draw every action of the page's object whose `locations` include `record_header` or `record_more`, read live from the object's metadata, in place of an authored `actions` id list. An action another package places on the object is then drawn without editing the page. Optional with no default: an unset key stays unset in the parsed output. -- **The `actions` list keeps its meaning.** An authored list is the whole set the header draws, and a header with neither `actions` nor `placedActions: true` draws none of the object's actions. `placedActions: false` beside a list is accepted and changes nothing. -- **Refused together.** `placedActions: true` beside an authored `actions` list, an empty one included, is refused at `properties.placedActions` by `os validate`, `os build`, `os lint` and the page save door (`component-props-invalid`, an error). The message names both keys and asks the author to keep one: `actions` for a curated list that is the whole set, or `placedActions: true` for every placed action. Nothing that parsed before is refused: until this release `placedActions` was an unknown key on the closed shape. -- **Declared ahead of its reader.** The console's header renderer does not read the key yet, so a header that sets it draws what the same header without it draws (none of the object's actions unless `actions` names them). The key's describe opens with `[PLANNED — not yet rendered]` until the renderer reads it. -- The published JSON Schema `ui/PageHeaderProps` still accepts the pair: the rule depends on the value `true`, which no projected refinement pattern states, so the parse refuses it and `dropped-refinements.baseline.json` records the site. -- `@objectstack/skills`: `objectstack-ui` `rules/pages.md` names the key in the component catalogue, marked not drawn yet, and drops a refused-shape clause from the print-page paragraph that the sentence before it already states. diff --git a/.changeset/30023-stored-sort-sortable-false-lint.md b/.changeset/30023-stored-sort-sortable-false-lint.md deleted file mode 100644 index 144db57cf45..00000000000 --- a/.changeset/30023-stored-sort-sortable-false-lint.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/lint': minor -'@objectstack/spec': minor ---- - -fix(lint)!: a stored sort naming a field that declares `sortable: false` is refused by `os validate` and at publish, judged by the same sortability projection the Data API list door refuses it by — on a list view and on every stored page sort (#30023) - -Clause-②: yes (narrowing) — `sort-field-unsortable` (error) now also fires on a sort naming a field whose author declared `sortable: false`, on the list-view rungs it already walked and on every stored page sort it did not walk before (a block's own `sort`, every element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort`, an app page's `record.sort`, where a `formula` sort is now refused too), so `os validate` / `os build` / `os lint` and the `page` and `view` publish doors refuse metadata they used to accept. The widening half: `@objectstack/lint` exports `indexObjectSortability` and the `SortabilityFields` type, and `checkSortDeclaration` takes an optional trailing `sortability` index. -Metadata-protocol: narrowed -Skills-docs: none — `skills/**` names no `sortable` field key and no page or view `sort` rule; `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field". The prescription ships in the finding's fix line, `os explain sort-field-unsortable` and the upgrade-guide entry. - - - -**BREAKING** accept-set narrowing at authoring (`@objectstack/lint`): a stored sort the Data API list door refuses is refused before it is stored. - -- **Before.** A list view whose `sort` named a field marked `sortable: false` passed `os validate` and the `PUT /api/v1/meta/view` door, then answered `400 INVALID_SORT` on its first fetch and on every fetch after it. No page sort was judged for sortability at all: a `record:related_list`, `record:line_items`, `list-view` or `object-*` block sorting by such a field (or by a `formula` field), an element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort` and an app page's `record.sort` all saved green and failed at read. -- **Now.** `validateSortableFields` judges each of those positions by `resolveObjectSortability`, the projection the list door refuses by, and reports `sort-field-unsortable` at `error` with the door's own clause ("a field on object X that declares `sortable: false`") at the slot's path. The reference-integrity suite now runs on a `page` write for this one member, so the Studio, REST `/meta` and MCP page doors give the same refusal (422). Only the literal `false` counts: a field with no `sortable` key, or `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its virtual-field verdict. Existence on page sorts stays `page-field-unknown`. -- **Not changed.** A stored view or page already carrying such a sort keeps loading and serving; only its next save is judged, and its first fetch still answers 400 as it did since the list door started refusing it. - -**The remedy.** Sort by another field, or remove `sortable: false` from the field to make it sortable. - -**Supersedes, in the same release.** The `@objectstack/spec` / `@objectstack/metadata-protocol` / `@objectstack/mcp` note for #29896 says "Lint does not yet judge these at authoring." From this release it does: `os validate` and the publish doors refuse every stored sort that note lists, and the stored page sorts it did not list. diff --git a/.changeset/30024-list-refused-sort-not-sent.md b/.changeset/30024-list-refused-sort-not-sent.md deleted file mode 100644 index 2e7ce2b305e..00000000000 --- a/.changeset/30024-list-refused-sort-not-sent.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/plugin-grid': patch ---- - -fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag. - -- **`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before. -- **`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before. diff --git a/.changeset/30025-flow-bare-name-screen-open-binder.md b/.changeset/30025-flow-bare-name-screen-open-binder.md deleted file mode 100644 index 8cff0cf1bbf..00000000000 --- a/.changeset/30025-flow-bare-name-screen-open-binder.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/formula': patch -'@objectstack/lint': patch ---- - -fix(lint): a flow condition's bare-name type verdict no longer refuses a name a `screen` field holds, and stands down to the warning after a node that can bind any name (#30025) - -Clause-②: yes (widening) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a `type-mismatch-run-fails` on a BARE condition name that a `screen` node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with no input contract), which is now the `type-mismatch-run-may-fail` warning. A `record.` / `previous.` read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional `bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`, and `collectScreenFieldNames` in `@objectstack/lint`'s flow-variable-scope module. -Metadata-protocol: none -Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the screen field and the open binder); `skills/**` and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves. - -- **Before.** A record-triggered flow whose `screen` node collects `title` (a number) and whose edge after it reads `title > 5`, with `title` a text field on the trigger object, was refused `type-mismatch-run-fails`: the bare name was typed as the field. The run binds the collected number there (a resume lands a screen's values under their plain names), so the condition runs. The same refusal hit a bare name after a `script`, `connector_action`, `wait`, `subflow` or `map` node, or a screen with no input contract, any of which can bind the name to a value nothing can see at build. -- **Now.** A screen's declared field names join the names a bare read holds instead of the field, so they are not typed as it (the null-guard verdict leaves them out too). In a flow with a node that can bind any name, a bare-name mismatch is the `type-mismatch-run-may-fail` warning, as the null-guard verdict already stands its bare names down there; `record.` is still refused. The stand-down reads the flow's nodes only: an entrance (and the publish door's per-write snapshot, which sees none) changes which record both the bare name and `record.` read, not whether a node rebinds the bare name, so a bare-name refusal on a record-triggered flow with no such node is unchanged at every door. diff --git a/.changeset/30031-page-builder-header-breadcrumb.md b/.changeset/30031-page-builder-header-breadcrumb.md deleted file mode 100644 index c8a9c66bcfd..00000000000 --- a/.changeset/30031-page-builder-header-breadcrumb.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/skills': patch ---- - -docs(skills): the `page:header` title row drops the retired `breadcrumb` and names the live `placedActions: true` - -Clause-②: no -Metadata-protocol: none - -- `skills/objectui/guides/page-builder.md`: the `page:header` sentence named `breadcrumb` among the title row's keys. `PageHeaderProps.breadcrumb` is a `retiredKey` tombstone since @objectstack/spec 17 (ADR-0087 D2), and the props lint refuses an authored one. The row now lists `title`, `subtitle`, `actions` or `placedActions: true`; the last two are refused together. diff --git a/.changeset/30033-related-table-name-column.md b/.changeset/30033-related-table-name-column.md deleted file mode 100644 index 864bb3ec63f..00000000000 --- a/.changeset/30033-related-table-name-column.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -fix(plugin-detail): the related table leads with the record's name when the relation declares no columns - -Clause-②: no -Metadata-protocol: none - -A related list with no columns declared took its columns from the child object's `highlightFields`, which by design never list the record's title, so the related tab on a record page showed amounts and dates with no deal or contact name, and the only link in a row was its first value. The record's name (`nameField`, else the field the platform derives, ADR-0079) now leads that set, listed once even when a highlight names it, and the grid links that first column to the record. The field walk used when the child declares no `highlightFields` leads with the declared name the same way. The enlarged tab and the table in the record body read the same set. Columns an author declares on the relation are drawn exactly as declared. A `titleFormat`-only object leads with the field the platform derives, not the formatted title, which has no column behind it. diff --git a/.changeset/30034-personal-view-url-default-state.md b/.changeset/30034-personal-view-url-default-state.md deleted file mode 100644 index ea93d4814ac..00000000000 --- a/.changeset/30034-personal-view-url-default-state.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): a personal view opened by its address no longer carries the default view's filter and sort in the address bar - -Clause-②: no -Metadata-protocol: none - -- While the list waits for the reader's own views to resolve a requested or pinned view it does not hold yet, the list's URL state is neither seeded nor written. The default view the page falls back to meanwhile is not the view the address names, so its stored filter, sort and search no longer reach the address bar. Before, when the list preferences landed first, that default seed was written to the URL, and a personal views read landing before the write committed left it there: a reload or a shared link then opened the personal view with conditions nobody chose for it and marked it personalized. -- When the view resolves, the seed is taken for it alone: from its own stored state, or from the `uf_*` params a link carried, as before. diff --git a/.changeset/30035-record-feed-reread-after-save.md b/.changeset/30035-record-feed-reread-after-save.md deleted file mode 100644 index 1ce6367a31d..00000000000 --- a/.changeset/30035-record-feed-reread-after-save.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): a record's 动态 stream is read again after a save, so its count and rows hold the change without a reload - -Clause-②: no -Metadata-protocol: none - -- **The record feed follows the record's invalidation.** A save through the edit dialog (or an action, an undo, a ⟳) put the record on the data-invalidation bus, and the page read the record again, but the 动态 feed kept the rows and the count the page payload answered when it opened. The feed now reads again on the same change, on the record page and in the drawer. -- **The re-read asks what the open asked.** Once the record changed after the open, the feed asks the record page route for the streams the open asked, and only those (the discussion, the activity rows, the stream's count), so the new activity row and the server's count land in place. The activity row a save writes exists before the save answers, so one ask after the save sees it; nothing polls. -- **The field changes follow the same change**, so on an object with `enable.trackHistory` the stream's field-change entries are read again too. diff --git a/.changeset/30036-activity-feed-default-view.md b/.changeset/30036-activity-feed-default-view.md deleted file mode 100644 index 414811137e6..00000000000 --- a/.changeset/30036-activity-feed-default-view.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -fix(plugin-audit): the activity list opens newest first, without the platform's own rows, under a plain name and description - -Clause-②: no -Metadata-protocol: none - -- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and every identity object `@objectstack/platform-objects/identity` declares (users, members, organizations, sessions, keys, SCIM): every sign-in stamps the last-login columns on `sys_user` under the signing-in person's own name, the signing key in `sys_jwks` is minted under the first sign-in's session, and adding a person writes `sys_user` and `sys_member` rows that person can read. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. -- **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. diff --git a/.changeset/30038-trigger-object-enter.md b/.changeset/30038-trigger-object-enter.md deleted file mode 100644 index d6dff3426ab..00000000000 --- a/.changeset/30038-trigger-object-enter.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): New automation no longer saves a typed prefix as the trigger object (#30038) - -Clause-②: no -Metadata-protocol: none - -In Studio's New automation dialog, typing part of an object name and pressing Enter committed the typed text itself, so the flow was created bound to an object that does not exist. The trigger-object picker there now takes objects only: Enter or leaving the field commits the object the text names exactly, else the only match left in the list, else no object, and the picker says no object is chosen. A record preset then keeps the dialog open until an object is picked. - -Every other object picker keeps its free text, because a flow reference may hold an expression. diff --git a/.changeset/30039-studio-zh-terms.md b/.changeset/30039-studio-zh-terms.md deleted file mode 100644 index aef9ae925e5..00000000000 --- a/.changeset/30039-studio-zh-terms.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): Studio on a zh-CN console reads in Chinese in the view, page and report panels, the publish sheet, the flow Problems tab and the save-notice / notify nodes, and calls a dashboard 仪表盘 everywhere - -Clause-②: no -Metadata-protocol: none - -The view, page and report property panels render the spec's authoring forms from the console's own copy of the spec, and only the dashboard form had a zh-CN overlay, so section headings such as Basics, Columns & filters and Navigation & sharing, their descriptions and schema-titled rows such as Searchable Fields read in English. The overlay now carries every `METADATA_FORM_REGISTRY` type, worded as the platform's own zh-CN metadata-forms bundle words them: view, page and report in full, the other types by field label, which is what the Pending changes sheet names a changed property by (a flow's `~ Status` now reads `~ 状态`). The sheet's summary line counts datasets as 数据集 instead of the raw type name. - -A flow whose trigger is not armed on this deployment showed the engine's developer sentence under 未在此运行, above 没有问题 —— 该流程结构有效. The note now says, from the engine's `unboundCause` code, what this deployment does not do and who can change it, keeps the engine's sentence as the hover detail, and the line below it says the structure checks found nothing to fix instead of "no problems". A cause the console has no row for still shows the engine's sentence. - -The palette names the save-notice node 保存提示, and the notify node's Template and Template data fields and the save-notice Message field read in Chinese. English consoles read the same English as before. zh-CN Console and Studio text uses 仪表盘 for dashboard throughout. diff --git a/.changeset/30039-view-panel-zh-filter-prompt.md b/.changeset/30039-view-panel-zh-filter-prompt.md deleted file mode 100644 index 72f451078a3..00000000000 --- a/.changeset/30039-view-panel-zh-filter-prompt.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): on a zh-CN console the view properties panel's empty filter button reads 「+ 添加筛选…」 and the description row's hint reads in Chinese - -Clause-②: no -Metadata-protocol: none - -- The `filter-builder` widget's empty trigger was an English literal (`+ Add filter…`); it now reads the Studio string the dataset scope filter already uses, so it follows the designer locale. -- The zh-CN view form overlay gains the `description` row's help text, which showed the schema's English description. diff --git a/.changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md b/.changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md deleted file mode 100644 index 53a348939ad..00000000000 --- a/.changeset/30042-boot-picks-the-key-file-that-opens-its-secrets.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-settings': patch ---- - -fix(cli): when two data-key files hold different keys, `os start` and `os dev` read with the one that opens the project's stored secrets, or a production boot refuses - -Clause-②: yes (narrowing) — a production `os start` / `os serve` boot whose two candidate key files (the shared `$HOME/.objectstack/dev-crypto-key` or `$OS_HOME`'s, and the project's `.objectstack/dev-crypto-key`) both fail to open the database's newest stored secret now refuses to start, naming both files; it used to start and serve every stored secret as unset. The widening half: `@objectstack/service-settings` gains new exports (`chooseKeyFileByTrialOpen`, `settleKeyFileChannel`, `storedSecretReaderOverEngine`, `deferredLocalCryptoProvider`, `DataKeyOpensNothingError`, `KEY_FILE_CANDIDATE_CHANNEL_ENV` and their types). -Metadata-protocol: none - - - -**BREAKING** boot narrowing (`@objectstack/cli`, `@objectstack/service-settings`): a production boot whose candidate data keys open none of the stored secrets refuses to start. - -- **`@objectstack/cli`.** When the shared key file and the project's own key file (the one an earlier `os start` minted) hold different keys, the CLI no longer picks one. It hands the `serve` child both, and the child decides once the database is open: it opens the newest stored `sys_secret` value under each key and reads with the key that opens it, and the boot says which file it took. A project that only ever ran `os start`, on a machine whose home holds a key from `os dev` in another project, reads its secrets back again. With no stored secret yet, the shared file is used, as before. When neither key opens the stored secrets, a production boot (`os start`, `os serve`) refuses to start and names both files; a development boot (`os dev`) starts and prints a boot error naming both files. The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) now read the same file by the same rules: the project's own file when it alone holds a key, and the key that opens a stored secret when both do. -- **`@objectstack/service-settings`.** The settings service makes that choice in its `start()`, before any reader decrypts, when the CLI names a second candidate on the internal `OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE` channel. A leading `~` in `OS_HOME` (or `OBJECTSTACK_HOME`) is now the home directory when the provider finds its key file, as `os start` already read it; it used to name a directory called `~` under the working directory. - -**The remedy.** If a production boot refuses with "neither data key opens this database's stored secrets", find the key those secrets were sealed under (a backup of either file, or the `OS_SECRET_KEY` the deployment once ran with), keep a copy of both files, and put that key at the shared file the message names, or set `OS_SECRET_KEY` to it. - -Supersedes, in the same release: the note for #29934 says that when the shared file and the project's file hold different keys, the shared one is used and a warning names both paths. After this change the boot uses whichever of the two opens the stored secrets, and refuses in production when neither does. diff --git a/.changeset/30043-dock-sends-selected-app-and-page.md b/.changeset/30043-dock-sends-selected-app-and-page.md deleted file mode 100644 index 7beb4969257..00000000000 --- a/.changeset/30043-dock-sends-selected-app-and-page.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the console AI dock tells the agent which app the user has selected and which page or record is in view, instead of the constant `activeApp: 'AI'` and no page at all (#30043) - -Clause-②: no -Metadata-protocol: none - -Every chat request the console sends to `POST /api/v1/ai/agents/:agent/chat` used to carry `context.activeApp: 'AI'`, whatever app the user was in, and the floating dock sent no `context.surface`. So the assistant asked "which app?" and "which record?" while the screen showed both. - -Now: - -- `context.activeApp` is the package of the app the user has selected: the console app the dock sits in (whether the dock runs `ask` or `build`), the app picked for editing on the full-page `/ai/build?package=…`, the package the Studio copilot designs. With no app selected the key is left out; nothing sends a placeholder. -- The dock, on the desktop rail and the phone sheet alike, sends `context.surface` in cloud#1610's existing shape for the page in view: an object list (`artifact: { type: 'object', name }`, with `selection: { kind: 'view', id }` on a saved list view), a record page (the object plus `selection: { kind: 'record', id }`), a dashboard, report or page (`artifact: { type, name }`, `mode: 'design'` in the dashboard designer). A page that shows no one artifact (the app landing, search, a component page) sends no `surface`. -- Both are read on every send, so navigating with the dock open moves the next turn's context. - -The dock's "discussing" chip now names the object by its label on object pages, as the Studio copilot's chip already did for its artifacts. diff --git a/.changeset/30046-studio-view-dropdown.md b/.changeset/30046-studio-view-dropdown.md deleted file mode 100644 index 27b2ce4db28..00000000000 --- a/.changeset/30046-studio-view-dropdown.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -feat(app-shell): Studio Interfaces picks an object's list view from the header title, object / view ▾, the way the running list does, and the open view stays in the address - -Clause-②: no -Metadata-protocol: none - -- **The view switcher is in the header's title.** An `object` entry's header reads object / view ▾. The switcher is the running list's own dropdown (`ObjectViewTitleSwitcher`): the object's list views by label, a view only drafted in this package included, a search box, and New view, which opens the Create View dialog. Studio does not offer the reader's own features there: no ☆, no 📌, no recent views, no Save as my view. It shows when the object has one list view too, so New view is always one click away. A read-only package switches views and offers no New view. -- **The Properties panel no longer holds a view picker, Add view or "Back to …".** It keeps the open view's settings, and its title is the view's name as the switcher gives it (the label the object's served `listViews` carry). A view with no label is named "Default view" or by its key; the header names the object before it. -- **The open view is in the address.** Switching writes `?view=.` beside `?surface=` and `?nav=`; the entry's own view writes none. A reload or a shared link opens that view again, a view only drafted in this package included. A link naming a view of another object is not taken. A pending edit is still sent before a switch, and an edit that cannot be sent still asks first. -- **`ObjectViewTitleSwitcher`: the pin is optional.** `onPinnedViewChange`, `pinnedViewId` and `recentViewIds` may be left out; without `onPinnedViewChange` no row draws 📌 and no Pinned section is listed. The running list passes all three, so it is unchanged. -- **`@object-ui/i18n` (studio namespace):** adds `engine.studio.inspector.listView.defaultView`; removes `defaultTitle`, `namedTitle`, `addView`, `backToEntry`, `switchView` and `switchViewDraft`, which only the removed panel controls read. diff --git a/.changeset/30050-console-locale-match.md b/.changeset/30050-console-locale-match.md deleted file mode 100644 index 2da0a51dc21..00000000000 --- a/.changeset/30050-console-locale-match.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -fix(app-shell): the console picks a signed-in user's UI language in the server's `resolveLocale` order - -Clause-②: no -Metadata-protocol: none - -- **Before.** `pickRenderableLocale` matched `sys_user.locale` against the offered locales exactly, then by base language only. A row stored mis-cased (`zh-cn`, written before the server door folded case and served as stored), or a tag the deployment offers only as a variant (`zh` or `zh-Hans-CN` against `zh-CN`), matched nothing, so the console rendered English. -- **Now.** It uses the order the server's i18n routes resolve with: exact, then case-insensitive, then base language, then the first offered variant of that base language. `zh-cn`, `zh` and `zh-Hans-CN` render `zh-CN`; `en-GB` renders `en`. A tag no offered locale shares a language with still matches nothing, and the device language keeps deciding, as before. -- No schema, key or server behaviour changes. diff --git a/.changeset/30052-facade-scoped-classes.md b/.changeset/30052-facade-scoped-classes.md deleted file mode 100644 index c1bdf362a0d..00000000000 --- a/.changeset/30052-facade-scoped-classes.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -refactor(objectql): `ObjectRepository` and `ScopedContext` in `engine.ts` are facades over their own facet modules, moved by the `engine.ts` codemod's second pass; no behaviour or published declaration changes - -Clause-②: no -Metadata-protocol: none - -- 20 of the 24 methods of the two classes keep their signatures and public docblocks on the class and forward, in one call, to an unedited copy of their body in `engine-object-repository.ts` or `engine-scoped-context.ts`. Each instance gets one host, built on first use (`engine-scoped-hosts.ts`), so a spy or a subclass override on an instance is still the method a body reaches. -- Four `ScopedContext` methods keep their bodies on the class, each named in the facade gate's registry with the card that will move it: `transaction` and the symbol-keyed `SEED_USER_GRANTS` method hand the instance itself on; `derive` and the symbol-keyed `APP_AUTHORED_HOOK_API` method set a private flag on another instance. -- Nothing to do on upgrade: the exported names, the published `.d.ts` surface and what every method does are the same. diff --git a/.changeset/30057-studio-form-designer-form-view.md b/.changeset/30057-studio-form-designer-form-view.md deleted file mode 100644 index c88b70fb742..00000000000 --- a/.changeset/30057-studio-form-designer-form-view.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): the Studio form designer lays out the form the app opens, so an object with a default form view no longer edits a layout users never see - -Clause-②: no -Metadata-protocol: none - -- **Which layout.** Studio → Data → an object → Form asks the app's own resolver which form view New and Edit open (`selectRecordFormView`, over the shell's merged objects). With none, the designer edits the object's `fieldGroups`, as before. With one (an `isDefault` form view, or an `OBJECT.create` view for New), the designer shows that view's sections and every layout edit (moving fields, adding, renaming, moving and deleting sections) is saved to the view's draft, which the package publish publishes. A chip in the toolbar names the layout being edited ("Form view: OBJECT.form" or "Field groups"). -- **New and Edit.** Where New and Edit open different form views, a New / Edit switch in the toolbar picks which one the designer and the Preview show. A pending edit of the shown view is sent before the other one loads. -- **Fields not on the form.** On a form view, a field the view lists in no section is not on the form; the designer keeps such fields in a "Not on this form" bucket, and dragging one into a section adds it. A field entry's own keys (`required`, `readonly`, …), a section's other keys, and entries the canvas does not show are written back as they were. A section that references a field group (`group`) keeps the reference until its members are changed. -- **Read-only base.** On a read-only base the view is customized, not edited: the edit is saved into the base's paired customization package as a declared override of the base's view (ADR-0146 D2), the same door list views use. -- **Preview.** The Form tab's Preview now lays the form out by the same form view (its sections, and the tabbed layout), as the dialog it previews does; before, it drew the object's field groups whatever form view the app opened. -- **`@object-ui/i18n`.** English and Chinese strings for the chip, the New / Edit switch and the "Not on this form" bucket. diff --git a/.changeset/30059-studio-customize-app-navigation.md b/.changeset/30059-studio-customize-app-navigation.md deleted file mode 100644 index 5ba52a7b157..00000000000 --- a/.changeset/30059-studio-customize-app-navigation.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -feat(app-shell): Studio customizes an installed app's navigation into the base's customization package (#30059) - -Clause-②: no -Metadata-protocol: none - -On a read-only (installed or code-shipped) package that has a paired customization package (`.custom`), the Interfaces rail now edits the app's navigation. Before, the rail's Edit button showed only on a writable package and its "+" offered only new pages, dashboards and reports, so an object made in the pair could be reached only by a typed URL. - -- Edit and the "+" open the same navigation editor a writable package uses (add, rename, reorder, group and remove entries, including the ones the base shipped). Nothing is written until an edit is saved. The first saved edit copies the whole app into the pair, declaring `overrides: { package: , basedOn: }`, the same route a view's Customize takes. Later edits update that copy. The base package is never written. -- The entry pickers list the pair's objects, pages, dashboards, reports and views after the base's, so a new object in the pair can be put on the menu. An entry bound to an object only the pair holds carries `requiresObject: `, so the runtime hides it if the object is later removed instead of linking to a missing object. -- The navigation change is a draft in the pair's pending changes and publishes in the same batch (one commit) as the object it opens. -- The rail marks a customized navigation with the same compact "customized" mark its rows carry, "only customizations" counts it and keeps the navigation listed, and the mark's popover offers "Restore package original", which deletes the pair's draft and copy of the app after a confirmation. - -A customized app is a whole copy: a navigation entry the vendor adds in a later version does not appear until the customization is restored and redone. A writable package's navigation editing is unchanged. No spec, server or metadata-type change. diff --git a/.changeset/30062-declared-override-mapping-removal-remedies.md b/.changeset/30062-declared-override-mapping-removal-remedies.md deleted file mode 100644 index 0c13d5c7c9f..00000000000 --- a/.changeset/30062-declared-override-mapping-removal-remedies.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@object-ui/app-shell': minor ---- - -fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062) - -Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer. -Metadata-protocol: semantic - -- **A mapping override in `.custom` is served.** A mapping item that declares `overrides: { package: '', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping. -- **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=`) is unchanged: the pair's row stays and its override stays served. -- **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged. -- **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag. - -The label precedence of a translated override (card item 3) is not in this change. diff --git a/.changeset/30063-studio-customize-page.md b/.changeset/30063-studio-customize-page.md deleted file mode 100644 index a924ca6e519..00000000000 --- a/.changeset/30063-studio-customize-page.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/app-shell': patch ---- - -feat(app-shell): Studio customizes an installed package's page into the base's customization package (#30063) - -Clause-②: no -Metadata-protocol: none - -On a read-only (installed or code-shipped) package, a page opened in the Interfaces pillar now offers Customize, the same button a dashboard, report or view shows. Before, Studio showed the page as read-only with no way to change it, although the server already serves a page override from the paired customization package (`.custom`). - -- Customize unlocks the page designer. Nothing is written until an edit is saved. The first saved edit copies the whole page into the pair, declaring `overrides: { package: , basedOn: }`. Later edits update that copy. The base package is never written. -- The edit is a draft in the pair's pending changes; once published, the running app serves the customized page at its usual address. -- The page's header carries the override mark, whose popover offers "Restore package original": it deletes the pair's draft and copy of the page after a confirmation, and the shipped page reads back. -- The Interfaces rail marks a customized page, "only customizations" counts and lists it, and the All metadata directory's page tile counts it. - -A customized page is a whole copy: a change the vendor ships to that page in a later version does not appear until the customization is restored and redone. A writable package's page editing is unchanged. No spec, server or metadata-type change. diff --git a/.changeset/30064-studio-dataset-customize.md b/.changeset/30064-studio-dataset-customize.md deleted file mode 100644 index 0d1bc3b69bc..00000000000 --- a/.changeset/30064-studio-dataset-customize.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/data-objectstack': patch -'@object-ui/i18n': patch ---- - -feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset - -Clause-②: yes (widening) -Metadata-protocol: none - -- **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. -- **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". -- **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. -- **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. -- **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. diff --git a/.changeset/30069-related-export-permission.md b/.changeset/30069-related-export-permission.md deleted file mode 100644 index 6d3623de4ee..00000000000 --- a/.changeset/30069-related-export-permission.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/plugin-detail': patch -'@object-ui/plugin-list': patch -'@object-ui/plugin-grid': patch -'@object-ui/i18n': patch ---- - -fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. -- **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. -- **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. diff --git a/.changeset/30073-personal-view-conditions.md b/.changeset/30073-personal-view-conditions.md deleted file mode 100644 index fb180bb163e..00000000000 --- a/.changeset/30073-personal-view-conditions.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/plugin-list': patch ---- - -feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, and 保存 writes the panel back into that view - -Clause-②: no -Metadata-protocol: none - -- **Opening one of the reader's own views (My views) shows its conditions as ordinary rows in the Filter panel**, with the active count on the Filter control. They are the list's filter while they are there: not applied under the list a second time, and not a personalization. A stored filter the panel cannot hold (an any-of group beside other conditions), or one naming a field the object no longer declares or the reader cannot read, stays applied under the list, as for any other view. -- **Editing those rows marks the list 已个性化, and the marker offers 保存 to that view** (for a reader allowed to update their views). 保存 replaces the row's `config` through the data API with the body Save as my view builds, so the stored `ListView` shape is unchanged and no key is added; the reader's temporary list state for the view is then cleared and the list opens on the saved view. Edits not saved stay the reader's temporary list state and are never written to the view. A panel the fold cannot write losslessly keeps the refusal Save as my view gives, and nothing is written. -- **Save as my view now clears the view it came from**: that view's stored list state (sort, filters, column layout, density, hidden fields) and cached search term go, and the address being left loses its `uf_*` params, so the source no longer reads filtered or 已个性化 after the save. -- Both saves read the Filter panel as the list reports it, not the address's copy, so an emptied panel saves as no filter. -- `@object-ui/plugin-list`: `ListView`'s host prop `viewDefaults` accepts `filters`, the view's own conditions for a host that shows them as panel rows; the `filters` personalization part is then judged against them. diff --git a/.changeset/30074-crm-track-history.md b/.changeset/30074-crm-track-history.md deleted file mode 100644 index b9fd7e855b1..00000000000 --- a/.changeset/30074-crm-track-history.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -feat(record page): the timeline's Changes chip holds the audit's field changes only, and is offered only while the record shows its field history - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/plugin-detail`: what Changes holds.** The timeline stream's Changes chip, and the activity panel's `changes_only` filter, hold the field changes read from the audit log (each with its before and after values) and nothing else. The activity rows that share their look (created, updated, deleted, assigned, shared) are events, so they sit under Activity whether or not the record shows its field history; before, they sat under Changes and, on the panel, under `changes_only`. -- **`@object-ui/plugin-detail`: `RecordFieldHistoryContext`.** A host tells the record page's timeline stream whether it shows the record's field history; while it says `false`, the stream draws the All / Activity / Discussion chips and no Changes chip, and a stream left on Changes goes back to All. With no provider every chip draws, as before. `RecordActivityTimeline` takes the chips to offer as `streamFilters` (all four when absent). -- **The Console's record page** hands over its `enable.trackHistory` gate, so an object that does not opt in no longer offers a Changes chip on its timeline tab. -- **The CRM example** opens `enable.trackHistory` on `crm_lead`, `crm_account`, `crm_contact` and `crm_opportunity`, so their record pages show each field change with its before and after values. The flag stays opt-in for every other object. diff --git a/.changeset/30075-default-page-stage-advance.md b/.changeset/30075-default-page-stage-advance.md deleted file mode 100644 index 67c58daa312..00000000000 --- a/.changeset/30075-default-page-stage-advance.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@object-ui/plugin-detail': patch ---- - -feat(plugin-detail): the default record page lets an editor move a record to another stage by pressing it on the stage path - -Clause-②: no -Metadata-protocol: none - -**Visible behaviour change on a released page.** The record page the console synthesizes for an object with a stage field (no authored page) used to draw its stage path as a read-only progress rail, so moving a deal to its next stage took 编辑 → change the stage → 更新. That page's `record:path` now carries `allowStageChange: true`, the existing opt-in (no new metadata key): - -- **Who can press.** Unchanged from the opt-in: a reader the record page lets edit this record, on a stage field that is not read-only or computed, whose field edit grant holds for the stored row. Everyone else keeps the read-only path, and so does an editor while the record's inline edit holds unsaved changes. -- **Every press asks for confirmation**, then saves the stage through the record's ordinary update with the record's version, so the object's validation rules and `state_machine` decide. A refused move is shown under the path, naming any other field the move needs (for example a field made required by the new stage); nothing is written. -- **Only reachable stages are offered.** On every page with the opt-in, a stage the stage field's blocking `state_machine` cannot reach from the stored value is drawn as a readout, not a button, the same moves the record form, the grid's inline editor and the board offer. A field with no such rule offers every stage, and the server still judges each move. -- **Authored pages keep their own setting.** A page, or an authored `highlights` slot, that declares its own `record:path` keeps whatever it declares: `true`, `false`, or the key left out (a read-only rail). - -To keep a default record page's path read-only, assign the object an authored page (or a `highlights` slot) whose `record:path` leaves `allowStageChange` out or sets it `false`. diff --git a/.changeset/30076-forward-date-range-presets.md b/.changeset/30076-forward-date-range-presets.md deleted file mode 100644 index d08bf000ead..00000000000 --- a/.changeset/30076-forward-date-range-presets.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/core': minor -'@objectstack/lint': patch -'@objectstack/verify': patch -'@object-ui/core': minor -'@object-ui/components': minor -'@object-ui/i18n': minor ---- - -feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` - -Clause-②: yes (widening) -Metadata-protocol: added - -The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. - -`DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. - -`@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. - -In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. diff --git a/.changeset/30077-list-add-columns.md b/.changeset/30077-list-add-columns.md deleted file mode 100644 index b454546debe..00000000000 --- a/.changeset/30077-list-add-columns.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@object-ui/plugin-list': patch -'@object-ui/app-shell': patch ---- - -feat(plugin-list): a reader can add any field they may read to their own list from the Hide Fields popover (#30077) - -Clause-②: no -Metadata-protocol: none - -The Hide Fields popover on a Console object list (the toolbar's, and the Hide Fields section of the Display panel) listed only the columns the view already has, so a reader who wanted another field on their list had to ask an administrator to change the view for everyone. After the view's columns it now lists every other field of the object the reader may read, unticked: business fields first, then system fields such as Owner. A field the reader may not read is not offered, and neither is a field the object definition marks `hidden`. Ticking one adds it as a column of the reader's own list; unticking it takes it off again. - -The addition is the reader's personalization, stored beside their hidden fields in their `ui.list.` preference row (`perView..addedFields`), never written into the view: another user sees the view's columns, and "Reset to view" drops it with the rest of the reader's list. While it is stored the list's Personalized marker names "Column layout". An added column is fetched and drawn like a declared one, and an added lookup field is expanded in the list read as a declared lookup column is. - -A column the view declares with no label of its own (a bare `{ field }` or field name) is now listed in the Hide Fields popover (and in the Group and Row color pickers, which share its list) under the object definition's field label, the label the grid's header shows, instead of under its machine name (`expected_revenue` now reads Expected Revenue); a column's own label and a translation still come first. - -It is offered wherever a Hide Fields choice is stored: a view that declares `columns`, and a saved view. On a system view or fallback tab that declares no columns the popover lists the view's columns alone, as before. - -For a `ListView` host: two new host props, `addedFields` (the names among `schema.columns` the reader added) and `onAddedFieldsChange` (the door that stores them). Without `onAddedFieldsChange` the popover is exactly what it was. No metadata key and no spec change. diff --git a/.changeset/30081-seed-mode-default-create-only.md b/.changeset/30081-seed-mode-default-create-only.md deleted file mode 100644 index e496783079b..00000000000 --- a/.changeset/30081-seed-mode-default-create-only.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor -'@objectstack/metadata-protocol': minor -'@objectstack/cloud-connection': minor ---- - -feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) - -Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. -Metadata-protocol: semantic - - - -**BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. - -- **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. -- **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). -- **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. -- **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. -- **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. -- **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. -- **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. - -**The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. diff --git a/.changeset/30091-data-key-trial-page.md b/.changeset/30091-data-key-trial-page.md deleted file mode 100644 index 4db447a42f7..00000000000 --- a/.changeset/30091-data-key-trial-page.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-settings': patch ---- - -fix(service-settings): when two data-key files hold different keys, the boot judges the newest stored secrets as a page, not only the newest one, and reports a database split between the two keys - -Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (`StoredSecretReader` now answers the page, `KeyFileChoice` gains the `mixed` reason and a `tally`, `DataKeyOpensNothingError` gains `judged`) were added by #30042, whose note is still pending, so they first ship together in this release; no released export changes. -Metadata-protocol: none - -- **`@objectstack/service-settings`.** When the shared key file and the project's own key file hold different keys, the boot now trial-opens each of the newest stored `sys_secret` rows (up to ten) under both keys and reads with the key that opens more of them, on a tie the one that opens the newest. A row that opens under neither key (a corrupt or foreign row) no longer decides the boot: if an older row opens under one key, that key is used, and only a page where no row opens under either key keeps the production refusal. When each key opens some of the rows, the database holds secrets under both keys: the boot uses the key that opens more of them and prints an error naming both files, how many rows each key opened, and the fix. The boot's notices now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets") instead of claiming the chosen file opens all of the database's stored secrets. -- **`@objectstack/cli`.** The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) share the same trial, so they make the same choice. - -**The remedy for a split database.** Keep a copy of both files, then save again each secret that reads as unset: it is sealed under the key the boot uses. Once nothing reads as unset, put that key at the shared file (or set `OS_SECRET_KEY` to it). - -Supersedes, in the same release: the note for #30042 says the boot opens the newest stored secret under each key and quotes the refusal as "neither data key opens this database's stored secrets". After this change the boot judges the newest rows as a page, and the refusal reads "neither data key opens any of the N newest stored secrets". diff --git a/.changeset/30106-install-custom-pair-into-writable-pair.md b/.changeset/30106-install-custom-pair-into-writable-pair.md deleted file mode 100644 index e31f367bc49..00000000000 --- a/.changeset/30106-install-custom-pair-into-writable-pair.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cloud-connection': patch -'@objectstack/cli': patch ---- - -fix(cloud-connection,cli): an exported `.custom` installed on another environment lands in that environment's writable pair, and its overrides are served (ADR-0146 D2 / D3) - -Clause-②: no -Metadata-protocol: none - -- **Before.** `os package install /objectstack.json` of a base's paired customization package answered success and printed "Package installed into the running kernel", but the install registered the pair as code. None of its overrides was served, `GET /packages//customizations` listed none of them, and the pair turned read-only, so `ensure-customization` answered `writable: false` and every further save into it was refused `WRITABLE_PACKAGE_REQUIRED`. -- **Now.** `POST /api/v1/marketplace/install-local` installs a pair as what it is. The target's own pair is answered by its ensure door, which creates it when absent. Every item of the body is then written into it through the save door a Studio edit takes, with `mode: 'publish'` and the installer as actor, in load order. The overrides are served at once, the customization read lists them, and the pair stays editable. Nothing is registered as code or written to the cached-manifest directory. The answer carries `customizationPair` (`basePackageId`, `packageId`, `created`, `writable`), `writtenCount` and `written`, and the CLI prints them. -- **Refusals, nothing written.** A body key with no item form (code, a `translations` bundle, a key the install-key table does not classify), a nameless item, or two items claiming one name: `422`, every problem listed. A pair manifest the ensure door would not write: `400 PLUGIN_MANIFEST_INVALID`. A pair inside a multi-package artifact: `422`. A pair already read-only on the target: `409 RESOURCE_CONFLICT`, asked before the ensure door. If an earlier install registered it as code, the message names the uninstall and the restart that clear it. The ensure door's own refusal is relayed with its status and code. -- **A save refused part-way.** The answer is a failure with the save's code and status, naming the item and where it sits in the body. A pair this install created is deleted again. A pair that existed before is kept, and the answer lists the items written and the ones not written. -- **The CLI.** A runtime that predates this answers a pair as a managed install. The CLI now prints that as an error with the way back (uninstall, upgrade and restart, install again) and exits 1, instead of printing success. diff --git a/.changeset/30121-notify-link-defaults-to-trigger-record.md b/.changeset/30121-notify-link-defaults-to-trigger-record.md deleted file mode 100644 index d26815cbdac..00000000000 --- a/.changeset/30121-notify-link-defaults-to-trigger-record.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -feat(service-automation): a record-triggered flow's notification opens the record that started the run, without the author writing `sourceObject` and `sourceId` - -Clause-②: no — no accepted key, value or exported type changes; the node's existing `sourceObject` / `sourceId` / `actionUrl` keys keep their meaning when written. -Metadata-protocol: semantic - -- **Before.** A `notify` node built its click-through link only from an explicit `sourceObject` + `sourceId` pair (or an `actionUrl`). The obvious record-triggered flow — `recipients`, `title`, `message` — passed `os validate` and sent a notification whose link was empty, so a click opened the notification list instead of the record. -- **Now.** A `notify` node that writes none of `sourceObject`, `sourceId` and `actionUrl` links to the run's trigger record when the run has one: a record change (create or update), a time-relative sweep's row, or an action started on a record. The notification stores that record as its `source_object` / `source_id`, and the inbox opens `/OBJECT/ID`. -- **Unchanged.** An explicit pair or an explicit `actionUrl` wins, as before. A half-written pair is still dropped rather than defaulted. A run with no trigger record (a schedule, an inbound API post, a manual start with no record) links nowhere, as before. A delete event is not linked: its record is gone once the write lands. -- **To link elsewhere** from a record-triggered flow, write the `actionUrl` or the `sourceObject` + `sourceId` pair the notification should open. diff --git a/.changeset/30123-scaffold-security-datasets.md b/.changeset/30123-scaffold-security-datasets.md deleted file mode 100644 index 4b19774ff6b..00000000000 --- a/.changeset/30123-scaffold-security-datasets.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/cli': patch -'create-objectstack': patch ---- - -feat(cli,create-objectstack): a new project has a wired place for permission sets and datasets, `os g permission` / `os g dataset` write them, and the generated AGENTS.md names the package manager the project was created with - -Clause-②: yes (widening) -Metadata-protocol: none - -- **`create-objectstack`: the blank starter ships `src/security/index.ts` and `src/datasets/index.ts`**, empty barrels already handed to the stack's `permissions` and `datasets` keys in `objectstack.config.ts`, like the eight directories it wired before. A permission set or a dataset exported from them is loaded and counted by `validate` with no edit to the config. The generated `AGENTS.md` lists both directories. -- **`@objectstack/cli`: `os g permission NAME [--object OBJECT]`** writes `src/security/NAME.permission.ts`, a `definePermissionSet` granting read, create and edit (no delete) on the object `--object` names, or the stack's only object, with `readScope: 'own'` declared. **`os g dataset NAME [--object OBJECT]`** writes `src/datasets/NAME.dataset.ts`, a `defineDataset` over that object with a `record_count` measure, grouped by the record's title field when it has one. `defineDataset` types the literal, so it needs no `as const`. Both pass `os validate`, `os build` and `os lint` with no finding on a fresh project. `os init` wires both directories too. -- **`create-objectstack`: the generated `AGENTS.md` (and `.github/copilot-instructions.md`) says `pnpm run validate` in a project created with pnpm**, and `npm run …` in one created with npm, the same package manager the install line and "Next steps" name. It used to say `npm run` in every project. diff --git a/.changeset/30140-compare-to-forward-presets.md b/.changeset/30140-compare-to-forward-presets.md deleted file mode 100644 index 899e188ee04..00000000000 --- a/.changeset/30140-compare-to-forward-presets.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/i18n': patch -'@object-ui/plugin-dashboard': patch -'@object-ui/plugin-charts': patch ---- - -fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself - -Clause-②: no -Metadata-protocol: none - -- **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. -- **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). -- **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). -- **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. diff --git a/.changeset/30178-numeric-screen-fields-refuse-strings.md b/.changeset/30178-numeric-screen-fields-refuse-strings.md deleted file mode 100644 index 523e5bc7618..00000000000 --- a/.changeset/30178-numeric-screen-fields-refuse-strings.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-automation': patch -'@object-ui/app-shell': patch ---- - -fix(automation)!: a `currency` or `percent` screen field refuses a non-number on resume, as a `number` field does, and the console renders `percent` as a numeric input (#30178) - -Clause-②: yes (narrowing) — `POST …/runs/RUN/resume` refuses a string under a `type: 'currency'` or `type: 'percent'` screen field, a bag it accepted before. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing at the screen-flow resume door. - -- **Before.** A `type: 'percent'` or `type: 'currency'` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too. The bound compares numbers, so it never judged a string, and the run consumed its suspension on the value. The console rendered `percent` as a text input and posted a string, so a `percent` field's `min` / `max` held for no console user. -- **Now.** `number`, `currency` and `percent` are one value domain: a present value that is not a finite JSON number is refused with `invalid_type` inside `400 INVALID_SCREEN_INPUT`, the suspension is kept, and a corrected resume still lands. A numeric string is not read as the number it spells. The console renders `percent` as a numeric input carrying `min` / `max`, so it posts a number and the browser stops an over-limit value before any request. -- **Unchanged.** An absent value and a hidden field are not judged; `rating`, `slider`, `progress`, `text` and untyped fields still take a string; a JSON number on `currency` / `percent` is judged against the bound as before. -- **For a caller that skips the console.** The resume bag carries the JSON number (`60`), not a string; the refusal names the field and says it must be a number. No stored metadata changes, so there is nothing for `objectstack migrate meta` to rewrite. diff --git a/.changeset/30178-screen-field-numeric-kinds-contract-text.md b/.changeset/30178-screen-field-numeric-kinds-contract-text.md deleted file mode 100644 index 3889a8161dd..00000000000 --- a/.changeset/30178-screen-field-numeric-kinds-contract-text.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec)!: the screen-field contract names `number`, `currency` and `percent` as the numeric value domain the resume enforces (#30178) - -Clause-②: yes (narrowing) — the resume door refuses a string under a `type: 'currency'` or `type: 'percent'` screen field (`@objectstack/service-automation`); this package's `ScreenFieldSpec` and `ScreenFieldConfigSchema` text now states that rule. No schema shape changes. -Metadata-protocol: none - - - -**BREAKING** (described here, enforced in `@objectstack/service-automation`): a `currency` or `percent` screen field takes a finite JSON number on resume, as a `number` field does; a numeric string is refused with `invalid_type`, never coerced. `ScreenFieldSpec.type` / `.min` / `.max` and `ScreenFieldConfigSchema.type` / `.min` / `.max` say so, and the `type` describe text names the three kinds. diff --git a/.changeset/30184-requiredwhen-transition-gate.md b/.changeset/30184-requiredwhen-transition-gate.md deleted file mode 100644 index 7b94ee8d994..00000000000 --- a/.changeset/30184-requiredwhen-transition-gate.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@object-ui/core': patch -'@object-ui/components': patch ---- - -fix(ui): the record Edit form judges a field's `requiredWhen` as the engine does, a transition gate against the stored record, so a record already inside the gate without the value saves an unrelated edit - -Clause-②: yes (widening) -Metadata-protocol: none - -This widens the public surface of `@object-ui/core`: the new export `requiredWhenRestsOnStoredRecord` and the new `requiredWhenAtRest` field in the return type of `resolveFieldRuleState`. - -- **`@object-ui/components` form renderer:** on an Edit form (`previousValues` set), a field's `requiredWhen` is required only when the stored record complied with it. A stored record that already violates it (the predicate holds over the stored record and its value is missing, as with a contract activated before its signed-copy rule existed) is not blocked any more: it saves an edit that leaves the field empty, as the engine and REST `PATCH` already accept. This follows `Field.requiredWhen`: the write is refused only when the merged record violates the requirement AND the pre-write record complied. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A Create form enforces as before. The "Required to run" note under an empty field (the update-action heads-up) is no longer shown for such a stored record, because the engine judges the action's write against the same stored record. -- **`@object-ui/core`:** `resolveFieldRuleState` takes `statics.name` (the field's name) and returns `requiredWhenAtRest`. With `previous` and `statics.name` given, a stored record already violating `requiredWhen` resolves `required: false` and `requiredWhenAtRest: true`. A caller that passes no `name` keeps the plain verdict. New export `requiredWhenRestsOnStoredRecord(pred, name, stored, scope?)`, the stored-record check alone. A predicate that cannot be evaluated over the stored record counts it as compliant, as on the server. diff --git a/.changeset/30192-environment-scoped-security-provider.md b/.changeset/30192-environment-scoped-security-provider.md deleted file mode 100644 index c84a014503a..00000000000 --- a/.changeset/30192-environment-scoped-security-provider.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the `/security` routes, and the auth, settings, i18n and metadata seams, answer from the request's environment kernel when one REST plugin fronts many environments - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `security` provider read the HOST kernel's service whatever environment it was asked for, and the unscoped (hostname-routed) suggested-binding mount asked it for no environment at all. The host has no `security` service, so `GET /security/suggested-bindings` and `POST /security/suggested-bindings/:id/confirm` answered 501 on every hosted environment: no owner could accept an installed app's default permission set, and every invited member read 403 on that app's data. `security/explain` and the record, export and meta doors that read the same service fell back to the host's occupant whenever the environment's kernel did not answer. -- **What changes.** Given an environment id and a registered `kernel-manager`, the `security` provider now reads `security` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, sharing-rule and analytics providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's occupant is no longer a fallback. -- **The audited seams.** The `auth`, `settings`, `i18n` and `metadata` providers are environment-scoped the same way. On a hosted runtime the REST face's execution context now localizes (time zone, locale, currency) from the environment's own settings, as the runtime dispatcher's already did, instead of the host's. An environment whose kernel carries no `auth` service is no longer authenticated by the host's identity store. The `tenancy` provider keeps reading the plugin's own kernel: every consumer reads the bound environment kernel's `tenancy` first. -- **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads every one of these services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. -- **No migration.** No configuration, route, response shape or export changes. diff --git a/.changeset/30193-override-label-pair-precedence.md b/.changeset/30193-override-label-pair-precedence.md deleted file mode 100644 index 87f30db905b..00000000000 --- a/.changeset/30193-override-label-pair-precedence.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(i18n): one label order on an overridden item, for all eight document translators — the pair's translation, then the override's own label, then the base's translation - -Clause-②: yes (widening) -Metadata-protocol: none - -- **New option `TranslateDocumentOptions.pairBundle`.** The translations the served item's paired `.custom` package ships itself (ADR-0146 D2/D3), locale-keyed as `bundle` is. The serving layer's `bundle` is one merged catalog per locale with the package ids dropped, so only this option can tell a translation the pair ships from the base's. -- **The order, per translatable string.** A string the override changed (`valueOverridesPackagedBase` against `packagedBase`, the comparison every translator already makes) reads the pair's translations only, along the locale chain, and otherwise keeps its own text: the base's translation is never read for it. A string the override left alone reads, locale by locale, the pair's translation and then the deployment catalog, so a pair that ships only `en` never displaces the base's `zh-CN` on an untouched label. Nested labels follow the same rule: widget titles, filter and option labels, view bulk-action defs and form sections, report blocks, field labels, help and options, page component copy, tab items and the header, dataset members, app navigation nodes and book groups. -- **`translatePage`, `translateDataset`, `translateApp` and `translateBook` now read `packagedBase`** (their third parameter widens from `ResolveOptions` to `TranslateDocumentOptions`), each string judged against the base's counterpart: the page's components by id and its header, the dataset's members by `name`, the app's navigation nodes by `id`, the book's groups by `key`. A page header title the override left as the base wrote it translates as the base's label. -- **Nothing moves for an existing caller.** With no `pairBundle` (or one that answers none of the chain's locales) every translator gives exactly the answer it gave before, and with no `packagedBase` nothing counts as changed. Passing both for a `.custom` override is the serving layer's half (#30062). -- An object's `validations[].message` and its inline `actions` keep reading the merged catalog: the rule evaluator answers the same message from `i18n.t()` with no pair input, and the refusal text must equal the served text. diff --git a/.changeset/30197-compare-to-past-presets.md b/.changeset/30197-compare-to-past-presets.md deleted file mode 100644 index 6d83b5186b2..00000000000 --- a/.changeset/30197-compare-to-past-presets.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@object-ui/core': patch ---- - -fix(core): a compareTo widget on an already-past date range (yesterday, last week / month / quarter / year, last N days) compares with the period before it, and previousYear from 29 February stays in February - -Clause-②: no -Metadata-protocol: none - -- **`shiftFilterByCompareTo`, `kind: 'previousPeriod'`.** `{yesterday}` and the `last_*` tokens were never moved, so a dashboard filtered to `yesterday`, `last_week`, `last_month`, `last_quarter` or `last_year` compared the window with itself and the delta always read 0. They now compare with the period before them: `{yesterday}` with `{2_days_ago}`, and `last_week_*` … `last_year_*` with the week, month, quarter or year before last. The macro vocabulary has no token for "two periods back", so those windows are computed: the `last_*` start is resolved in the display zone, moved back one period, and the end is the day before it. -- **Rolling windows.** `{N_days_ago}` … `{today}` (`last_7_days`, `last_30_days`, `last_90_days`; N + 1 days) moved only its end and compared with the N days ending yesterday, which overlap it. It now compares with the N + 1 days before it, `{(2N+1)_days_ago}` … `{(N+1)_days_ago}`, the same window the dataset executor computes. -- **`kind: 'previousYear'` from 29 February** re-resolved the filter at 1 March of the year before (`new Date(2027, 1, 29)`), so `this_month` compared with March. The day is now clamped to the month's last day, 28 February. -- **`compareToTrendLabelKey`** reads all of these as `vsPreviousPeriod` (they read `vsLastWeek`, `vsLastMonth`, `vsLastQuarter`, `vsLastYear` and `vsYesterday`); no `dashboard.trend.*` key names "the month before last". `today`, the `this_*` and the forward presets compare and label as before. diff --git a/.changeset/30198-org-admin-update-refused-before-lookup.md b/.changeset/30198-org-admin-update-refused-before-lookup.md deleted file mode 100644 index c41e9e9778f..00000000000 --- a/.changeset/30198-org-admin-update-refused-before-lookup.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an organization administrator's update by id is refused before the row is looked up, whatever the id names - -Clause-②: no -Metadata-protocol: none - -An organization administrator (`organization_admin` or `organization_admin_no_bypass`, holding no `adminScope`) updates an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. An edit in place is never part of that administrator's right, so the write is refused before the row is looked up: `403 PERMISSION_DENIED`, with the same sentence and details for a row, for a row the administrator cannot read, and for an id that names no row. - -Unchanged: a removal by id of a row the administrator cannot read, or of an id that names no row, answers the read door's `404 RECORD_NOT_FOUND`. An administrator who also holds an `adminScope` is judged by that scope. diff --git a/.changeset/30199-suggested-binding-reconsider.md b/.changeset/30199-suggested-binding-reconsider.md deleted file mode 100644 index ab124ecd833..00000000000 --- a/.changeset/30199-suggested-binding-reconsider.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch ---- - -fix(app-shell): a dismissed or revoked default permission set in the suggested-bindings panel can be reconsidered (#30199) - -Clause-②: no -Metadata-protocol: none - -The suggested-bindings panel (the install dialog and Marketplace package page, and Studio's Access pillar) drew a dismissed suggestion, or a revoked acceptance, as a state line with no action. An administrator who chose Dismiss could only take it back by calling `POST /api/v1/security/suggested-bindings/:id/reopen` by hand, although the server has offered that door all along. - -Such a row now offers "Reconsider". It calls the re-open door, and the row comes back as a pending suggestion with Accept and Dismiss (a Guest row with Dismiss only, as before). Re-opening grants nothing: the set reaches the Everyone position only when it is accepted again, under the same checks. The button is shown to the same callers who see Accept and Dismiss: a caller without `manage_metadata` is refused the list and sees no panel. A refusal (the row is gone, the set is no longer suggested, or the deployment's security service has no re-open door) is shown with the server's own message, and the panel re-reads the list. - -New client call: `reopenSuggestedBinding(id)`, beside `dismissSuggestedBinding`. New strings: `marketplace.suggestedBindings.reopen`, `.reopening` and `.reopenedToast` in all ten locale packs, and `engine.studio.access.suggestReopen`, `.suggestReopening` and `.suggestReopenedToast` (en, zh). diff --git a/.changeset/30204-feature-flags-namespace-removed.md b/.changeset/30204-feature-flags-namespace-removed.md deleted file mode 100644 index b2e94e5be60..00000000000 --- a/.changeset/30204-feature-flags-namespace-removed.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/service-settings': patch -'@objectstack/platform-objects': patch ---- - -fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204) - -Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`. - -- **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it. -- **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged. -- **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels). -- **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will. - -The one-line fix for a host: - -- FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it. diff --git a/.changeset/30215-uninstall-forgets-engine-manifest.md b/.changeset/30215-uninstall-forgets-engine-manifest.md deleted file mode 100644 index 52d422d7308..00000000000 --- a/.changeset/30215-uninstall-forgets-engine-manifest.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): an uninstalled package no longer reads as read-only until a restart - -Clause-②: yes (widening) — `@objectstack/objectql`'s exported `SchemaRegistry` gains the public method `onPackageUninstalled(listener)`, which returns its unsubscribe. Nothing is refused that was accepted before. -Metadata-protocol: none - -An administrator who uninstalled a package through `DELETE /api/v1/marketplace/install-local/:id` got a 200 "withdrawn from the running kernel", but on the same instance the package id still read as a booted code package until the server restarted. Uninstalling a `.custom` pair that an older runtime had installed as code, as the pair install now prescribes, left `POST /api/v1/packages//ensure-customization` answering `created: true` with `writable: false`, so nothing could be customized into the new pair. The engine's manifest map, which the ADR-0070 writability check reads first, kept the id after the registry had withdrawn the package. - -`SchemaRegistry.uninstallPackage` now announces each completed uninstall to `onPackageUninstalled` listeners, after the package record is gone (never for a refused uninstall), and the engine drops its manifest entry there, so the id is released on whichever door withdrew the package, not per door. A reinstall of the same id registers it again as before. A listener that throws does not turn the completed uninstall into a refusal; it is logged at `error`. diff --git a/.changeset/30218-environment-scoped-objectql-provider.md b/.changeset/30218-environment-scoped-objectql-provider.md deleted file mode 100644 index 6270cba1f65..00000000000 --- a/.changeset/30218-environment-scoped-objectql-provider.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -fix(rest): the transactional `POST /batch`, the `/meta` object-state and published reads, and the meta read gate's disabled-package check use the request's environment's data engine when one REST plugin fronts many environments - -Clause-②: no -Metadata-protocol: none - -- **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `objectql` provider read the HOST kernel's engine whatever environment it was asked for, and the unscoped (hostname-routed) mount asked it for no environment at all. `POST /batch` therefore opened its transaction on the host's engine and sent its update and delete ops through it, while its create ops went to the environment's own protocol, joined to the host's transaction. `GET /meta/object/:name/state/:field` judged the object by the host engine's registry, and `GET /meta/:type/:name/published` and the meta read gate asked the host engine's registry whether an environment's package is disabled. The request's execution context already read the environment's engine, so one request used two kernels. -- **What changes.** Given an environment id and a registered `kernel-manager`, the `objectql` provider now reads `objectql` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, security and other providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's engine is no longer a fallback: an environment whose kernel carries no engine answers `POST /batch` with `501 NOT_IMPLEMENTED`. -- **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads the engine on the plugin's own kernel exactly as before, through the same factory-aware lookup: a factory that fails to build is still a loud `503`, and only an engine that was never registered reads as absent. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. -- **No migration.** No configuration, route, response shape or export changes. diff --git a/.changeset/30223-hitl-continuation-not-user-bubble.md b/.changeset/30223-hitl-continuation-not-user-bubble.md deleted file mode 100644 index eb9d3fa0ec6..00000000000 --- a/.changeset/30223-hitl-continuation-not-user-bubble.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch -'@object-ui/app-shell': patch ---- - -fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message - -Clause-②: no -Metadata-protocol: none - -After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL ]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before. diff --git a/.changeset/30228-queue-reclaim-expired-lease.md b/.changeset/30228-queue-reclaim-expired-lease.md deleted file mode 100644 index e9de72d922a..00000000000 --- a/.changeset/30228-queue-reclaim-expired-lease.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-queue': patch ---- - -fix(service-queue): a job whose worker died mid-handler is reclaimed once its lease runs out, instead of staying `running` forever - -Clause-②: no — restores the documented `leaseMs` reclaim; no accept set or export widens -Metadata-protocol: none - -`DbQueueAdapterOptions.leaseMs` documented a lease "before another worker may reclaim", but the claim read only `pending` rows, so a worker that crashed, was OOM-killed or was redeployed mid-handler left its `sys_job_queue` row `running` for good: never retried, never dead-lettered. - -- **Reclaim.** A poll tick reads the table's `running` rows (one read for every queue, at most once per lease window) and returns each whose `locked_until` has passed to the retry path. The lost run counts as an attempt: the row goes back to `pending` on its backoff, or to `dlq` once `maxAttempts` is spent, with `last_error` saying the lease expired. The write is a compare-and-set on the expired lease, so a holder that renewed in the meantime keeps its run. -- **Renewal.** While a handler runs, its worker moves its lease a full window ahead every third of `leaseMs`, so a slow but live handler is never reclaimed and run twice. A handler must not block the event loop for most of a lease window. -- **Sole writer.** With `soleWriter` no other process claims rows, so the reclaim reads once per boot (finding what a crashed previous process left behind) and again only when one of this process's own claims was left unsettled by a failed write and its lease runs out. `nextDueAt` folds in those leases and the earliest live lease the last read saw, at no read of its own, so the loop wakes for them instead of at the idle ceiling. -- **Cost.** Without `soleWriter`, an idle queue pays one more indexed read per lease window (default 30 s): an idle hour at the defaults goes from 125 to 246 reads. With `soleWriter`, one more read per boot: a fresh kernel's first minute goes from 2 to 3 reads, and an idle hour carries no other. diff --git a/.changeset/assignment-write-unaddressable-row-not-found.md b/.changeset/assignment-write-unaddressable-row-not-found.md deleted file mode 100644 index a088e923502..00000000000 --- a/.changeset/assignment-write-unaddressable-row-not-found.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an assignment write the administrator cannot address answers not-found, as a read does; a refusal names only values the caller can read - -Clause-②: no -Metadata-protocol: none - -An organization administrator or a delegated administrator (a caller holding an `adminScope`, an organization administrator who holds one included) updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. - -- **A row the caller cannot read, and an id that names no row,** now answer `404 RECORD_NOT_FOUND`: the read door's answer for that object and id, which is what a by-id write of any other object already answers. Both used to answer `403 PERMISSION_DENIED` ("the assignment row … could not be read"). An organization administrator's edit in place is asked the same question first, so an edit of a row it reads is still refused as an edit. -- **A caller the read door refuses outright** (no read grant on the table) still gets that read refusal. -- **A refusal names a value only when the caller can read it.** A delegated administrator's `DELEGATION_SCOPE_VIOLATION` names the row's position, business unit, holder or permission set only when the caller's own read returns that column at its stored value, or when the caller wrote the value itself. Otherwise the sentence is the same without the value (for example "the assignment's position distributes a permission set which is not in the scope's allowlist"), and `details` leaves the value out. The verdict does not change: it is still taken on the stored row. -- **Unchanged.** A row the caller reads keeps its verdict and its guidance. Inserts and tenant-level administrators are judged as before, and every write that was admitted is still admitted. - -For clients: a by-id update or delete of an assignment row that is gone now answers `404`, as it does on every other object. diff --git a/.changeset/change-email-notice-every-sign-in.md b/.changeset/change-email-notice-every-sign-in.md deleted file mode 100644 index 97a0c1719a7..00000000000 --- a/.changeset/change-email-notice-every-sign-in.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): a change-email request notifies the current address whatever way the caller is signed in - -Clause-②: no -Metadata-protocol: none - -- **The rule.** Every `POST /api/v1/auth/change-email` request that is accepted mails the account's current address the `auth.email_change_notice`, whatever way the caller is signed in. The notice goes to the account the request changes: its address is read from the session the endpoint itself acted on. -- **Unchanged.** The notice's template and text; the confirmation mail to the requested address; a refused request still mails nothing; the notice still never blocks the change. diff --git a/.changeset/cli-connection-label-shared-url-grammar.md b/.changeset/cli-connection-label-shared-url-grammar.md deleted file mode 100644 index cf4484ecbd4..00000000000 --- a/.changeset/cli-connection-label-shared-url-grammar.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): a connection label printed by the CLI removes a password or a credential query pair wherever the URL rules read one - -Clause-②: no -Metadata-protocol: none - -- **One credential grammar.** The database label that `os dev`, `os start` and `os serve` print, and the migrate and resync confirms, now remove URL credentials through the same shared function the datasource read door applies (`redactUrlCredentials` from `@objectstack/spec/data`): the userinfo password, and every credential query pair judged by the shared query-pair judge and its one list of names. Wherever those rules read a credential, on every path the label takes, the label drops it. When the shared rules read more, the label follows with no edit of its own. -- **Unchanged.** Every value the shared rules read no credential in prints exactly as before: `postgres://`, `libsql://` and `file:` values, values with no userinfo, a username-only userinfo, and everything the URL parser already cleaned. diff --git a/.changeset/cloud-3485-turso-remote-balanced-group.md b/.changeset/cloud-3485-turso-remote-balanced-group.md deleted file mode 100644 index a6945f0d61d..00000000000 --- a/.changeset/cloud-3485-turso-remote-balanced-group.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-turso': patch -'@objectstack/driver-sql': patch ---- - -fix(driver-turso): the remote face joins a large `$and` / `$or` as balanced halves, so comment and attachment reads fit a hosted libSQL server's depth limit - -Clause-②: no -Metadata-protocol: none - -On a remote Turso / libSQL database (`TursoDriver` with a `libsql://`, `https://` or `wss://` url and no `syncUrl`, as every hosted environment runs), every read of `sys_comment` and `sys_attachment` answered `500 DATABASE_ERROR` once the deployment registered roughly a hundred objects, as an installed app does. The driver's log named the cause: `Expression tree is too large (maximum depth 100)`. The comment and attachment read gates OR one `ReadableIds` branch per registered parent object, and the remote face's compiler joined a group's members into one flat chain, one expression level per member, while the hosted server refuses a tree deeper than 100. - -The local face (`SqlDriver`) has emitted such groups as balanced halves since #29888. The remote face compiles its SQL text itself, so that change never reached it. It now regroups by the same rule, shared from `@objectstack/driver-sql` as `joinBalancedGroup` (with `FLAT_GROUP_MAX_MEMBERS`): a group of at most 8 members compiles byte-identical to before, and a larger one as two parenthesised halves, recursively, so the depth it adds grows with the logarithm of its size. Measured on the gate's own shape with 134 branches and a three-level master chain: 162 levels before, 38 after. - -The regrouped expression selects exactly the rows the flat one did, on every dialect: `AND` and `OR` are associative over SQL's three-valued logic, the members keep their order, and so do their bind values. No gate, scope or tenant wall changes. diff --git a/.changeset/cloud-3565-chatbot-tool-labels.md b/.changeset/cloud-3565-chatbot-tool-labels.md deleted file mode 100644 index c795c1967ed..00000000000 --- a/.changeset/cloud-3565-chatbot-tool-labels.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@object-ui/i18n': patch ---- - -feat(i18n): `chatbot.tool.create_record` / `update_record` / `remove_sample_data` labels and the record-write card's `console.ai.recordWrite*` labels in all ten packs; `chatbot.tool.draft_record` retired - -Clause-②: no -Metadata-protocol: none - -The spec registry now lists the cloud chat agents' confirmed record write and the sample-data cleanup (cloud#3565) and drops `draft_record`, so the chatbot's tool timeline gets a sentence for each new step in every pack ("Create the record" / "新建记录", "Change the record" / "修改记录", "Remove the sample data" / "清理示例数据") and loses the retired one, keeping `toolLabels-locale-parity-7481` and `toolLabels-sentences-12724` green. The console's record-write card gets its eleven labels under `console.ai.recordWrite*`: the create and update titles (with the object's label), the three buttons 确认 / 打开完整表单 / 取消, the two stored reasons, the "not included" lead-in and the three states (writing, written, cancelled) — plus the 「清理示例数据」 card's four: `console.ai.sampleDataCleanupTitle` (its title over the seeded tables and counts), `console.ai.sampleDataCleanupIrreversible` (「删除后无法恢复」 — the removal cannot be undone, said on every card), and the two count families `console.ai.sampleDataCleanupAbsent` (the recorded rows the card does not list) and `console.ai.sampleDataCleanupEdited` (the rows kept because someone edited them), each with every plural slot its language selects (objectui#11445). diff --git a/.changeset/cloud-3565-record-write-card.md b/.changeset/cloud-3565-record-write-card.md deleted file mode 100644 index 33829500d65..00000000000 --- a/.changeset/cloud-3565-record-write-card.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@object-ui/plugin-chatbot': patch ---- - -feat(plugin-chatbot): the record-write card — ONE record the chat proposes and the user confirms, with 确认 / 打开完整表单 / 取消 (cloud#3565) - -Clause-②: no -Metadata-protocol: none - -The cloud chat's `create_record` / `update_record` tools queue a HITL pending action in the user's name and answer the approval envelope plus the record: `{ status:'pending_approval', pendingActionId, recordWrite:{ operation, objectName, objectLabel, recordId?, label?, reason?, fields:[{ name, label, type, value, current? }], dropped } }`. The chat now draws that as a card instead of the generic Approve / Deny strip and the raw payload: - -- **New export** `detectRecordWrite(result)` (with the `RecordWrite` and `RecordWriteField` types), lifted onto `ChatbotEnhancedToolInvocation.recordWrite` by the live mapper beside `pendingActionId`. It reads the bare object, the persisted `{ type:'text', value }` wrapper and a JSON string; an envelope without the pending id or without a field to confirm is not a card. -- **The card** (`data-testid="record-write"`): the create or update title with the object's label, the record's label and reason, every field as a labelled row — an update shows the stored value struck through, then →, then the proposed one — the fields the server left out and why, and three buttons. 确认 calls `onToolApprove(toolCallId, true)` (the pending action's dispatcher writes as the user); 取消 calls it with `false` and `recordWriteCancelReason`; 打开完整表单 calls it with `false` and `recordWriteOpenFormReason` FIRST, then the new `onOpenRecordForm(write)` prop, so a record saved in the form is never written a second time from the card (disabled where the host passes no handler). After a decision the card says written (with the record's link through `onOpenRecord`, read from the dispatcher's `{ status:'written', objectName, recordId, label }` result), cancelled, or the server's refusal. -- **New props**: `onOpenRecordForm`, `recordWriteCreateTitle(objectLabel)`, `recordWriteUpdateTitle(objectLabel)`, `recordWriteConfirmLabel`, `recordWriteOpenFormLabel`, `recordWriteCancelLabel`, `recordWriteCancelReason`, `recordWriteOpenFormReason`, `recordWriteDroppedLabel`, `recordWriteSubmittingLabel`, `recordWriteWrittenLabel`, `recordWriteCancelledLabel`, all with English defaults. -- **「清理示例数据」** rides the same seam: `detectSampleDataCleanup(result)` (with the `SampleDataCleanup` type) lifts `{ status:'pending_approval', pendingActionId, sampleDataCleanup:{ tables:[{ objectName, label, count }], total, absent, edited } }` onto `sampleDataCleanup`, and the chat draws the tables and counts (`data-testid="sample-data-cleanup"`, title from the new `sampleDataCleanupTitleLabel` prop) above the generic Approve / Deny strip, with the raw payload hidden. The card always says the removal cannot be undone (`sampleDataCleanupIrreversibleLabel`, default "Removed rows cannot be recovered." — there is no recycle bin until objectstack#3146), says how many recorded rows it does not list when `absent` is above 0 (`sampleDataCleanupAbsentLabel(count)`) and how many stay because someone edited them when `edited` is above 0 (`sampleDataCleanupEditedLabel(count)`; an envelope without `edited` reads as 0). -- **`ToolDecisionState`** (widened, additive): every state may carry `decision: 'approved' | 'rejected'`, and an approved `success` may carry `result`, the dispatcher's answer. `useHitlInChat` sets both; the generic strip keeps reading `message` alone. diff --git a/.changeset/cloud-3565-spec-record-write-tool-names.md b/.changeset/cloud-3565-spec-record-write-tool-names.md deleted file mode 100644 index 05bde56e6cd..00000000000 --- a/.changeset/cloud-3565-spec-record-write-tool-names.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -feat(spec): `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']` lists `create_record`, `update_record` and `remove_sample_data`; `draft_record` is retired - -Clause-②: no -Metadata-protocol: none - -The cloud chat agents now create or change ONE record of the user's data after the user confirms it on a card, and remove the sample rows a seed replay recorded after a card listing them (cloud#3565, the maintainer's ruling of 2026-11-04). The three tools replace `draft_record` (cloud#3383), whose "draft it and the user saves it in a form" ending was never built. The registry lists the three names in their alphabetical order and drops the retired one, so `PLATFORM_PROVIDED_TOOL_NAMES` carries them and a skill whose `tools[]` names them resolves under ADR-0109 reference integrity, while a skill still naming `draft_record` is reported as a fictional tool (`ai-skill-tool-unresolved`). That is the whole change: three accepted `skill.tools[]` values in, one out. No schema shape changes, no export is added or removed. - -**Reach of the retirement.** `draft_record` was a published constant, so removing it breaks any skill whose `tools[]` still names it. Measured before this landed: no skill in this repo names it (`validate-ai-tool-references.test.ts` names it only as the control proving the report fires; the two spec page fixtures called `draft_record` are page names, not tools); the one skill anywhere that names it is the cloud chat's `record_drafting` skill, which the cloud half of cloud#3565 (cloud#3574) moves to `create_record` / `update_record` in the same landing. A third-party skill that still names it reports `ai-skill-tool-unresolved` at validation and is not silently dropped. That is why this is graded `patch` and not left as a silent one. diff --git a/.changeset/connect-agent-work-assistants.md b/.changeset/connect-agent-work-assistants.md deleted file mode 100644 index 56676c7ca6c..00000000000 --- a/.changeset/connect-agent-work-assistants.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@object-ui/app-shell': patch -'@object-ui/i18n': patch -'@objectstack/mcp': patch ---- - -feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card - -Clause-②: no -Metadata-protocol: none - -The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: - -- **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. -- **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. -- **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. - -Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. - -The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. diff --git a/.changeset/delegated-admin-unreadable-row.md b/.changeset/delegated-admin-unreadable-row.md deleted file mode 100644 index 8d4df1af836..00000000000 --- a/.changeset/delegated-admin-unreadable-row.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): a delegated administrator's refused assignment write answers the same for a row it cannot read as for a missing one - -Clause-②: no -Metadata-protocol: none - -A delegated administrator — a caller holding an `adminScope`, an organization administrator who holds one included — updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks first whether the caller itself reads that row, in its own context. - -- **A row the caller cannot read** is answered exactly as an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. Both are answered at the gate, before any later step judges the write. -- **A row the caller reads** keeps the verdict and the guidance it had: the gate judges the stored row, as before. -- **A caller the read door refuses outright** (no read grant on the table) is answered as before: the read refusal, for every id the gate refuses, and the writes it was admitted are still admitted. -- **Unchanged.** Inserts, tenant-level administrators, and organization administrators who hold no scope are judged as before. diff --git a/.changeset/driver-options-declared-keys-only.md b/.changeset/driver-options-declared-keys-only.md deleted file mode 100644 index fb1a343ff99..00000000000 --- a/.changeset/driver-options-declared-keys-only.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a data driver receives the declared driver options and nothing else - -Clause-②: no -Metadata-protocol: none - -The engine → driver hop is a closed contract, as the engine's own option bags are: every driver call the engine makes is handed the keys `DriverOptionsSchema` declares and no other key. - -- **Which calls.** `find`, `findOne`, `count`, `aggregate`, `insert`, `update` and `delete` (by id and multi-row), the reads and writes those verbs make along the way, and the raw `execute` door. -- **What arrives, unchanged.** `transaction`, `tenantId`, `tenantIds`, `timezone`, `preserveAudit` and `bypassTenantAudit` reach the driver exactly as before, and a caller's explicit value still wins. The schema's other declared keys pass when a caller sets them at a door that admits them, which is `insert` and the raw `execute` door; `find`, `findOne`, `update` and `delete` admit only the six keys above at their own door. The retired `timeout` tombstone is not handed on. -- **What stays with the engine.** Undeclared keys in a caller's engine option bag stay with the engine and are no longer copied onto a driver's options. A driver reads the query from its `query` argument, as the driver contract declares. -- **Hooks are unchanged.** An `after*` hook still reads `input.options` as the caller's bag with the driver options merged on. -- **Who moves.** A driver that read a key `DriverOptionsSchema` does not declare off its options argument: read the query from the `query` argument, or have the key declared on `DriverOptionsSchema` first. No driver in this repository reads one. diff --git a/.changeset/org-admin-removal-caller-read.md b/.changeset/org-admin-removal-caller-read.md deleted file mode 100644 index c57f14e8cd7..00000000000 --- a/.changeset/org-admin-removal-caller-read.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an organization administrator's refused assignment write answers the same for a row it cannot read as for a missing one - -Clause-②: no -Metadata-protocol: none - -An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks the administrator's own read, in its own context, whether that row is there to remove. - -- **The removal of a row the administrator cannot read** is answered exactly as the removal of an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. -- **A row the administrator can read** is judged on the stored row. A refusal names the organization boundary, and names the row's organization, position or permission set only when the administrator's own read returns that value as stored. -- **Unchanged.** An edit in place of a row it reads is refused as before, every removal that was admitted is still admitted, and a tenant-level administrator is judged as before. diff --git a/.changeset/org-admin-removal-stored-row-verdict.md b/.changeset/org-admin-removal-stored-row-verdict.md deleted file mode 100644 index 032dd8a53c1..00000000000 --- a/.changeset/org-admin-removal-stored-row-verdict.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): an organization administrator's assignment removal is judged on the stored row, and its refusal names only values the caller can read - -Clause-②: no -Metadata-protocol: none - -An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. Once its own read returns the row, the delegated-administration gate (ADR-0090 D12) judges the removal on the stored row, as it judges a delegated administrator's write. - -- **The verdict is the stored row's.** A field rule or a masking rule on the row's `position` or `permission_set` does not change it: the removal of a row inside the organization boundary is admitted, and the removal of a row outside it is refused. -- **A refusal names a value only when the caller can read it.** The row's organization, its position and the sets that position carries, or its permission set are named only when the caller's own read returned that column at its stored value. Otherwise the sentence leaves the value out, for example "the assignment's position is not inside the organization boundary", and `details` leaves it out too. -- **Unchanged.** A row the caller cannot read, and an id that names no row, still answer the read door's `404 RECORD_NOT_FOUND`. An edit in place is refused as before. Inserts, tenant-level administrators and delegated administrators are judged as before. diff --git a/.changeset/password-reset-revocation-trail.md b/.changeset/password-reset-revocation-trail.md deleted file mode 100644 index 40cf9734fe4..00000000000 --- a/.changeset/password-reset-revocation-trail.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/platform-objects': patch ---- - -fix(plugin-auth): a session a password reset ends leaves a revocation trail - -Clause-②: no -Metadata-protocol: none - -- **A session a password reset ends leaves a revocation trail.** When a completed self-service password reset ends the account's sessions — the emailed link (`POST /api/v1/auth/reset-password`), and the phone OTP reset (`POST /api/v1/auth/phone-number/reset-password`); better-auth's email-OTP reset door, which the platform does not mount, is covered on its own path too — each `sys_session` row is now kept and stamped, `revoked_at` with `revoke_reason: password_reset`, the same kind of trail an interactive revoke leaves, instead of being deleted. The session is still refused (`401`). -- **The value says who ended the session.** `password_reset` is distinct from `user_revoked` (the owner revoking their own sessions) and from `admin_password_reset`, which an administrator's forced reset keeps writing. -- **`sys_session.revoke_reason`'s published vocabulary names `password_reset`** (`@objectstack/platform-objects`, the field's description and its translated help). -- **Unchanged.** Which sessions a reset ends; sign-out, which still deletes its row; the account's API keys, which a self-service reset does not revoke; and the audit log, which records no `logout` row for a reset. diff --git a/.changeset/per-record-action-missing-record-id.md b/.changeset/per-record-action-missing-record-id.md deleted file mode 100644 index ebe4f23af5e..00000000000 --- a/.changeset/per-record-action-missing-record-id.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): a per-record action called with no recordId is refused 400 naming it, and an unknown id 404s at the door, before its flow runs - -Clause-②: no — every call refused now already failed on `main`: the flow behind the action ran and answered `400 FLOW_FAILED` naming its internal node, with no id or with an id the caller cannot resolve. -Metadata-protocol: none - -An action declared `execution: 'perRecord'` (an api action or a flow action) called through the REST `/actions` door or MCP `run_action` with no `recordId`, or an empty one, is refused `400 VALIDATION_ERROR` naming `recordId`, and nothing is forwarded or started. The same api action called with an id the caller's scope does not resolve answers the door's own `404 RECORD_NOT_FOUND` instead of being forwarded, as a flow or script action already did. Actions declared `aggregate`, or declaring no `execution`, run record-less as before (#29173). Fixes #29881. diff --git a/.changeset/phone-reset-text-after-response.md b/.changeset/phone-reset-text-after-response.md deleted file mode 100644 index 04e5443a265..00000000000 --- a/.changeset/phone-reset-text-after-response.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): the phone-number password-reset request answers without waiting for the SMS send, as the emailed reset does - -Clause-②: no -Metadata-protocol: none - -- **The rule.** `POST /api/v1/auth/phone-number/request-password-reset` answers at once, the same way for any number: the reset text is built and sent after the response, as the emailed reset (`POST /api/v1/auth/request-password-reset`) already sends its mail. -- **A send that fails is logged on the server**, at error (`phone password-reset text was not delivered`), and never reaches the caller, who is answered `{ status: true }` either way, as before. The one-time code is in the text only, never in that line. -- **Unchanged.** The answer's status and body; the per-number cooldown and hourly cap and the per-IP limit on the route; the text itself; and a server-side `auth.api.requestPasswordResetPhoneNumber` call, which carries no request and still awaits its send. diff --git a/.changeset/rollback-addresses-served-row.md b/.changeset/rollback-addresses-served-row.md deleted file mode 100644 index 19de3041cf6..00000000000 --- a/.changeset/rollback-addresses-served-row.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -Clause-②: no -Metadata-protocol: none - -fix(metadata-protocol): the per-item rollback (`POST /api/v1/meta/:type/:name/rollback`) restores into the row the by-name read naming no package serves. A `.custom` pair's declared override stored beside a row bound to its read-only base (a row saved before the named-base close and not yet moved) is now rolled back in place, and the base row is left untouched; before, the door read a package-agnostic binding, addressed the base row, and was refused `403 NOT_OVERRIDABLE` naming a row the caller never meant. The door takes no new parameter. A rollback whose served row is the base's own is still refused by the named-base close. diff --git a/.changeset/sec-hardening-g-external-object-organization-scope.md b/.changeset/sec-hardening-g-external-object-organization-scope.md deleted file mode 100644 index cb3149414c5..00000000000 --- a/.changeset/sec-hardening-g-external-object-organization-scope.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/plugin-security': minor -'@objectstack/core': minor -'@objectstack/spec': minor ---- - -feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope - -Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. -Metadata-protocol: narrowed - - - -**BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. - -- **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. -- **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. -- **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. -- **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. -- **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. -- **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. - -Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. diff --git a/.changeset/sec-hardening-p3-write-refused-read-door.md b/.changeset/sec-hardening-p3-write-refused-read-door.md deleted file mode 100644 index 9c8362328cd..00000000000 --- a/.changeset/sec-hardening-p3-write-refused-read-door.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security)!: a write from a caller the read door refuses is refused, whatever rows it addresses - -Clause-②: no (narrowing) — a predicate update or delete (`multi: true`, addressed by a `where`) from a caller whose read of the object is refused is refused with that read refusal before the write runs, where it was admitted before; nothing is widened. -Metadata-protocol: none - - - -**BREAKING** for a caller whose read of an object is refused while its write is admitted, and that updates or deletes that object's records by predicate. With the permission model's own grants that caller exists in one shape only: an object whose `requiredPermissions` map gates `read` and not `update` or `delete` (ADR-0066 ⑤), and a caller holding the write grant without the capability the read requires. - -- **The rule.** A caller the read door refuses writes none of the object's records, by id or by predicate. The predicate update and delete such a caller addresses — a flow's `update_record` or `delete_record` node with `multi: true` run as that user, or an engine `update` / `delete` with `multi: true` under that caller's context — now answer the read door's own refusal (the `PERMISSION_DENIED` envelope that caller's read gets), and nothing is written. The by-id update and delete are refused too. -- **Unchanged.** A caller who reads the object keeps its predicate write, scoped to the records it reads. A caller the object's CRUD grants refuse is refused as before. A write the platform issues under the caller's context (a hook's or a cascade's own write, the referential clear of a deleted record's lookups) and a system write keep their answers. A store fault on the read propagates as before. -- **Who moves.** A deployment that relies on such a caller writing by predicate grants it the capability the object's read requires, or gates the write verbs on a capability that caller holds. diff --git a/.changeset/sec-hardening-s-external-scope-every-caller.md b/.changeset/sec-hardening-s-external-scope-every-caller.md deleted file mode 100644 index bde71022d0c..00000000000 --- a/.changeset/sec-hardening-s-external-scope-every-caller.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security)!: an external object that states no organization scope reads as empty for every caller, the platform owner included - -Clause-②: no (narrowing) — under the `isolated` and `group` postures, an external object registered after the boot that states no organization scope reads as empty for every caller, the declared platform owner included; nothing is widened, and the `single` posture is unchanged. -Metadata-protocol: none - - - -**BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) that registers an external object after the boot without stating its organization scope. - -- **The rule (ADR-0015 §10 as amended).** Whose rows an external object serves is a fact only its author can state. Under a walled posture an external object that states none is refused by name when the boot completes, and one registered after the boot — a Studio import, a runtime datasource — reads as empty for every caller, the platform owner included, and accepts no write, until it states its scope. That deny judges the object's configuration, not the reader's organization. -- **What is unchanged.** The declared platform owner's reads still cross the organization wall everywhere else: on local objects, on an external object that declares its own `organization_id`, and in a session with no active organization. Writes stay walled for everyone. A member's reads are unchanged. -- **Who moves, and the fix.** Declare one of the three on the external object, as the ADR-0087 entry `external-object-organization-scope-declared` prescribes: an `organization_id` field of its own (the rows are then walled by organization), `tenancy: { enabled: false }` (shared by every organization), or `systemFields: { tenant: false }` (the same opt-out on the system fields). diff --git a/.changeset/sec-hardening-u-write-implies-read.md b/.changeset/sec-hardening-u-write-implies-read.md deleted file mode 100644 index 16559a166f9..00000000000 --- a/.changeset/sec-hardening-u-write-implies-read.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-security': minor -'@object-ui/app-shell': minor ---- - -fix(spec)!: an object permission that edits or deletes a record also reads it - -Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. -Metadata-protocol: narrowed -Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. - - - -**BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). - -- **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. -- **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. -- **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. -- **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. -- **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. -- Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. - -**FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). diff --git a/.changeset/sign-up-mail-after-response.md b/.changeset/sign-up-mail-after-response.md deleted file mode 100644 index 1d4c47d6d49..00000000000 --- a/.changeset/sign-up-mail-after-response.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): a sign-up answers without waiting for any mail send, whether or not the address already has an account - -Clause-②: no -Metadata-protocol: none - -- **The rule.** `POST /api/v1/auth/sign-up/email` answers without waiting for any mail send, whether or not the address already has an account: the verification mail, and the notice to an address that already has an account, are built and sent after the response, as the password-reset mail already is. -- **A send that fails is logged on the server**, at error (`verification email was not delivered`; the notice keeps its own error line), and never reaches the caller, whose answer is the one a sent mail gets, as before. -- **Unchanged.** The answer's status and body; the sign-up rate limits; the mails themselves; a signed-in caller's `/send-verification-email` resend, which still awaits its send and still reports one that failed; and a server-side `auth.api.signUpEmail` call, which carries no request and still awaits its sends. diff --git a/.changeset/sys-file-uploader-is-its-creator.md b/.changeset/sys-file-uploader-is-its-creator.md deleted file mode 100644 index 8d90f85817f..00000000000 --- a/.changeset/sys-file-uploader-is-its-creator.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -fix(service-storage)!: a stored file's uploader is the user who created it, as an attachment's and a comment's author are - -Clause-②: no (narrowing) — a non-system create of `sys_file` records the session's user as `owner_id`, whatever the request carries, and one whose session names no signed-in user is refused. Nothing is widened. -Metadata-protocol: none - - - -**BREAKING** accept-set narrowing on `sys_file` creates (`@objectstack/service-storage`), served by every data door (REST, MCP, the console). - -- **Now.** A stored file's uploader is the user who created it, as an attachment's (`sys_attachment.uploaded_by`) and a comment's (`sys_comment.author_id`) author are. A non-system create of `sys_file` records the session's user as `owner_id`, over whatever the request carries. The column was already read-only on update, so the stamp is the row's uploader for life. -- **Refused.** A non-system create of `sys_file` whose session names no signed-in user is refused with 403 `RECORD_NOT_ACCESSIBLE`, the answer an attachment or comment create with no signed-in user already gets: there is no user to record as the uploader. -- **Unchanged.** The upload doors (presigned, chunked, complete) already record the signed-in user under the system context, and keep doing so. System-context writes (seeds, imports, migrations) keep the `owner_id` they supply, as does a programmatic create with no session at all on a bare kernel. An update of `owner_id` is dropped and reported in `droppedFields`, as before. Stored rows keep what they were written with. The console needs no change: it never creates `sys_file` rows through the data API. - -**The remedy.** None for the console or the upload doors. A client that creates a `sys_file` row through the data API gets the row recorded under its own user, whatever `owner_id` it sends; to record a different uploader, write from a system context (a migration or an import). A server-side caller that creates `sys_file` rows with no signed-in user runs as the system principal or names the user it acts for. diff --git a/.changeset/url-credential-special-scheme-read-however-many-slashes.md b/.changeset/url-credential-special-scheme-read-however-many-slashes.md deleted file mode 100644 index fb4a0dd1f53..00000000000 --- a/.changeset/url-credential-special-scheme-read-however-many-slashes.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme - -Clause-②: no (narrowing) -Metadata-protocol: narrowed - - - -**BREAKING** accept-set narrowing at the two metadata doors that judge a URL credential: an authored connector instance (`DeclarativeConnectorEntrySchema` — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load, `PUT /api/v1/meta/connector/:name`) and a datasource driver config's URL keys (`config.url` on postgres, mysql, mongo and turso, turso `config.syncUrl`) at every door that parses the datasource. - -- **A special scheme's authority is read as a URL parser reads it.** For `http`, `https`, `ws`, `wss` and `ftp`, in any letter case, the authority starts after the whole run of `/` and `\` that follows the scheme, however long, none included. A connector string holding a userinfo there, and a datasource URL holding a userinfo password there, are refused (`422 INVALID_METADATA` at the save door, located at the string, naming the parts, never the value); a bare username in a datasource URL stays accepted. -- **One grammar at both doors.** The connector entry and the datasource doors read the one userinfo grammar, and both read redactors rewrite exactly what the write refuses, at the position the write judged, serving every other character as stored. -- **Unchanged.** Every other scheme keeps `scheme://` or a scheme-relative `//` (`postgres:`, `libsql:`, `mongodb:` and the rest carry no authority without `//`); `file:` carries no userinfo; a URL spelled `scheme://` reads exactly as before. A stored row is never re-judged: a save of its served form keeps the stored value, and an edit of the served value stores exactly the edit. - -## FROM → TO - -```ts -// before — parsed green; now refused at providerConfig.baseUrl -{ name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', - providerConfig: { baseUrl: 'https:///billing:…@api.billing.example.com/v1' }, - auth: { type: 'none' } }, - -// after — the URL holds no credential, and the password is a reference -{ name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', - providerConfig: { baseUrl: 'https://api.billing.example.com/v1' }, - auth: { type: 'basic', username: 'billing', credentialRef: 'BILLING_API_PASSWORD' } }, -``` - -The same holds for `https:billing:…@host`, `https:/billing:…@host` and `https:\billing:…@host`, and for a datasource URL key: spell it `scheme://`, keep it credential-free (a bare username stays accepted) and bind the password through the Setup → Datasources connection form's secret field, or reference it with `external.credentialsRef`. There is deliberately no automatic rewrite: the value in the string may be the only copy of the secret. The upgrade guide entry is `url-credential-special-scheme-read-however-many-slashes`. diff --git a/.changeset/visitor-verification-resend.md b/.changeset/visitor-verification-resend.md deleted file mode 100644 index aa528a88e24..00000000000 --- a/.changeset/visitor-verification-resend.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -fix(plugin-auth): a verification-email resend from a visitor who is not signed in answers the same whatever the send does - -Clause-②: no -Metadata-protocol: none - -- **The rule.** `POST /api/v1/auth/send-verification-email` from a visitor who is not signed in answers the same whatever the send does. While the email transport is live it answers `{ "status": true }`: the verification mail is built and sent after the response, as the sign-up's verification mail is, and a send that fails is logged on the server at error (`verification email was not delivered`). -- **While the email transport is not live** (no email service, or one whose `isConfigured()` does not answer `true`, such as the `log` transport), every such resend is refused with `400 EMAIL_SERVICE_REQUIRED`, the code `/change-email` and the identity import already answer for a deployment with no email delivery. Its message names the fix (Settings → Mail), and the verify screens show it. -- **Unchanged.** A signed-in caller's resend for their own address, by session cookie or by bearer token, still awaits its send, still reports one that failed, and is not refused while the transport is not live. A server-side `auth.api.sendVerificationEmail` call, which carries no request, still awaits its send. diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index 7763f827c46..9b37c2ea1b6 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:18.3.0 + ghcr.io/objectstack-ai/objectstack:18.4.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:18.3.0 + ghcr.io/objectstack-ai/objectstack:18.4.0 ``` Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly @@ -150,7 +150,7 @@ COPY . . RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── -FROM ghcr.io/objectstack-ai/objectstack:18.3.0 +FROM ghcr.io/objectstack-ai/objectstack:18.4.0 COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json ``` @@ -173,7 +173,7 @@ FROM node:22-slim RUN npm install --prefix /tmp/os-installer-npm --no-save --no-audit --no-fund "npm@11.21.0" \ && export PATH="/tmp/os-installer-npm/node_modules/.bin:$PATH" \ && test "$(npm --version)" = "11.21.0" \ - && npm install -g @objectstack/cli@18.3.0 "pg@^8.0.0" "mysql2@^3.0.0" \ + && npm install -g @objectstack/cli@18.4.0 "pg@^8.0.0" "mysql2@^3.0.0" \ && npm cache clean --force \ && rm -rf /tmp/os-installer-npm diff --git a/content/docs/releases/index.mdx b/content/docs/releases/index.mdx index 89b1f826929..64a3754ed8c 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 -- [v18](/docs/releases/v18) — The line that carries ADR-0131, total organization ownership with no NULL `organization_id`; its breaking changes landed in stages through the `18.0.0-next.N` prereleases on npm's `next` tag, each with its own changeset and migration, and its GA release, 18.0.0, publishes to npm's `latest` tag and moves it from 17.x to 18 (current series: 18.3.0, released 2026-11-03). +- [v18](/docs/releases/v18) — The line that carries ADR-0131, total organization ownership with no NULL `organization_id`; its breaking changes landed in stages through the `18.0.0-next.N` prereleases on npm's `next` tag, each with its own changeset and migration, and its GA release, 18.0.0, publishes to npm's `latest` tag and moves it from 17.x to 18 (current series: 18.4.0, released 2026-11-05). - [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 (final release: 17.7.0). - [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). diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index 29277a34273..d563adce0e7 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:18.3.0 +image: ghcr.io/objectstack-ai/objectstack:18.4.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 657d155e0b2..82191670b77 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:18.3.0 +FROM ghcr.io/objectstack-ai/objectstack:18.4.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:18.3.0 + ghcr.io/objectstack-ai/objectstack:18.4.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:18.3.0 +FROM ghcr.io/objectstack-ai/objectstack:18.4.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=18.3.0 docker/ +docker build -t objectstack:dev --build-arg OS_CLI_VERSION=18.4.0 docker/ ``` diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index e8a917c6c1c..f69987a7a95 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/example-crm +## 4.0.104 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [30dcd8a] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-i18n@18.4.0 + ## 4.0.103 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index b31cd95a287..104fb33156b 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.103", + "version": "4.0.104", "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 cda9231041a..ffea6e01f7d 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/example-multi-package +## 0.0.11 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 0.0.10 ### Patch Changes diff --git a/examples/app-multi-package/package.json b/examples/app-multi-package/package.json index 66f414035f5..e1ed51b3c46 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.10", + "version": "0.0.11", "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 fa80fedd71a..84b9b8c3eef 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,51 @@ # @objectstack/example-showcase +## 0.3.26 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [afc761c] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [30dcd8a] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/cloud-connection@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/connector-mcp@18.4.0 + - @objectstack/organizations@18.4.0 + - @objectstack/connector-openapi@18.4.0 + - @objectstack/connector-rest@18.4.0 + - @objectstack/connector-slack@18.4.0 + - @objectstack/service-i18n@18.4.0 + - @objectstack/service-print@18.4.0 + ## 0.3.25 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 8b4f832edb1..46f7f9079f3 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.25", + "version": "0.3.26", "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 47eb04b53a8..9f5b52966bd 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,54 @@ # @objectstack/example-todo +## 4.0.104 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [a26abbc] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [7193160] +- Updated dependencies [30dcd8a] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/client@18.4.0 + - @objectstack/service-knowledge@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/knowledge-memory@18.4.0 + - @objectstack/service-i18n@18.4.0 + ## 4.0.103 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 94ed2b757cc..16873716ca1 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.103", + "version": "4.0.104", "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 06b68663fc1..7adc19b811b 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,45 @@ # @objectstack/example-embed-objectql +## 0.0.44 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [a26abbc] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/driver-memory@18.4.0 + ## 0.0.43 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index fc04acb5cad..1621b7cf54e 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.43", + "version": "0.0.44", "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 f2008aedd91..8a618097b37 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,16 @@ # @objectstack/hono +## 18.4.0 + +### Patch Changes + +- Updated dependencies [3b8c724] +- Updated dependencies [fc869c2] +- Updated dependencies [30dcd8a] + - @objectstack/runtime@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index e12fd130c18..d8bdc53a72b 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "type": "commonjs", "main": "dist/index.js", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index fecfa5bff7b..e0658cccd91 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/account +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index 3a363b5def7..e48c8da9558 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/apps/connector-docusign/CHANGELOG.md b/packages/apps/connector-docusign/CHANGELOG.md index 9163876d971..d7418cd976b 100644 --- a/packages/apps/connector-docusign/CHANGELOG.md +++ b/packages/apps/connector-docusign/CHANGELOG.md @@ -1,5 +1,36 @@ # @objectstack/connector-app-docusign +## 0.1.2 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/connector-openapi@18.4.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/apps/connector-docusign/package.json b/packages/apps/connector-docusign/package.json index 626277d28dd..1a429ff5b5c 100644 --- a/packages/apps/connector-docusign/package.json +++ b/packages/apps/connector-docusign/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-app-docusign", - "version": "0.1.1", + "version": "0.1.2", "description": "DocuSign eSignature connector app (open edition, sandbox first): DocuSign's eSignature OpenAPI document, trimmed offline to three envelope operations, as a declarative `openapi` connector instance authorized by OAuth 2.0 (`authorization_code`) with an OAuth client each deployment's administrator registers and enters on the connection.", "license": "Apache-2.0", "private": true, diff --git a/packages/apps/connector-github/CHANGELOG.md b/packages/apps/connector-github/CHANGELOG.md index 87af2898cc7..65faf501f7c 100644 --- a/packages/apps/connector-github/CHANGELOG.md +++ b/packages/apps/connector-github/CHANGELOG.md @@ -1,5 +1,36 @@ # @objectstack/connector-app-github +## 0.1.2 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/connector-mcp@18.4.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/apps/connector-github/package.json b/packages/apps/connector-github/package.json index 8c9508df3c6..632f2e8673c 100644 --- a/packages/apps/connector-github/package.json +++ b/packages/apps/connector-github/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-app-github", - "version": "0.1.1", + "version": "0.1.2", "description": "GitHub connector app (open edition): GitHub's remote MCP server as a declarative `mcp` connector instance with a read-tool allowlist, authenticated by a personal access token the administrator supplies through the credential store.", "license": "Apache-2.0", "private": true, diff --git a/packages/apps/connector-stripe/CHANGELOG.md b/packages/apps/connector-stripe/CHANGELOG.md index bba3e4beae9..55758a8bb47 100644 --- a/packages/apps/connector-stripe/CHANGELOG.md +++ b/packages/apps/connector-stripe/CHANGELOG.md @@ -1,5 +1,36 @@ # @objectstack/connector-app-stripe +## 0.1.2 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/connector-openapi@18.4.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/apps/connector-stripe/package.json b/packages/apps/connector-stripe/package.json index c90c88d1a02..9cc354d80e4 100644 --- a/packages/apps/connector-stripe/package.json +++ b/packages/apps/connector-stripe/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-app-stripe", - "version": "0.1.1", + "version": "0.1.2", "description": "Stripe connector app (open edition, read only): Stripe's OpenAPI document, trimmed offline to eight read operations, as a declarative `openapi` connector instance authenticated by a restricted secret key the administrator supplies through the credential store.", "license": "Apache-2.0", "private": true, diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index 437bb6f55f0..bf51e9f60d0 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/setup +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index 6e364861bf3..2d85d635397 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index 396f4a53e16..8b7694e490f 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/studio +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 619206e40c7..8f4d7fc1f39 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 4371e3f5e2d..3e11ef5c658 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,287 @@ # @objectstack/cli +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. +- 699ebd8: fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) + + Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. + + - **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. + - **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. + - **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. + - **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). + - **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. + + **FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. +- e3b56b2: feat(spec): a lookup or master-detail field's related-list title is translatable + + Clause-②: yes + Metadata-protocol: added + + A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. + + - `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. + - It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. + - `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. + - The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. + +### Patch Changes + +- 31fe289: feat(objectql,cli): `os migrate recompute-home-amounts` — recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/objectql`: `recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })`**, with `assertHomeAmountRecomputeSince` (the pure `since` check a door can run before it opens a database), `formatHomeAmountRecomputeReport`, `homeAmountRecomputeComplete` and `HOME_AMOUNT_SKIP_REASONS`. It walks every stored row of an object whose `record`-mode amounts declare `currencyConfig.homeAmount` and projects each through the write-time projection itself (the same rate row, division, single rounding, same-currency rule and organization-timezone day), in the row's own organization, so each organization converts into its own home currency at its own rates and is counted apart. A dry run (the default) reports what would change and writes nothing; `apply` writes only the rows that differ, through `engine.update` with the home field in the payload, so the value stored is the one the write itself computes, and only the home and rate fields move (`updated_at` is kept, so a consumer syncing by `updated_at` does not see the recompute). Being a write, it moves what reads the home field as a fresh write would: stored formulas, parent summaries, and the per-row audit rows where audit capture is installed. Rows that cannot be converted are listed by reason and never written: `rate_missing` (with the currency and date), `code_blank` (an amount with no currency code; the remedy is `os migrate record-currency-codes`), `document_date_blank`, `home_currency_unset` and `unconvertible`. `since` filters on the document date (`homeAmount.rateDate`). One run-level `sys_audit_log` entry records each applied run (counts per organization and reason, and the skipped rows), also when an unclassified error stops the run partway (marked `aborted`, then the error is thrown on). A walk cut short by `maxRecords` is `truncated` and not complete. Refused before any row is read: an unknown object (`OBJECT_NOT_FOUND`), an object or `field` declaring no home amount (`INVALID_FIELD`), a `since` that is not `YYYY-MM-DD` (`VALIDATION_FAILED`). + - **`@objectstack/objectql`: `ObjectQL.homeAmountReadsFor(context?)`**, the reads the write-time projection makes (its serving, and the organization a write converts a row under), for a caller that projects a stored row as a write would. + - **`@objectstack/cli`: `os migrate recompute-home-amounts --object [--field ] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`**, the shell of `os migrate record-currency-codes`, booting the settings service (each organization's `localization.currency` and `.timezone`) and the audit ledger. It exits 1 while any row is listed and skipped, on a dry run as on an apply, because a total over the home field silently leaves such a row out, and when `--max-records` cut the walk short. +- fbbbaee: fix(cli): `os package install` prints the connector maps the package export withheld (#29873) + + Clause-②: no + Metadata-protocol: none + + An exported package names each connector header or environment map the export withheld whole in its `withheldMaps`, and both install modes of the runtime's install-local endpoint name each map in their answer. The command dropped that list, so an operator who installed an export the documented way (`os package install DIR/NN-ID/objectstack.json`) was never told that the connector sends none of those headers or environment variables. + + The command now prints one warning, one line per map (`connector 'NAME' PATH`, for example `connector 'erp' providerConfig.headers`), and the remedy: re-enter each map where the connector is authored. It does this for a managed install (hot-loaded or not) and for an unmanaged one, from a catalog id or an artifact file. An answer without the list prints nothing new, and the exit code is unchanged. The command has no `--json` output. +- 8d5aab3: fix(cli): `os dev` and `os start` share one data-key file, so a secret saved under `npm run dev` reads back under `os start` + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/cli`.** Both commands open the same project database, but `os start` used to mint its own data key under its own home (`PROJECT/.objectstack/dev-crypto-key` in project mode) while `os dev` kept the key at `$HOME/.objectstack/dev-crypto-key` (or `$OS_HOME`'s). Every secret written during development then read back as unset under `os start`. Both commands now resolve the key file the way `os dev` always has (`os start --home` counts as `OS_HOME`) and hand it to their `serve` child on the internal `OS_INTERNAL_CRYPTO_KEY_FILE` channel. The project-local file an earlier `os start` minted is not orphaned: when it alone holds a key it is still read, and the boot says which file; when it and the shared file hold different keys, the shared one is used and a warning names both paths. An explicit `OS_SECRET_KEY` / `OS_DEV_CRYPTO_KEY` still wins, unchanged. + - **`@objectstack/service-settings`.** New exports `resolvePersistedKeyFile` (the one resolution both commands take, beside the provider's own key-file path) and `KEY_FILE_CHANNEL_ENV`; the local crypto provider's file tier reads the file that channel names, ahead of `OS_HOME`. +- cd5f554: fix(objectql,cli): `os migrate stored-expressions` fills a stored computed field on rows stored before it was declared, and the boot names the command when a sample of rows needs it + + Clause-②: yes (widening) + Metadata-protocol: none + + A stored computed field (a non-`formula` field declaring `expression`) is computed by the engine on every insert and update and on nothing else, so declaring one on an object that already holds rows left the new column NULL on every existing row until each was next written; a filter, sort, roll-up or analytics anchor over it read NULL where the expression gives a value, with no error. A fresh database was right and an upgraded one was not. + + - **`@objectstack/cli`: `os migrate stored-expressions [--object ]… [--field ] [--changed] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`.** A dry run (the default) reports, per field, the rows holding a NULL the expression gives a value; `--apply` writes them. Each value is the write's own computation over the stored row (option labels, the one reference hop, the `can()` map), and the apply writes through `engine.update` with `updated_at` kept, so hooks, roll-ups over the column and audit capture see an ordinary write. A NULL the expression itself answers (an empty reference, a null input) is left alone. A value that differs from the expression — the expression changed after the row was written — is counted on every run and rewritten only with `--changed`; an expression reading the clock always differs there. A row on which an expression does not evaluate while it holds a value is listed and not written, because the write would store NULL over it. The command exits 1 while a row is skipped or could not be written, or when `--max-records` cut the walk short. Idempotent: a re-run finds nothing to fill. + - **Boot advisory.** At `kernel:bootstrapped` the engine plugin reads the first 100 rows of each object declaring a stored computed field (nothing for an object declaring none) and, when that sample holds such a NULL, warns once naming each `object.field` and `os migrate stored-expressions`. It writes nothing and never fails a boot. + - **`@objectstack/objectql`:** `backfillStoredExpressions`, `formatStoredExpressionBackfillReport`, `storedExpressionBackfillComplete`, `announceStoredExpressionHoles`, `sampleStoredExpressionHoles`, and `ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)` — a stored computed field's values over rows already stored, computed as a write computes them. +- d9895a1: fix(cli): `os serve` reads `OS_TRUSTED_APP_SCHEMES` into AuthPlugin's `trustedAppSchemes`, so a deployment can register its mobile app for the sign-in hand-off + + Clause-②: no + Metadata-protocol: none + + - **Before.** `serve` constructed `AuthPlugin` with no `trustedAppSchemes`, and no setting a deployment could make reached the key. The `one-time-token` plugin never mounted and `GET /api/v1/auth/one-time-token/handoff` answered `400 … is not a registered app scheme` for every app, whatever the operator set. + - **Now.** `OS_TRUSTED_APP_SCHEMES` is read the way `OS_TRUSTED_ORIGINS` is: comma-separated, each entry trimmed, empty entries dropped, repeats folded. The list is passed as `trustedAppSchemes`. `os dev` and `os start` run `serve` as a child with the inherited environment, so they read it through the same line. + - **Unset stays unset.** No default scheme is registered: unset, empty or only separators, the plugin stays unmounted and the hand-off refuses every target, as before. + - **A bad entry still fails the boot.** Entries are not validated, lower-cased or dropped by the reader; plugin-auth's `assertTrustedAppSchemes` refuses a bad one at construction, naming it. + - Environment variable only: no stack key is added. The hosted runtime's own `AuthPlugin` is not changed. Listed in `content/docs/deployment/environment-variables.mdx` beside `OS_TRUSTED_ORIGINS`. +- f6f663a: fix(cli): when two data-key files hold different keys, `os start` and `os dev` read with the one that opens the project's stored secrets, or a production boot refuses + + Clause-②: yes (narrowing) — a production `os start` / `os serve` boot whose two candidate key files (the shared `$HOME/.objectstack/dev-crypto-key` or `$OS_HOME`'s, and the project's `.objectstack/dev-crypto-key`) both fail to open the database's newest stored secret now refuses to start, naming both files; it used to start and serve every stored secret as unset. The widening half: `@objectstack/service-settings` gains new exports (`chooseKeyFileByTrialOpen`, `settleKeyFileChannel`, `storedSecretReaderOverEngine`, `deferredLocalCryptoProvider`, `DataKeyOpensNothingError`, `KEY_FILE_CANDIDATE_CHANNEL_ENV` and their types). + Metadata-protocol: none + + + + **BREAKING** boot narrowing (`@objectstack/cli`, `@objectstack/service-settings`): a production boot whose candidate data keys open none of the stored secrets refuses to start. + + - **`@objectstack/cli`.** When the shared key file and the project's own key file (the one an earlier `os start` minted) hold different keys, the CLI no longer picks one. It hands the `serve` child both, and the child decides once the database is open: it opens the newest stored `sys_secret` value under each key and reads with the key that opens it, and the boot says which file it took. A project that only ever ran `os start`, on a machine whose home holds a key from `os dev` in another project, reads its secrets back again. With no stored secret yet, the shared file is used, as before. When neither key opens the stored secrets, a production boot (`os start`, `os serve`) refuses to start and names both files; a development boot (`os dev`) starts and prints a boot error naming both files. The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) now read the same file by the same rules: the project's own file when it alone holds a key, and the key that opens a stored secret when both do. + - **`@objectstack/service-settings`.** The settings service makes that choice in its `start()`, before any reader decrypts, when the CLI names a second candidate on the internal `OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE` channel. A leading `~` in `OS_HOME` (or `OBJECTSTACK_HOME`) is now the home directory when the provider finds its key file, as `os start` already read it; it used to name a directory called `~` under the working directory. + + **The remedy.** If a production boot refuses with "neither data key opens this database's stored secrets", find the key those secrets were sealed under (a backup of either file, or the `OS_SECRET_KEY` the deployment once ran with), keep a copy of both files, and put that key at the shared file the message names, or set `OS_SECRET_KEY` to it. + + Supersedes, in the same release: the note for #29934 says that when the shared file and the project's file hold different keys, the shared one is used and a warning names both paths. After this change the boot uses whichever of the two opens the stored secrets, and refuses in production when neither does. +- 159930a: fix(service-settings): when two data-key files hold different keys, the boot judges the newest stored secrets as a page, not only the newest one, and reports a database split between the two keys + + Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (`StoredSecretReader` now answers the page, `KeyFileChoice` gains the `mixed` reason and a `tally`, `DataKeyOpensNothingError` gains `judged`) were added by #30042, whose note is still pending, so they first ship together in this release; no released export changes. + Metadata-protocol: none + + - **`@objectstack/service-settings`.** When the shared key file and the project's own key file hold different keys, the boot now trial-opens each of the newest stored `sys_secret` rows (up to ten) under both keys and reads with the key that opens more of them, on a tie the one that opens the newest. A row that opens under neither key (a corrupt or foreign row) no longer decides the boot: if an older row opens under one key, that key is used, and only a page where no row opens under either key keeps the production refusal. When each key opens some of the rows, the database holds secrets under both keys: the boot uses the key that opens more of them and prints an error naming both files, how many rows each key opened, and the fix. The boot's notices now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets") instead of claiming the chosen file opens all of the database's stored secrets. + - **`@objectstack/cli`.** The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) share the same trial, so they make the same choice. + + **The remedy for a split database.** Keep a copy of both files, then save again each secret that reads as unset: it is sealed under the key the boot uses. Once nothing reads as unset, put that key at the shared file (or set `OS_SECRET_KEY` to it). + + Supersedes, in the same release: the note for #30042 says the boot opens the newest stored secret under each key and quotes the refusal as "neither data key opens this database's stored secrets". After this change the boot judges the newest rows as a page, and the refusal reads "neither data key opens any of the N newest stored secrets". +- afc761c: fix(cloud-connection,cli): an exported `.custom` installed on another environment lands in that environment's writable pair, and its overrides are served (ADR-0146 D2 / D3) + + Clause-②: no + Metadata-protocol: none + + - **Before.** `os package install /objectstack.json` of a base's paired customization package answered success and printed "Package installed into the running kernel", but the install registered the pair as code. None of its overrides was served, `GET /packages//customizations` listed none of them, and the pair turned read-only, so `ensure-customization` answered `writable: false` and every further save into it was refused `WRITABLE_PACKAGE_REQUIRED`. + - **Now.** `POST /api/v1/marketplace/install-local` installs a pair as what it is. The target's own pair is answered by its ensure door, which creates it when absent. Every item of the body is then written into it through the save door a Studio edit takes, with `mode: 'publish'` and the installer as actor, in load order. The overrides are served at once, the customization read lists them, and the pair stays editable. Nothing is registered as code or written to the cached-manifest directory. The answer carries `customizationPair` (`basePackageId`, `packageId`, `created`, `writable`), `writtenCount` and `written`, and the CLI prints them. + - **Refusals, nothing written.** A body key with no item form (code, a `translations` bundle, a key the install-key table does not classify), a nameless item, or two items claiming one name: `422`, every problem listed. A pair manifest the ensure door would not write: `400 PLUGIN_MANIFEST_INVALID`. A pair inside a multi-package artifact: `422`. A pair already read-only on the target: `409 RESOURCE_CONFLICT`, asked before the ensure door. If an earlier install registered it as code, the message names the uninstall and the restart that clear it. The ensure door's own refusal is relayed with its status and code. + - **A save refused part-way.** The answer is a failure with the save's code and status, naming the item and where it sits in the body. A pair this install created is deleted again. A pair that existed before is kept, and the answer lists the items written and the ones not written. + - **The CLI.** A runtime that predates this answers a pair as a managed install. The CLI now prints that as an error with the way back (uninstall, upgrade and restart, install again) and exits 1, instead of printing success. +- 959c26b: feat(cli,create-objectstack): a new project has a wired place for permission sets and datasets, `os g permission` / `os g dataset` write them, and the generated AGENTS.md names the package manager the project was created with + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`create-objectstack`: the blank starter ships `src/security/index.ts` and `src/datasets/index.ts`**, empty barrels already handed to the stack's `permissions` and `datasets` keys in `objectstack.config.ts`, like the eight directories it wired before. A permission set or a dataset exported from them is loaded and counted by `validate` with no edit to the config. The generated `AGENTS.md` lists both directories. + - **`@objectstack/cli`: `os g permission NAME [--object OBJECT]`** writes `src/security/NAME.permission.ts`, a `definePermissionSet` granting read, create and edit (no delete) on the object `--object` names, or the stack's only object, with `readScope: 'own'` declared. **`os g dataset NAME [--object OBJECT]`** writes `src/datasets/NAME.dataset.ts`, a `defineDataset` over that object with a `record_count` measure, grouped by the record's title field when it has one. `defineDataset` types the literal, so it needs no `as const`. Both pass `os validate`, `os build` and `os lint` with no finding on a fresh project. `os init` wires both directories too. + - **`create-objectstack`: the generated `AGENTS.md` (and `.github/copilot-instructions.md`) says `pnpm run validate` in a project created with pnpm**, and `npm run …` in one created with npm, the same package manager the install line and "Next steps" name. It used to say `npm run` in every project. +- fec1f58: fix(cli): a connection label printed by the CLI removes a password or a credential query pair wherever the URL rules read one + + Clause-②: no + Metadata-protocol: none + + - **One credential grammar.** The database label that `os dev`, `os start` and `os serve` print, and the migrate and resync confirms, now remove URL credentials through the same shared function the datasource read door applies (`redactUrlCredentials` from `@objectstack/spec/data`): the userinfo password, and every credential query pair judged by the shared query-pair judge and its one list of names. Wherever those rules read a credential, on every path the label takes, the label drops it. When the shared rules read more, the label follows with no edit of its own. + - **Unchanged.** Every value the shared rules read no credential in prints exactly as before: `postgres://`, `libsql://` and `file:` values, values with no userinfo, a username-only userinfo, and everything the URL parser already cleaned. +- Updated dependencies [31fe289] +- Updated dependencies [a35fc66] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [509c40a] +- Updated dependencies [88cd0ab] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [8d5aab3] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [f9e5bef] +- Updated dependencies [63b86b3] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [171b6dd] +- Updated dependencies [f6f663a] +- Updated dependencies [343ddb2] +- Updated dependencies [15f808a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [159930a] +- Updated dependencies [afc761c] +- Updated dependencies [04f36b6] +- Updated dependencies [959c26b] +- Updated dependencies [4be9734] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [977be38] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [8b117c0] +- Updated dependencies [474c030] +- Updated dependencies [85c7374] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/service-analytics@18.4.0 + - @objectstack/service-push@18.4.0 + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/verify@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/plugin-email@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/service-queue@18.4.0 + - @objectstack/service-settings@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/plugin-approvals@18.4.0 + - @objectstack/trigger-api@18.4.0 + - @objectstack/plugin-audit@18.4.0 + - @objectstack/cloud-connection@18.4.0 + - @objectstack/service-automation@18.4.0 + - create-objectstack@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/driver-turso@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/client@18.4.0 + - @objectstack/driver-mongodb@18.4.0 + - @objectstack/plugin-pinyin-search@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/plugin-webhooks@18.4.0 + - @objectstack/service-mailbox@18.4.0 + - @objectstack/service-package@18.4.0 + - @objectstack/trigger-record-change@18.4.0 + - @objectstack/account@18.4.0 + - @objectstack/setup@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/service-cache@18.4.0 + - @objectstack/service-job@18.4.0 + - @objectstack/service-realtime@18.4.0 + - @objectstack/service-sms@18.4.0 + - @objectstack/trigger-schedule@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/console@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index c7672bdcd98..c861b27a40a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "18.3.0", + "version": "18.4.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 7a6a02e1881..ad980734850 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/client-react +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/client@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 6c0ab23e330..2173fb15a58 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "type": "commonjs", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index bc1d9e013fc..44a196b6948 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/client +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index f94205c79af..c20032a23bb 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "type": "commonjs", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index 4dbf34b4d91..1f4cf083303 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,88 @@ # @objectstack/cloud-connection +## 18.4.0 + +### Minor Changes + +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. + +### Patch Changes + +- afc761c: fix(cloud-connection,cli): an exported `.custom` installed on another environment lands in that environment's writable pair, and its overrides are served (ADR-0146 D2 / D3) + + Clause-②: no + Metadata-protocol: none + + - **Before.** `os package install /objectstack.json` of a base's paired customization package answered success and printed "Package installed into the running kernel", but the install registered the pair as code. None of its overrides was served, `GET /packages//customizations` listed none of them, and the pair turned read-only, so `ensure-customization` answered `writable: false` and every further save into it was refused `WRITABLE_PACKAGE_REQUIRED`. + - **Now.** `POST /api/v1/marketplace/install-local` installs a pair as what it is. The target's own pair is answered by its ensure door, which creates it when absent. Every item of the body is then written into it through the save door a Studio edit takes, with `mode: 'publish'` and the installer as actor, in load order. The overrides are served at once, the customization read lists them, and the pair stays editable. Nothing is registered as code or written to the cached-manifest directory. The answer carries `customizationPair` (`basePackageId`, `packageId`, `created`, `writable`), `writtenCount` and `written`, and the CLI prints them. + - **Refusals, nothing written.** A body key with no item form (code, a `translations` bundle, a key the install-key table does not classify), a nameless item, or two items claiming one name: `422`, every problem listed. A pair manifest the ensure door would not write: `400 PLUGIN_MANIFEST_INVALID`. A pair inside a multi-package artifact: `422`. A pair already read-only on the target: `409 RESOURCE_CONFLICT`, asked before the ensure door. If an earlier install registered it as code, the message names the uninstall and the restart that clear it. The ensure door's own refusal is relayed with its status and code. + - **A save refused part-way.** The answer is a failure with the save's code and status, naming the item and where it sits in the body. A pair this install created is deleted again. A pair that existed before is kept, and the answer lists the items written and the ones not written. + - **The CLI.** A runtime that predates this answers a pair as a managed install. The CLI now prints that as an error with the way back (uninstall, upgrade and restart, install again) and exits 1, instead of printing success. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [88cd0ab] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [15f808a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [a26abbc] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [30dcd8a] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index a72a717f520..b7a3ae4eded 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "18.3.0", + "version": "18.4.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-imap/CHANGELOG.md b/packages/connectors/connector-imap/CHANGELOG.md index f9e76e626ec..e6549a12663 100644 --- a/packages/connectors/connector-imap/CHANGELOG.md +++ b/packages/connectors/connector-imap/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/connector-imap +## 18.4.0 + +No changes in this release. + ## 18.3.0 No changes in this release. diff --git a/packages/connectors/connector-imap/package.json b/packages/connectors/connector-imap/package.json index c196533b55e..097e28997a6 100644 --- a/packages/connectors/connector-imap/package.json +++ b/packages/connectors/connector-imap/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-imap", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "IMAP connector for ObjectStack — a read-only IMAP4rev1 session (EXAMINE, UID FETCH with BODY.PEEK) and the per-user-shaped mail account and credential abstraction the mailbox sync connects through.", "type": "commonjs", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index 04a8600ccf1..c4359bce508 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/connector-mcp +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index db7e951a8ed..21424f75b53 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index 24d2bcf0081..33ddd5605f3 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/connector-openapi +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 220c0cd0f1b..a6a18800861 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index c254670a065..bf36c6c7c64 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/connector-rest +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 3a248167c7f..a2276af6474 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index f815b45d8f3..3b563625423 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/connector-slack +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 446b73d7d93..53dbc9d932c 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 98b05c92792..c79644c8142 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/console +## 18.4.0 + +No changes in this release. + ## 18.3.0 No changes in this release. diff --git a/packages/console/package.json b/packages/console/package.json index 531e38480e1..df9738adc5a 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "18.3.0", + "version": "18.4.0", "description": "Prebuilt Console SPA built from this workspace's ui/apps/console (@object-ui/console) at the same version, installed as a dependency of @objectstack/cli.", "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 aec2cd718c7..ae85d766807 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,111 @@ # @objectstack/core +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- 21510bb: feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope + + Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: narrowed + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. + + - **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. + - **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. + - **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. + - **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. + - **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. + - **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. + + Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. + +### Patch Changes + +- 771b2db: fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. + - **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). + - **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. + - **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. + + Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. +- f19fbb8: fix(core): a hook that aborts with free text answers `HOOK_ABORTED` on the import row too, not `IMPORT_ROW_FAILED` + + Clause-②: no + Metadata-protocol: none + + - **Before.** A sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany`, `/api/v1/batch` and every `POST /api/v1/data/:object/batch` row with `HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/import` and the async import job answered the same abort as a row `{ ok: false, code: 'IMPORT_ROW_FAILED', error: }`, so an import client branching on the row's `code` could not tell a business rule's refusal from any other failed row. + - **Now.** The import row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read), judged by `resolveThrownHttpError`, the resolver that declares that code. The value moves, from `IMPORT_ROW_FAILED` to `HOOK_ABORTED`, on every row the core import runner builds from a write failure: the synchronous import, the async import job's stored results, the create and update halves of an upsert, and every write path (single-row, batched, partial-success). + - **Unchanged.** A code the hook declared still wins, registered or not. The row's `error` is still the hook's sentence. A body that crashed, a refusal that declared a 5xx status, a driver fault and every other row failure keep the code they had. The row's `code` is an open string in `ImportRowResultSchema`, so no schema moves, and no error code is added: `HOOK_ABORTED` is the one the ledger already registers. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 91b70fd4346..bdd248c36b1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "18.3.0", + "version": "18.4.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 c2c91f2f077..7296eb1b773 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,18 @@ # create-objectstack +## 18.4.0 + +### Patch Changes + +- 959c26b: feat(cli,create-objectstack): a new project has a wired place for permission sets and datasets, `os g permission` / `os g dataset` write them, and the generated AGENTS.md names the package manager the project was created with + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`create-objectstack`: the blank starter ships `src/security/index.ts` and `src/datasets/index.ts`**, empty barrels already handed to the stack's `permissions` and `datasets` keys in `objectstack.config.ts`, like the eight directories it wired before. A permission set or a dataset exported from them is loaded and counted by `validate` with no edit to the config. The generated `AGENTS.md` lists both directories. + - **`@objectstack/cli`: `os g permission NAME [--object OBJECT]`** writes `src/security/NAME.permission.ts`, a `definePermissionSet` granting read, create and edit (no delete) on the object `--object` names, or the stack's only object, with `readScope: 'own'` declared. **`os g dataset NAME [--object OBJECT]`** writes `src/datasets/NAME.dataset.ts`, a `defineDataset` over that object with a `record_count` measure, grouped by the record's title field when it has one. `defineDataset` types the literal, so it needs no `as const`. Both pass `os validate`, `os build` and `os lint` with no finding on a fresh project. `os init` wires both directories too. + - **`create-objectstack`: the generated `AGENTS.md` (and `.github/copilot-instructions.md`) says `pnpm run validate` in a project created with pnpm**, and `npm run …` in one created with npm, the same package manager the install line and "Next steps" name. It used to say `npm run` in every project. + ## 18.3.0 ### Patch Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 33302a659e5..b431b6d6aca 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "18.3.0", + "version": "18.4.0", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index e11f0a65c04..125b3997454 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -16,17 +16,17 @@ "test": "vitest run" }, "dependencies": { - "@objectstack/spec": "18.3.0", - "@objectstack/runtime": "18.3.0", - "@objectstack/plugin-hono-server": "18.3.0", - "@objectstack/connector-rest": "18.3.0", - "@objectstack/connector-openapi": "18.3.0", - "@objectstack/connector-mcp": "18.3.0" + "@objectstack/spec": "18.4.0", + "@objectstack/runtime": "18.4.0", + "@objectstack/plugin-hono-server": "18.4.0", + "@objectstack/connector-rest": "18.4.0", + "@objectstack/connector-openapi": "18.4.0", + "@objectstack/connector-mcp": "18.4.0" }, "devDependencies": { - "@objectstack/cli": "18.3.0", - "@objectstack/skills": "18.3.0", - "@objectstack/verify": "18.3.0", + "@objectstack/cli": "18.4.0", + "@objectstack/skills": "18.4.0", + "@objectstack/verify": "18.4.0", "typescript": "^5.3.0", "vitest": "~4.1.12" }, diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index 8216c873124..547052bc99c 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/driver-memory +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index a4b28cc9bbe..fd900841a09 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "type": "commonjs", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index 504b08b4e0f..251bad51fae 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/driver-mongodb +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index 942d4ec0518..715c45efac5 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "type": "commonjs", diff --git a/packages/drivers/driver-odata/CHANGELOG.md b/packages/drivers/driver-odata/CHANGELOG.md index cb0d48131f6..3822ca9bef7 100644 --- a/packages/drivers/driver-odata/CHANGELOG.md +++ b/packages/drivers/driver-odata/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/driver-odata +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/drivers/driver-odata/package.json b/packages/drivers/driver-odata/package.json index 1aa5b49b17f..aedbb5fb9a0 100644 --- a/packages/drivers/driver-odata/package.json +++ b/packages/drivers/driver-odata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-odata", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "OData v4 and v2 datasource driver for ObjectStack — serve an OData service's entity sets as external objects through a plugin driver", "keywords": [ diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index 90c6b0e841e..9ead02d45e7 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,50 @@ # @objectstack/driver-sql +## 18.4.0 + +### Patch Changes + +- dcb5fb3: fix(driver-turso): the remote face joins a large `$and` / `$or` as balanced halves, so comment and attachment reads fit a hosted libSQL server's depth limit + + Clause-②: no + Metadata-protocol: none + + On a remote Turso / libSQL database (`TursoDriver` with a `libsql://`, `https://` or `wss://` url and no `syncUrl`, as every hosted environment runs), every read of `sys_comment` and `sys_attachment` answered `500 DATABASE_ERROR` once the deployment registered roughly a hundred objects, as an installed app does. The driver's log named the cause: `Expression tree is too large (maximum depth 100)`. The comment and attachment read gates OR one `ReadableIds` branch per registered parent object, and the remote face's compiler joined a group's members into one flat chain, one expression level per member, while the hosted server refuses a tree deeper than 100. + + The local face (`SqlDriver`) has emitted such groups as balanced halves since #29888. The remote face compiles its SQL text itself, so that change never reached it. It now regroups by the same rule, shared from `@objectstack/driver-sql` as `joinBalancedGroup` (with `FLAT_GROUP_MAX_MEMBERS`): a group of at most 8 members compiles byte-identical to before, and a larger one as two parenthesised halves, recursively, so the depth it adds grows with the logarithm of its size. Measured on the gate's own shape with 134 branches and a three-level master chain: 162 levels before, 38 after. + + The regrouped expression selects exactly the rows the flat one did, on every dialect: `AND` and `OR` are associative over SQL's three-valued logic, the members keep their order, and so do their bind values. No gate, scope or tenant wall changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index a1571708b2b..ef5893b06dd 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "type": "commonjs", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index 17930be629d..5a0d3912a9c 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/driver-sqlite-wasm +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/driver-sql@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index bc330f1a79c..f9687de23d6 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": "18.3.0", + "version": "18.4.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 1287bb124f7..4bc076761ed 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,50 @@ # @objectstack/driver-turso +## 18.4.0 + +### Patch Changes + +- dcb5fb3: fix(driver-turso): the remote face joins a large `$and` / `$or` as balanced halves, so comment and attachment reads fit a hosted libSQL server's depth limit + + Clause-②: no + Metadata-protocol: none + + On a remote Turso / libSQL database (`TursoDriver` with a `libsql://`, `https://` or `wss://` url and no `syncUrl`, as every hosted environment runs), every read of `sys_comment` and `sys_attachment` answered `500 DATABASE_ERROR` once the deployment registered roughly a hundred objects, as an installed app does. The driver's log named the cause: `Expression tree is too large (maximum depth 100)`. The comment and attachment read gates OR one `ReadableIds` branch per registered parent object, and the remote face's compiler joined a group's members into one flat chain, one expression level per member, while the hosted server refuses a tree deeper than 100. + + The local face (`SqlDriver`) has emitted such groups as balanced halves since #29888. The remote face compiles its SQL text itself, so that change never reached it. It now regroups by the same rule, shared from `@objectstack/driver-sql` as `joinBalancedGroup` (with `FLAT_GROUP_MAX_MEMBERS`): a group of at most 8 members compiles byte-identical to before, and a larger one as two parenthesised halves, recursively, so the depth it adds grows with the logarithm of its size. Measured on the gate's own shape with 134 branches and a three-level master chain: 162 levels before, 38 after. + + The regrouped expression selects exactly the rows the flat one did, on every dialect: `AND` and `OR` are associative over SQL's three-valued logic, the members keep their order, and so do their bind values. No gate, scope or tenant wall changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/driver-sql@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index 8d9e91bc103..396a8e2e58f 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "18.3.0", + "version": "18.4.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 6e7fba5f3e9..e8369a12b9d 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,110 @@ # @objectstack/formula +## 18.4.0 + +### Minor Changes + +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). + +### Patch Changes + +- 0f3ee58: fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: four codes join `ExpressionRefusalCode`**: `date-arithmetic-run-fails`, `date-arithmetic-run-may-fail`, `date-text-arithmetic-run-fails` and `date-text-arithmetic-run-may-fail`, with the same params as `date-arithmetic` / `date-text-arithmetic`. A flow condition (a `'flattened'` site) that does arithmetic on a date field used to be told the field "silently evaluates to null", but the engine's `evaluateCondition` throws on the fault on every flow, so the run fails at the condition. Where the caller declares the run proven to hold the field (`faultOutcome: 'run-fails'`, with `bareNamesUnproven` unset) the verdict is served under `-run-fails` and says the run fails; anywhere else in a flow it is served under `-run-may-fail` and says the run fails wherever the name holds the field's date when the condition runs (a flow started by an API call, an action or a parent flow binds what its entrance hands over, which can be a number). The grade is unchanged: an error at every site. A `record`-scoped site (a formula field, a validation rule, a display predicate) keeps `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a flow condition re-keys to the new ones. + - **`@objectstack/lint`**: the fault-outcome ledger's docblock names the date verdicts' flow codes; the routing is the existing `faultOutcome` / `bareNamesUnproven` hint, unchanged. +- 29cbfcc: fix(lint): a flow condition's bare-name type verdict no longer refuses a name a `screen` field holds, and stands down to the warning after a node that can bind any name (#30025) + + Clause-②: yes (widening) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a `type-mismatch-run-fails` on a BARE condition name that a `screen` node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with no input contract), which is now the `type-mismatch-run-may-fail` warning. A `record.` / `previous.` read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional `bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`, and `collectScreenFieldNames` in `@objectstack/lint`'s flow-variable-scope module. + Metadata-protocol: none + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the screen field and the open binder); `skills/**` and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves. + + - **Before.** A record-triggered flow whose `screen` node collects `title` (a number) and whose edge after it reads `title > 5`, with `title` a text field on the trigger object, was refused `type-mismatch-run-fails`: the bare name was typed as the field. The run binds the collected number there (a resume lands a screen's values under their plain names), so the condition runs. The same refusal hit a bare name after a `script`, `connector_action`, `wait`, `subflow` or `map` node, or a screen with no input contract, any of which can bind the name to a value nothing can see at build. + - **Now.** A screen's declared field names join the names a bare read holds instead of the field, so they are not typed as it (the null-guard verdict leaves them out too). In a flow with a node that can bind any name, a bare-name mismatch is the `type-mismatch-run-may-fail` warning, as the null-guard verdict already stands its bare names down there; `record.` is still refused. The stand-down reads the flow's nodes only: an entrance (and the publish door's per-write snapshot, which sees none) changes which record both the bare name and `record.` read, not whether a node rebinds the bare name, so a bare-name refusal on a record-triggered flow with no such node is unchanged at every door. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index 6f68ab97eaa..fb019306696 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "type": "commonjs", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index d52564a713a..313f7687cb3 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,168 @@ # @objectstack/lint +## 18.4.0 + +### Minor Changes + +- 46d2b65: fix(lint)!: a report whose `runtimeFilter` names a field its dataset's object lacks is refused at publish (`report-filter-field-unknown`), as the dashboard twin already is (#29900) + + Clause-②: yes (narrowing) — the report publish door, `os validate` and `os build` refuse a report `runtimeFilter` key they accepted before; `@objectstack/lint` also exports `validateReportRuntimeFilters`, `REPORT_FILTER_FIELD_UNKNOWN` and the `ReportRuntimeFilterFinding` type. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` and `content/docs/**` teach `runtimeFilter` keys that are fields of the dataset's object, which this rule accepts; no page teaches a key the object lacks. The reasoning ships as the `os explain report-filter-field-unknown` entry. + + + + **BREAKING** accept-set narrowing at the report publish door (`PUT /api/v1/meta/report/NAME`, `POST /api/v1/meta/report/NAME/publish`, package draft publish; Studio, REST and MCP authors) and in `os validate` / `os build`. + + - **Before.** A report with `runtimeFilter: { no_such_field: 'x' }` over a dataset whose object has no `no_such_field` published `200`, and every reader of the report saw a "Dataset query failed: 400 Bad Request — Filter member no_such_field …" panel instead of numbers. The same key on a dashboard widget's `filter` was already refused `422` (`widget-filter-field-unknown`). + - **Now.** It is refused `422 INVALID_METADATA` with `report-filter-field-unknown` at `reports[0].runtimeFilter.no_such_field`, and the verdict names the object and the nearest field ("Did you mean …"). A `joined` report's block filter is judged against the block's dataset, and the report-level `runtimeFilter` (merged into every block's query) against each block's dataset. + - **Accepted, as on a widget.** A field of the dataset's object, a registry-injected system column (`created_at`, `owner_id` where injected), a dotted relationship path, and every `$` operator and combinator. One more key is accepted on a report: a bare key naming a dimension of the bound dataset, which the dataset query binds through that dimension. + - **Not judged.** A `dataset` that resolves to nothing (`chart-dataset-unknown` reports it once), an object the stack (or the publish snapshot) does not carry, an object with no readable field map, and a dotted key whose prefix the dataset's `include` does not declare. + - **`os explain report-filter-field-unknown`** prints the reasoning; `RULE_EXPLANATIONS` gains the entry. + + **What an author does.** Nothing for a report whose filter keys name real fields or dimensions. A refused key never served the report (every reader saw the error panel), so there is no rewrite that keeps what the author meant: name a field of the dataset's object, a dimension of the dataset, or a `relationship.field` path whose prefix the dataset's `include` declares. The refusal names the key and the nearest field. +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). +- 3a27208: fix(lint)!: a stored sort naming a field that declares `sortable: false` is refused by `os validate` and at publish, judged by the same sortability projection the Data API list door refuses it by — on a list view and on every stored page sort (#30023) + + Clause-②: yes (narrowing) — `sort-field-unsortable` (error) now also fires on a sort naming a field whose author declared `sortable: false`, on the list-view rungs it already walked and on every stored page sort it did not walk before (a block's own `sort`, every element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort`, an app page's `record.sort`, where a `formula` sort is now refused too), so `os validate` / `os build` / `os lint` and the `page` and `view` publish doors refuse metadata they used to accept. The widening half: `@objectstack/lint` exports `indexObjectSortability` and the `SortabilityFields` type, and `checkSortDeclaration` takes an optional trailing `sortability` index. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key and no page or view `sort` rule; `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field". The prescription ships in the finding's fix line, `os explain sort-field-unsortable` and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at authoring (`@objectstack/lint`): a stored sort the Data API list door refuses is refused before it is stored. + + - **Before.** A list view whose `sort` named a field marked `sortable: false` passed `os validate` and the `PUT /api/v1/meta/view` door, then answered `400 INVALID_SORT` on its first fetch and on every fetch after it. No page sort was judged for sortability at all: a `record:related_list`, `record:line_items`, `list-view` or `object-*` block sorting by such a field (or by a `formula` field), an element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort` and an app page's `record.sort` all saved green and failed at read. + - **Now.** `validateSortableFields` judges each of those positions by `resolveObjectSortability`, the projection the list door refuses by, and reports `sort-field-unsortable` at `error` with the door's own clause ("a field on object X that declares `sortable: false`") at the slot's path. The reference-integrity suite now runs on a `page` write for this one member, so the Studio, REST `/meta` and MCP page doors give the same refusal (422). Only the literal `false` counts: a field with no `sortable` key, or `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its virtual-field verdict. Existence on page sorts stays `page-field-unknown`. + - **Not changed.** A stored view or page already carrying such a sort keeps loading and serving; only its next save is judged, and its first fetch still answers 400 as it did since the list door started refusing it. + + **The remedy.** Sort by another field, or remove `sortable: false` from the field to make it sortable. + + **Supersedes, in the same release.** The `@objectstack/spec` / `@objectstack/metadata-protocol` / `@objectstack/mcp` note for #29896 says "Lint does not yet judge these at authoring." From this release it does: `os validate` and the publish doors refuse every stored sort that note lists, and the stored page sorts it did not list. + +### Patch Changes + +- f9e5bef: feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. + - **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. + - **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. +- 0f3ee58: fix(formula,lint): a date-arithmetic verdict in a flow condition says the run fails, not that the value reads null + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/formula`: four codes join `ExpressionRefusalCode`**: `date-arithmetic-run-fails`, `date-arithmetic-run-may-fail`, `date-text-arithmetic-run-fails` and `date-text-arithmetic-run-may-fail`, with the same params as `date-arithmetic` / `date-text-arithmetic`. A flow condition (a `'flattened'` site) that does arithmetic on a date field used to be told the field "silently evaluates to null", but the engine's `evaluateCondition` throws on the fault on every flow, so the run fails at the condition. Where the caller declares the run proven to hold the field (`faultOutcome: 'run-fails'`, with `bareNamesUnproven` unset) the verdict is served under `-run-fails` and says the run fails; anywhere else in a flow it is served under `-run-may-fail` and says the run fails wherever the name holds the field's date when the condition runs (a flow started by an API call, an action or a parent flow binds what its entrance hands over, which can be a number). The grade is unchanged: an error at every site. A `record`-scoped site (a formula field, a validation rule, a display predicate) keeps `date-arithmetic` / `date-text-arithmetic` and their sentence. A consumer keyed to the old codes on a flow condition re-keys to the new ones. + - **`@objectstack/lint`**: the fault-outcome ledger's docblock names the date verdicts' flow codes; the routing is the existing `faultOutcome` / `bareNamesUnproven` hint, unchanged. +- 29cbfcc: fix(lint): a flow condition's bare-name type verdict no longer refuses a name a `screen` field holds, and stands down to the warning after a node that can bind any name (#30025) + + Clause-②: yes (widening) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) no longer refuses, on a record-triggered flow (or a time-relative sweep of its own object), a `type-mismatch-run-fails` on a BARE condition name that a `screen` node's declared field holds (no verdict now), nor on a bare name in a flow with a node that can bind a name no reader sees (`script`, `connector_action`, `wait`, `subflow`, `map`, a screen with no input contract), which is now the `type-mismatch-run-may-fail` warning. A `record.` / `previous.` read keeps its refusal, and nothing new is refused. The null-guard verdict's bare names also leave a screen's field names out. Added beside them: the optional `bareNamesUnproven` key of `@objectstack/formula`'s `ExprSchemaHint`, and `collectScreenFieldNames` in `@objectstack/lint`'s flow-variable-scope module. + Metadata-protocol: none + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the screen field and the open binder); `skills/**` and the MCP skill text teach no bare flow-condition name a screen or an open binder holds, so no skill or eval moves. + + - **Before.** A record-triggered flow whose `screen` node collects `title` (a number) and whose edge after it reads `title > 5`, with `title` a text field on the trigger object, was refused `type-mismatch-run-fails`: the bare name was typed as the field. The run binds the collected number there (a resume lands a screen's values under their plain names), so the condition runs. The same refusal hit a bare name after a `script`, `connector_action`, `wait`, `subflow` or `map` node, or a screen with no input contract, any of which can bind the name to a value nothing can see at build. + - **Now.** A screen's declared field names join the names a bare read holds instead of the field, so they are not typed as it (the null-guard verdict leaves them out too). In a flow with a node that can bind any name, a bare-name mismatch is the `type-mismatch-run-may-fail` warning, as the null-guard verdict already stands its bare names down there; `record.` is still refused. The stand-down reads the flow's nodes only: an entrance (and the publish door's per-write snapshot, which sees none) changes which record both the bare name and `record.` read, not whether a node rebinds the bare name, so a bare-name refusal on a record-triggered flow with no such node is unchanged at every door. +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/sdui-parser@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 050ed2726ee..fdbb9fc245e 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "18.3.0", + "version": "18.4.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 ccee68b11c9..8522df25b37 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,132 @@ # @objectstack/plugin-mcp-server +## 18.4.0 + +### Minor Changes + +- 140d8ce: fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) + + Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. + + - **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. + - **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. + - **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. + - **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. + + **The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). + +### Patch Changes + +- 614e831: feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card + + Clause-②: no + Metadata-protocol: none + + The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: + + - **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. + - **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. + - **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. + + Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. + + The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index f97b769afb1..ccc5e2c654b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "18.3.0", + "version": "18.4.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 1ea659eb4d4..1df4f175526 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/metadata-core +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 8fa054750a1..56ab5aa5358 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "18.3.0", + "version": "18.4.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 d80fb2f7263..a0b495de784 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,11 @@ # @objectstack/metadata-fs +## 18.4.0 + +### Patch Changes + +- @objectstack/metadata-core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index e75514451b0..b49e4f6df78 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "18.3.0", + "version": "18.4.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 a97b45d9c45..007126a3f45 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,173 @@ # @objectstack/metadata-protocol +## 18.4.0 + +### Minor Changes + +- 140d8ce: fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) + + Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. + + - **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. + - **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. + - **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. + - **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. + + **The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. +- 699ebd8: fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) + + Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. + + - **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. + - **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. + - **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. + - **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). + - **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. + + **FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. +- e3b56b2: feat(spec): a lookup or master-detail field's related-list title is translatable + + Clause-②: yes + Metadata-protocol: added + + A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. + + - `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. + - It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. + - `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. + - The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. +- 15f808a: fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062) + + Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer. + Metadata-protocol: semantic + + - **A mapping override in `.custom` is served.** A mapping item that declares `overrides: { package: '', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping. + - **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=`) is unchanged: the pair's row stays and its override stays served. + - **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged. + - **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag. + + The label precedence of a translated override (card item 3) is not in this change. +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. + +### Patch Changes + +- 3b8c724: fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not + + Clause-②: yes (widening) + Metadata-protocol: none + + A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. + + - **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. + - **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. + + `SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. + + Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. +- 88cd0ab: fix(metadata-protocol): a hook that aborts with free text answers `HOOK_ABORTED` on the per-object batch row too, not `VALIDATION_ERROR` + + Clause-②: no + Metadata-protocol: none + + - **Before.** Since #27597 (and #27672) a sandboxed hook body that aborts with free text (`throw new Error('…')`, no `code`) answers `/api/v1/data/:object`, `/createMany` and `/api/v1/batch` with `400 HOOK_ABORTED` and the hook's sentence. `POST /api/v1/data/:object/batch` answered the same abort as a row `{ code: 'VALIDATION_ERROR', message: }`: the row mapper (`toRowApiError`) derived the code from the `400` the doors serve, so a bulk client branching on the code read a business refusal as malformed input. + - **Now.** The row answers `HOOK_ABORTED` (the `UNDECLARED_HOOK_REFUSAL_CODE` the other doors read). The value moves, from `VALIDATION_ERROR` to `HOOK_ABORTED`, on every row that shares the mapper: `POST /api/v1/data/:object/batch` with `operation` `create`, `update`, `upsert` or `delete`, in every mode (plain, `atomic`, `continueOnError`), and the per-row results of `updateManyData` / `deleteManyData`. + - **Unchanged.** A code the hook declared still wins (a registered code is its own row code; an unregistered one still gets the code its status derives). The row's `message` is still the hook's sentence and `httpStatus` is still absent when the body declared no status. A crashed body and a non-sandbox producer answer as before. No error code is added; `HOOK_ABORTED` is the one #27597 registered. +- 5f47410: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension + + Clause-②: yes (widening) + Metadata-protocol: none + + After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages//export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged. + + - **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension. + - **The pair's export** is unchanged: it carries the extension in `objectExtensions`. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in. + + An extension contributed by any other installed package is still folded into the base's object in the base's export, as before. + + `SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it. +- 07685ae: fix(metadata-protocol): `os migrate packageless-overlays --apply` heals the registry after it deletes the unbound row, as the delete door does + + Clause-②: no + Metadata-protocol: none + + - **Now.** After the ceremony saves a row into its base's `.custom` pair and reads it back, it deletes the unbound row through the protocol's one repository and then runs the delete door's registry heal for an active row. The unbound row's registration leaves with the row, and the pair's row is registered again, so the process serves what a restart serves. Before, the delete went through a second repository and no heal followed, so the unbound row stayed registered beside the base's entry until the next restart. The five reads of the item already served the pair's row, so no read answered differently. + - **Unchanged.** A draft row's removal heals nothing, as a draft discard does not. Outcomes, receipts and the rows written are the same. +- 713beba: Clause-②: no + Metadata-protocol: none + + fix(metadata-protocol): the per-item rollback (`POST /api/v1/meta/:type/:name/rollback`) restores into the row the by-name read naming no package serves. A `.custom` pair's declared override stored beside a row bound to its read-only base (a row saved before the named-base close and not yet moved) is now rolled back in place, and the base row is left untouched; before, the door read a package-agnostic binding, addressed the base row, and was refused `403 NOT_OVERRIDABLE` naming a row the caller never meant. The door takes no new parameter. A rollback whose served row is the base's own is still refused by the named-base close. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f9e5bef] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/sdui-parser@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 771c5cd2397..7429543234f 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "18.3.0", + "version": "18.4.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 730038672e5..8b0af8a248e 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/metadata +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/metadata-fs@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index fc9c5fb3085..550e996186b 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "18.3.0", + "version": "18.4.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 fba22b42ffd..f327b35fc8a 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,136 @@ # @objectstack/objectql +## 18.4.0 + +### Patch Changes + +- 31fe289: feat(objectql,cli): `os migrate recompute-home-amounts` — recompute stored home-currency amounts at the rates in force now, report first, rows without a rate listed and skipped + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/objectql`: `recomputeHomeAmounts(engine, logger, { object, field?, since?, apply?, maxRecords?, maxListed?, source? })`**, with `assertHomeAmountRecomputeSince` (the pure `since` check a door can run before it opens a database), `formatHomeAmountRecomputeReport`, `homeAmountRecomputeComplete` and `HOME_AMOUNT_SKIP_REASONS`. It walks every stored row of an object whose `record`-mode amounts declare `currencyConfig.homeAmount` and projects each through the write-time projection itself (the same rate row, division, single rounding, same-currency rule and organization-timezone day), in the row's own organization, so each organization converts into its own home currency at its own rates and is counted apart. A dry run (the default) reports what would change and writes nothing; `apply` writes only the rows that differ, through `engine.update` with the home field in the payload, so the value stored is the one the write itself computes, and only the home and rate fields move (`updated_at` is kept, so a consumer syncing by `updated_at` does not see the recompute). Being a write, it moves what reads the home field as a fresh write would: stored formulas, parent summaries, and the per-row audit rows where audit capture is installed. Rows that cannot be converted are listed by reason and never written: `rate_missing` (with the currency and date), `code_blank` (an amount with no currency code; the remedy is `os migrate record-currency-codes`), `document_date_blank`, `home_currency_unset` and `unconvertible`. `since` filters on the document date (`homeAmount.rateDate`). One run-level `sys_audit_log` entry records each applied run (counts per organization and reason, and the skipped rows), also when an unclassified error stops the run partway (marked `aborted`, then the error is thrown on). A walk cut short by `maxRecords` is `truncated` and not complete. Refused before any row is read: an unknown object (`OBJECT_NOT_FOUND`), an object or `field` declaring no home amount (`INVALID_FIELD`), a `since` that is not `YYYY-MM-DD` (`VALIDATION_FAILED`). + - **`@objectstack/objectql`: `ObjectQL.homeAmountReadsFor(context?)`**, the reads the write-time projection makes (its serving, and the organization a write converts a row under), for a caller that projects a stored row as a write would. + - **`@objectstack/cli`: `os migrate recompute-home-amounts --object [--field ] [--since YYYY-MM-DD] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`**, the shell of `os migrate record-currency-codes`, booting the settings service (each organization's `localization.currency` and `.timezone`) and the audit ledger. It exits 1 while any row is listed and skipped, on a dry run as on an apply, because a total over the home field silently leaves such a row out, and when `--max-records` cut the walk short. +- 3b8c724: fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not + + Clause-②: yes (widening) + Metadata-protocol: none + + A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. + + - **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. + - **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. + + `SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. + + Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. +- 5f47410: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(objectql, metadata-protocol): a base package's own export carries its objects without its `.custom` pair's extension + + Clause-②: yes (widening) + Metadata-protocol: none + + After an administrator added a field to a vendor package's object through Studio's Customize (an extension published in `.custom`, for example `region__c`, or a relabelled base field), `GET /api/v1/packages//export` carried that field and label inside the base's object, as if the vendor had shipped them. The pair's export carries the same extension as its `objectExtensions` entry, so installing the two exports elsewhere declared the customer's field twice, once as the vendor's. ADR-0146 D8 item 3 says the base's export carries the base unchanged. + + - **The base's export** (and its entry in `GET /api/v1/packages/export`) now carries each object as the base declares it, with no field, label or other patch from its pair's extension. + - **The pair's export** is unchanged: it carries the extension in `objectExtensions`. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with the pair's field and labels folded in. + + An extension contributed by any other installed package is still folded into the base's object in the base's export, as before. + + `SchemaRegistry.listItems` takes a `withoutExtensionsOf` option beside `includeDisabledPackages`, and `getAllObjects` and `foldObjectExtendersOnto` take the same option: each object is resolved with that package's `extend` contributions left out of the fold. Only the metadata protocol's per-package layer (the package export) passes it. +- 314ad95: fix(objectql): the lifecycle sweep no longer runs 60 s after a boot that created its datastore from empty; its first sweep runs one interval later + + Clause-②: yes (widening) — the exported `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean` (an engine without it keeps the old schedule); no accept set changes. + Metadata-protocol: none + + - `LifecycleService.start()` asks the engine, at the initial delay, whether this process created the datastore from empty (`ObjectQL.wasDatastoreCreatedFromEmpty()`, answered from every driver's schema-sync stats without a statement). When it did, the scheduled first sweep is skipped and the sweep runs on the interval, one interval later. Such a store holds no row older than the boot, and no declared window is shorter than an hour, so the skipped sweep had nothing to reap. It still read every declared object, the `lifecycle` settings and every lookup the reference audit walks: 130 statements in the first minute of a hosted tenant kernel's first boot. + - A store that existed before the boot, or an engine that cannot say, sweeps at the initial delay as before. A direct `sweep()` call always sweeps. `OS_LIFECYCLE_DISABLED`, `initialDelayMs` and `sweepIntervalMs` keep their meaning. + - `LifecycleEngineLike` gains the optional member `wasDatastoreCreatedFromEmpty?(): boolean`. An engine double that omits it keeps the old schedule. + - A re-boot on an existing store still sweeps at the initial delay. Persisting a last-swept watermark for that case is separate work. +- cd5f554: fix(objectql,cli): `os migrate stored-expressions` fills a stored computed field on rows stored before it was declared, and the boot names the command when a sample of rows needs it + + Clause-②: yes (widening) + Metadata-protocol: none + + A stored computed field (a non-`formula` field declaring `expression`) is computed by the engine on every insert and update and on nothing else, so declaring one on an object that already holds rows left the new column NULL on every existing row until each was next written; a filter, sort, roll-up or analytics anchor over it read NULL where the expression gives a value, with no error. A fresh database was right and an upgraded one was not. + + - **`@objectstack/cli`: `os migrate stored-expressions [--object ]… [--field ] [--changed] [--apply] [--yes] [--json] [--force] [--max-records] [--database-url]`.** A dry run (the default) reports, per field, the rows holding a NULL the expression gives a value; `--apply` writes them. Each value is the write's own computation over the stored row (option labels, the one reference hop, the `can()` map), and the apply writes through `engine.update` with `updated_at` kept, so hooks, roll-ups over the column and audit capture see an ordinary write. A NULL the expression itself answers (an empty reference, a null input) is left alone. A value that differs from the expression — the expression changed after the row was written — is counted on every run and rewritten only with `--changed`; an expression reading the clock always differs there. A row on which an expression does not evaluate while it holds a value is listed and not written, because the write would store NULL over it. The command exits 1 while a row is skipped or could not be written, or when `--max-records` cut the walk short. Idempotent: a re-run finds nothing to fill. + - **Boot advisory.** At `kernel:bootstrapped` the engine plugin reads the first 100 rows of each object declaring a stored computed field (nothing for an object declaring none) and, when that sample holds such a NULL, warns once naming each `object.field` and `os migrate stored-expressions`. It writes nothing and never fails a boot. + - **`@objectstack/objectql`:** `backfillStoredExpressions`, `formatStoredExpressionBackfillReport`, `storedExpressionBackfillComplete`, `announceStoredExpressionHoles`, `sampleStoredExpressionHoles`, and `ObjectQL.computeStoredExpressionsOver(object, rows, context?, onFault?)` — a stored computed field's values over rows already stored, computed as a write computes them. +- 343ddb2: refactor(objectql): `ObjectRepository` and `ScopedContext` in `engine.ts` are facades over their own facet modules, moved by the `engine.ts` codemod's second pass; no behaviour or published declaration changes + + Clause-②: no + Metadata-protocol: none + + - 20 of the 24 methods of the two classes keep their signatures and public docblocks on the class and forward, in one call, to an unedited copy of their body in `engine-object-repository.ts` or `engine-scoped-context.ts`. Each instance gets one host, built on first use (`engine-scoped-hosts.ts`), so a spy or a subclass override on an instance is still the method a body reaches. + - Four `ScopedContext` methods keep their bodies on the class, each named in the facade gate's registry with the card that will move it: `transaction` and the symbol-keyed `SEED_USER_GRANTS` method hand the instance itself on; `derive` and the symbol-keyed `APP_AUTHORED_HOOK_API` method set a private flag on another instance. + - Nothing to do on upgrade: the exported names, the published `.d.ts` surface and what every method does are the same. +- a26abbc: fix(objectql): an uninstalled package no longer reads as read-only until a restart + + Clause-②: yes (widening) — `@objectstack/objectql`'s exported `SchemaRegistry` gains the public method `onPackageUninstalled(listener)`, which returns its unsubscribe. Nothing is refused that was accepted before. + Metadata-protocol: none + + An administrator who uninstalled a package through `DELETE /api/v1/marketplace/install-local/:id` got a 200 "withdrawn from the running kernel", but on the same instance the package id still read as a booted code package until the server restarted. Uninstalling a `.custom` pair that an older runtime had installed as code, as the pair install now prescribes, left `POST /api/v1/packages//ensure-customization` answering `created: true` with `writable: false`, so nothing could be customized into the new pair. The engine's manifest map, which the ADR-0070 writability check reads first, kept the id after the registry had withdrawn the package. + + `SchemaRegistry.uninstallPackage` now announces each completed uninstall to `onPackageUninstalled` listeners, after the package record is gone (never for a refused uninstall), and the engine drops its manifest entry there, so the id is released on whichever door withdrew the package, not per door. A reinstall of the same id registers it again as before. A listener that throws does not turn the completed uninstall into a refusal; it is logged at `error`. +- 7193160: fix(objectql): a data driver receives the declared driver options and nothing else + + Clause-②: no + Metadata-protocol: none + + The engine → driver hop is a closed contract, as the engine's own option bags are: every driver call the engine makes is handed the keys `DriverOptionsSchema` declares and no other key. + + - **Which calls.** `find`, `findOne`, `count`, `aggregate`, `insert`, `update` and `delete` (by id and multi-row), the reads and writes those verbs make along the way, and the raw `execute` door. + - **What arrives, unchanged.** `transaction`, `tenantId`, `tenantIds`, `timezone`, `preserveAudit` and `bypassTenantAudit` reach the driver exactly as before, and a caller's explicit value still wins. The schema's other declared keys pass when a caller sets them at a door that admits them, which is `insert` and the raw `execute` door; `find`, `findOne`, `update` and `delete` admit only the six keys above at their own door. The retired `timeout` tombstone is not handed on. + - **What stays with the engine.** Undeclared keys in a caller's engine option bag stay with the engine and are no longer copied onto a driver's options. A driver reads the query from its `query` argument, as the driver contract declares. + - **Hooks are unchanged.** An `after*` hook still reads `input.options` as the caller's bag with the driver options merged on. + - **Who moves.** A driver that read a key `DriverOptionsSchema` does not declare off its options argument: read the query from the `query` argument, or have the key declared on `DriverOptionsSchema` first. No driver in this repository reads one. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [88cd0ab] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [15f808a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 154150dff40..133b8467bf7 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "type": "commonjs", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index 274d3bbe42d..c52a8b5e0e7 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/observability +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index b11247e7937..09a9c86a0a5 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "18.3.0", + "version": "18.4.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 81d65e3afc6..062cf04421d 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,73 @@ # @objectstack/platform-objects +## 18.4.0 + +### Patch Changes + +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- 977be38: fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204) + + Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`. + + - **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it. + - **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged. + - **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels). + - **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will. + + The one-line fix for a host: + + - FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it. +- 7b2bb66: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(plugin-auth): a session a password reset ends leaves a revocation trail + + Clause-②: no + Metadata-protocol: none + + - **A session a password reset ends leaves a revocation trail.** When a completed self-service password reset ends the account's sessions — the emailed link (`POST /api/v1/auth/reset-password`), and the phone OTP reset (`POST /api/v1/auth/phone-number/reset-password`); better-auth's email-OTP reset door, which the platform does not mount, is covered on its own path too — each `sys_session` row is now kept and stamped, `revoked_at` with `revoke_reason: password_reset`, the same kind of trail an interactive revoke leaves, instead of being deleted. The session is still refused (`401`). + - **The value says who ended the session.** `password_reset` is distinct from `user_revoked` (the owner revoking their own sessions) and from `admin_password_reset`, which an administrator's forced reset keeps writing. + - **`sys_session.revoke_reason`'s published vocabulary names `password_reset`** (`@objectstack/platform-objects`, the field's description and its translated help). + - **Unchanged.** Which sessions a reset ends; sign-out, which still deletes its row; the account's API keys, which a self-service reset does not revoke; and the audit log, which records no `logout` row for a reset. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/metadata-core@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 101768f3fa3..7316e73a7f9 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "type": "commonjs", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index 06d5b12bb0e..7b5369af9f9 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/embedder-openai +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 16c65810ac7..3cc7c3fb43c 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "18.3.0", + "version": "18.4.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.", "type": "commonjs", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index 6dea9381ce8..7da0f527c59 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/knowledge-memory +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-knowledge@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 108a8303b59..5d347c779e0 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "type": "commonjs", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index f661e129303..2b95edab772 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/knowledge-ragflow +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-knowledge@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index c83e63ce7bd..6a53086eff7 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "type": "commonjs", diff --git a/packages/plugins/organizations/CHANGELOG.md b/packages/plugins/organizations/CHANGELOG.md index 656ca44dffa..26977d72dc9 100644 --- a/packages/plugins/organizations/CHANGELOG.md +++ b/packages/plugins/organizations/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/organizations +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/organizations/package.json b/packages/plugins/organizations/package.json index aaa86be049d..607831ba78f 100644 --- a/packages/plugins/organizations/package.json +++ b/packages/plugins/organizations/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/organizations", - "version": "18.3.0", + "version": "18.4.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.", "type": "commonjs", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index 1270e070fc3..e95497bc20a 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/plugin-approvals +## 18.4.0 + +### Patch Changes + +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 046e0f0166f..026feb3af89 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "type": "commonjs", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index 7462e5107ad..6f8aca0ae98 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/plugin-audit +## 18.4.0 + +### Patch Changes + +- 171b6dd: fix(plugin-audit): the activity list opens newest first, without the platform's own rows, under a plain name and description + + Clause-②: no + Metadata-protocol: none + + - **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and every identity object `@objectstack/platform-objects/identity` declares (users, members, organizations, sessions, keys, SCIM): every sign-in stamps the last-login columns on `sys_user` under the signing-in person's own name, the signing key in `sys_jwks` is minted under the first sign-in's session, and adding a person writes `sys_user` and `sys_member` rows that person can read. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. + - **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [a26abbc] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 14ee5ec6310..509d40497be 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "type": "commonjs", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index d5853667aef..58744b5e00d 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,126 @@ # Changelog +## 18.4.0 + +### Minor Changes + +- 4be8a2e: feat(spec)!: retire `emailAndPassword.revokeSessionsOnPasswordReset` — a completed password reset always ends every session the account holds, and there is no setting (#29909) + + Clause-②: no (narrowing) — `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` becomes a `retiredKey()` tombstone, so the schema parse and `tsc` refuse it at any value, and plugin-auth's entry guard refuses it at any value too, `true` included, which it accepted before. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** for a configuration that still writes `emailAndPassword.revokeSessionsOnPasswordReset`, at any value. + + - **`@objectstack/spec`.** `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` is retired (ADR-0049 enforce-or-remove). A completed self-service password reset already ended every session the account holds, on every reset door, and an explicit `false` was already refused at boot, so the key could hold one value only. It is now a tombstone: a typed configuration that writes it fails `tsc`, and `AuthConfigSchema` / `EmailAndPasswordConfigSchema` refuse it with the prescription. The authorable-surface row reads `[RETIRED]`, `RETIRED_KEYS_BY_MAJOR[18]` lists `system/EmailAndPasswordConfig:revokeSessionsOnPasswordReset`, and the upgrade guide carries the semantic entry `email-and-password-revoke-sessions-on-reset-retired`. The prescription is exported as `REVOKE_SESSIONS_ON_PASSWORD_RESET_RETIRED` (`@objectstack/spec/system`). No ADR-0087 conversion: the key lives in the configuration a host hands the auth plugin, never in a stack or a stored row, so `os migrate meta` has nothing to rewrite. + - **`@objectstack/plugin-auth`.** The auth manager takes its configuration unparsed, so it refuses the retired key itself, where the configuration enters: at construction, which stops the boot, and on a configuration patch, where the standing configuration keeps ruling. It now refuses `true` as well as `false`, with the tombstone's own prescription. `true` asked for what the platform does anyway; the key is to be deleted either way. The auth instance still sets better-auth's own option of that name itself, so a completed reset ends the account's sessions exactly as before. + - **Unchanged.** What a password reset does: every self-service reset door ends every session the account holds, and each ended session is stamped `revoke_reason: password_reset`. An absent key is accepted, as before. + + **The remedy.** Delete `revokeSessionsOnPasswordReset` from the `emailAndPassword` block of the auth configuration, whatever it holds; nothing replaces it. + +### Patch Changes + +- c23501b: fix: a user's language preference is stored in its BCP-47 case, so `zh-cn` and `ZH-CN` are saved as `zh-CN` and the console renders that user's language (#29895) + + Clause-②: no + Metadata-protocol: none + + `sys_user.locale` used to store a tag as written. The column checks only the shape of a tag, so `zh-cn` was accepted, and the console, which picks its UI language by exact match, then rendered English for that user with no error. Every write of the column is now stored in BCP-47 case: language lower case, region upper case, script title case (`ZH-HANS-cn` → `zh-Hans-CN`). This applies to the data API (`PATCH /api/v1/data/sys_user/:id` and its bulk form), the admin user import and the identity-provider locale sync. The fold is the one `LocaleSchema` applies to translation locales. Nothing new is refused: `zh_CN` is still refused with 400, and an empty value still clears the preference. Rows stored mis-cased before this release are not rewritten. Such a row reads back as stored until the column is written again. +- cde492d: feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: + + - A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. + - Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. + - A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. + - A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. + + An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. +- 85c7374: fix(plugin-auth): a change-email request notifies the current address whatever way the caller is signed in + + Clause-②: no + Metadata-protocol: none + + - **The rule.** Every `POST /api/v1/auth/change-email` request that is accepted mails the account's current address the `auth.email_change_notice`, whatever way the caller is signed in. The notice goes to the account the request changes: its address is read from the session the endpoint itself acted on. + - **Unchanged.** The notice's template and text; the confirmation mail to the requested address; a refused request still mails nothing; the notice still never blocks the change. +- 7b2bb66: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(plugin-auth): a session a password reset ends leaves a revocation trail + + Clause-②: no + Metadata-protocol: none + + - **A session a password reset ends leaves a revocation trail.** When a completed self-service password reset ends the account's sessions — the emailed link (`POST /api/v1/auth/reset-password`), and the phone OTP reset (`POST /api/v1/auth/phone-number/reset-password`); better-auth's email-OTP reset door, which the platform does not mount, is covered on its own path too — each `sys_session` row is now kept and stamped, `revoked_at` with `revoke_reason: password_reset`, the same kind of trail an interactive revoke leaves, instead of being deleted. The session is still refused (`401`). + - **The value says who ended the session.** `password_reset` is distinct from `user_revoked` (the owner revoking their own sessions) and from `admin_password_reset`, which an administrator's forced reset keeps writing. + - **`sys_session.revoke_reason`'s published vocabulary names `password_reset`** (`@objectstack/platform-objects`, the field's description and its translated help). + - **Unchanged.** Which sessions a reset ends; sign-out, which still deletes its row; the account's API keys, which a self-service reset does not revoke; and the audit log, which records no `logout` row for a reset. +- c92b887: fix(plugin-auth): the phone-number password-reset request answers without waiting for the SMS send, as the emailed reset does + + Clause-②: no + Metadata-protocol: none + + - **The rule.** `POST /api/v1/auth/phone-number/request-password-reset` answers at once, the same way for any number: the reset text is built and sent after the response, as the emailed reset (`POST /api/v1/auth/request-password-reset`) already sends its mail. + - **A send that fails is logged on the server**, at error (`phone password-reset text was not delivered`), and never reaches the caller, who is answered `{ status: true }` either way, as before. The one-time code is in the text only, never in that line. + - **Unchanged.** The answer's status and body; the per-number cooldown and hourly cap and the per-IP limit on the route; the text itself; and a server-side `auth.api.requestPasswordResetPhoneNumber` call, which carries no request and still awaits its send. +- a7293f7: fix(plugin-auth): a sign-up answers without waiting for any mail send, whether or not the address already has an account + + Clause-②: no + Metadata-protocol: none + + - **The rule.** `POST /api/v1/auth/sign-up/email` answers without waiting for any mail send, whether or not the address already has an account: the verification mail, and the notice to an address that already has an account, are built and sent after the response, as the password-reset mail already is. + - **A send that fails is logged on the server**, at error (`verification email was not delivered`; the notice keeps its own error line), and never reaches the caller, whose answer is the one a sent mail gets, as before. + - **Unchanged.** The answer's status and body; the sign-up rate limits; the mails themselves; a signed-in caller's `/send-verification-email` resend, which still awaits its send and still reports one that failed; and a server-side `auth.api.signUpEmail` call, which carries no request and still awaits its sends. +- 4851668: fix(plugin-auth): a verification-email resend from a visitor who is not signed in answers the same whatever the send does + + Clause-②: no + Metadata-protocol: none + + - **The rule.** `POST /api/v1/auth/send-verification-email` from a visitor who is not signed in answers the same whatever the send does. While the email transport is live it answers `{ "status": true }`: the verification mail is built and sent after the response, as the sign-up's verification mail is, and a send that fails is logged on the server at error (`verification email was not delivered`). + - **While the email transport is not live** (no email service, or one whose `isConfigured()` does not answer `true`, such as the `log` transport), every such resend is refused with `400 EMAIL_SERVICE_REQUIRED`, the code `/change-email` and the identity import already answer for a deployment with no email delivery. Its message names the fix (Settings → Mail), and the verify screens show it. + - **Unchanged.** A signed-in caller's resend for their own address, by session cookie or by bearer token, still awaits its send, still reports one that failed, and is not refused while the transport is not live. A server-side `auth.api.sendVerificationEmail` call, which carries no request, still awaits its send. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [63b86b3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [8eca18f] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 47f2f79951e..4adbff862b7 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "type": "commonjs", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index 1fa93857465..ad596dd734e 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/plugin-dev +## 18.4.0 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [63b86b3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [474c030] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/account@18.4.0 + - @objectstack/setup@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/service-i18n@18.4.0 + - @objectstack/service-realtime@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index ad877dbc9c2..717632f196c 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "type": "commonjs", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index b104f6d6a7a..13b38e1d7c5 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,52 @@ # @objectstack/plugin-email +## 18.4.0 + +### Patch Changes + +- 509c40a: fix(plugin-email): a non-string recipient display name answers the send door's 400, not 500 + + Clause-②: no + Metadata-protocol: none + + `POST /api/v1/email/send` with a recipient such as `{ "address": "ok@example.com", "name": 42 }` in `to`, `cc`, `bcc` or `replyTo` (or as one entry of a `to` / `cc` / `bcc` list) now answers `400 VALIDATION_FAILED` with "invalid recipient address in `to`: the display name of ok@example.com must be a string" (`reply-to address` for `replyTo`). It used to answer `500 EMAIL_SEND_FAILED` with `a.name?.trim is not a function`. + + The refusal itself was already right. The 500 came from the code that writes the `sys_email` rejection row after the refusal: it called `.trim()` on the caller's raw display name. That writer now reads the name as unknown input and leaves a non-string name out of the row (the row's `error` column names it), so the caller gets the 400 and the rejection is still recorded. A string name, an absent name and a `null` name behave as before. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 4b279612df8..e507f1db782 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "type": "commonjs", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index 9dc3e3dbbc9..1a536bc3788 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/plugin-hono-server +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index d184de97e26..d3fd0174423 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": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "type": "commonjs", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index f2280f5483b..ef44ecf5d61 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/plugin-pinyin-search +## 18.4.0 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [a26abbc] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 6ffca057a52..1313c691e36 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": "18.3.0", + "version": "18.4.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).", "type": "commonjs", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index 4670afa0fbd..302164f2e2f 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,162 @@ # @objectstack/plugin-security +## 18.4.0 + +### Minor Changes + +- 21510bb: feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope + + Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: narrowed + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. + + - **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. + - **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. + - **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. + - **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. + - **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. + - **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. + + Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. +- 0fd363d: fix(spec)!: an object permission that edits or deletes a record also reads it + + Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. + + + + **BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). + + - **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. + - **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. + - **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. + - **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. + - **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. + - Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. + + **FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). + +### Patch Changes + +- 8f05e61: fix(plugin-security): an organization administrator's update by id is refused before the row is looked up, whatever the id names + + Clause-②: no + Metadata-protocol: none + + An organization administrator (`organization_admin` or `organization_admin_no_bypass`, holding no `adminScope`) updates an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. An edit in place is never part of that administrator's right, so the write is refused before the row is looked up: `403 PERMISSION_DENIED`, with the same sentence and details for a row, for a row the administrator cannot read, and for an id that names no row. + + Unchanged: a removal by id of a row the administrator cannot read, or of an id that names no row, answers the read door's `404 RECORD_NOT_FOUND`. An administrator who also holds an `adminScope` is judged by that scope. +- 474c030: fix(plugin-security): an assignment write the administrator cannot address answers not-found, as a read does; a refusal names only values the caller can read + + Clause-②: no + Metadata-protocol: none + + An organization administrator or a delegated administrator (a caller holding an `adminScope`, an organization administrator who holds one included) updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. + + - **A row the caller cannot read, and an id that names no row,** now answer `404 RECORD_NOT_FOUND`: the read door's answer for that object and id, which is what a by-id write of any other object already answers. Both used to answer `403 PERMISSION_DENIED` ("the assignment row … could not be read"). An organization administrator's edit in place is asked the same question first, so an edit of a row it reads is still refused as an edit. + - **A caller the read door refuses outright** (no read grant on the table) still gets that read refusal. + - **A refusal names a value only when the caller can read it.** A delegated administrator's `DELEGATION_SCOPE_VIOLATION` names the row's position, business unit, holder or permission set only when the caller's own read returns that column at its stored value, or when the caller wrote the value itself. Otherwise the sentence is the same without the value (for example "the assignment's position distributes a permission set which is not in the scope's allowlist"), and `details` leaves the value out. The verdict does not change: it is still taken on the stored row. + - **Unchanged.** A row the caller reads keeps its verdict and its guidance. Inserts and tenant-level administrators are judged as before, and every write that was admitted is still admitted. + + For clients: a by-id update or delete of an assignment row that is gone now answers `404`, as it does on every other object. +- 42e1211: fix(plugin-security): a delegated administrator's refused assignment write answers the same for a row it cannot read as for a missing one + + Clause-②: no + Metadata-protocol: none + + A delegated administrator — a caller holding an `adminScope`, an organization administrator who holds one included — updates or removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks first whether the caller itself reads that row, in its own context. + + - **A row the caller cannot read** is answered exactly as an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. Both are answered at the gate, before any later step judges the write. + - **A row the caller reads** keeps the verdict and the guidance it had: the gate judges the stored row, as before. + - **A caller the read door refuses outright** (no read grant on the table) is answered as before: the read refusal, for every id the gate refuses, and the writes it was admitted are still admitted. + - **Unchanged.** Inserts, tenant-level administrators, and organization administrators who hold no scope are judged as before. +- 72d7db0: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(plugin-security): an organization administrator's refused assignment write answers the same for a row it cannot read as for a missing one + + Clause-②: no + Metadata-protocol: none + + An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. The delegated-administration gate (ADR-0090 D12) now asks the administrator's own read, in its own context, whether that row is there to remove. + + - **The removal of a row the administrator cannot read** is answered exactly as the removal of an id that names no row is: the read door's `404 RECORD_NOT_FOUND` for that object and id. + - **A row the administrator can read** is judged on the stored row. A refusal names the organization boundary, and names the row's organization, position or permission set only when the administrator's own read returns that value as stored. + - **Unchanged.** An edit in place of a row it reads is refused as before, every removal that was admitted is still admitted, and a tenant-level administrator is judged as before. +- d0da880: fix(plugin-security): an organization administrator's assignment removal is judged on the stored row, and its refusal names only values the caller can read + + Clause-②: no + Metadata-protocol: none + + An organization administrator (`organization_admin` or `organization_admin_no_bypass`) removes an assignment row (`sys_user_position` or `sys_user_permission_set`) by id. Once its own read returns the row, the delegated-administration gate (ADR-0090 D12) judges the removal on the stored row, as it judges a delegated administrator's write. + + - **The verdict is the stored row's.** A field rule or a masking rule on the row's `position` or `permission_set` does not change it: the removal of a row inside the organization boundary is admitted, and the removal of a row outside it is refused. + - **A refusal names a value only when the caller can read it.** The row's organization, its position and the sets that position carries, or its permission set are named only when the caller's own read returned that column at its stored value. Otherwise the sentence leaves the value out, for example "the assignment's position is not inside the organization boundary", and `details` leaves it out too. + - **Unchanged.** A row the caller cannot read, and an id that names no row, still answer the read door's `404 RECORD_NOT_FOUND`. An edit in place is refused as before. Inserts, tenant-level administrators and delegated administrators are judged as before. +- d7c0389: fix(plugin-security)!: a write from a caller the read door refuses is refused, whatever rows it addresses + + Clause-②: no (narrowing) — a predicate update or delete (`multi: true`, addressed by a `where`) from a caller whose read of the object is refused is refused with that read refusal before the write runs, where it was admitted before; nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for a caller whose read of an object is refused while its write is admitted, and that updates or deletes that object's records by predicate. With the permission model's own grants that caller exists in one shape only: an object whose `requiredPermissions` map gates `read` and not `update` or `delete` (ADR-0066 ⑤), and a caller holding the write grant without the capability the read requires. + + - **The rule.** A caller the read door refuses writes none of the object's records, by id or by predicate. The predicate update and delete such a caller addresses — a flow's `update_record` or `delete_record` node with `multi: true` run as that user, or an engine `update` / `delete` with `multi: true` under that caller's context — now answer the read door's own refusal (the `PERMISSION_DENIED` envelope that caller's read gets), and nothing is written. The by-id update and delete are refused too. + - **Unchanged.** A caller who reads the object keeps its predicate write, scoped to the records it reads. A caller the object's CRUD grants refuse is refused as before. A write the platform issues under the caller's context (a hook's or a cascade's own write, the referential clear of a deleted record's lookups) and a system write keep their answers. A store fault on the read propagates as before. + - **Who moves.** A deployment that relies on such a caller writing by predicate grants it the capability the object's read requires, or gates the write verbs on a capability that caller holds. +- 917f024: fix(plugin-security)!: an external object that states no organization scope reads as empty for every caller, the platform owner included + + Clause-②: no (narrowing) — under the `isolated` and `group` postures, an external object registered after the boot that states no organization scope reads as empty for every caller, the declared platform owner included; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: none + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) that registers an external object after the boot without stating its organization scope. + + - **The rule (ADR-0015 §10 as amended).** Whose rows an external object serves is a fact only its author can state. Under a walled posture an external object that states none is refused by name when the boot completes, and one registered after the boot — a Studio import, a runtime datasource — reads as empty for every caller, the platform owner included, and accepts no write, until it states its scope. That deny judges the object's configuration, not the reader's organization. + - **What is unchanged.** The declared platform owner's reads still cross the organization wall everywhere else: on local objects, on an external object that declares its own `organization_id`, and in a session with no active organization. Writes stay walled for everyone. A member's reads are unchanged. + - **Who moves, and the fix.** Declare one of the three on the external object, as the ADR-0087 entry `external-object-organization-scope-declared` prescribes: an `organization_id` field of its own (the rows are then walled by organization), `tenancy: { enabled: false }` (shared by every organization), or `systemFields: { tenant: false }` (the same opt-out on the system fields). +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index f13edcb9030..1e027afcede 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "type": "commonjs", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index f3641f6cc16..c05ee4ecc31 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,55 @@ # @objectstack/plugin-sharing +## 18.4.0 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [a26abbc] +- Updated dependencies [eca7cd2] +- Updated dependencies [7193160] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index f3168ed6d37..425c4d6af22 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "type": "commonjs", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index e8ce0c50d4a..18733f61f34 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,43 @@ # @objectstack/plugin-webhooks +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 15e99c53e9a..7cd7963105e 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "18.3.0", + "version": "18.4.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 0f77840187c..ed0e345445a 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,96 @@ # @objectstack/dogfood +## 0.0.52 + +### Patch Changes + +- Updated dependencies [31fe289] +- Updated dependencies [a35fc66] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [509c40a] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [171b6dd] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [977be38] +- Updated dependencies [a26abbc] +- Updated dependencies [474c030] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/service-analytics@18.4.0 + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/verify@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/plugin-email@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/plugin-approvals@18.4.0 + - @objectstack/plugin-audit@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/example-crm@4.0.104 + - @objectstack/example-showcase@0.3.26 + - @objectstack/connector-mcp@18.4.0 + - @objectstack/plugin-pinyin-search@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/plugin-webhooks@18.4.0 + - @objectstack/trigger-record-change@18.4.0 + - @objectstack/example-multi-package@0.0.11 + - @objectstack/connector-openapi@18.4.0 + - @objectstack/connector-rest@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/trigger-schedule@18.4.0 + - @objectstack/types@18.4.0 + ## 0.0.51 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index 8f6f446be0d..b3bad7afc2e 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.51", + "version": "0.0.52", "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 f5a57b24fbc..0e023db1f01 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/downstream-contract +## 0.0.50 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 0.0.49 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index f62ddf6c174..37a69bcbde0 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.49", + "version": "0.0.50", "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 f30ae9d33d2..e6c2b8a2362 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,16 @@ # @objectstack/http-conformance +## 0.1.12 + +### Patch Changes + +- Updated dependencies [9ebca94] +- Updated dependencies [771b2db] +- Updated dependencies [f19fbb8] +- Updated dependencies [aa57c76] +- Updated dependencies [21510bb] + - @objectstack/core@18.4.0 + ## 0.1.11 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 7c5e1c36fb8..df3d20cd4b7 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.11", + "version": "0.1.12", "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 a0c78d34dad..efef4a86370 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,82 @@ # @objectstack/rest +## 18.4.0 + +### Patch Changes + +- 18112bf: fix(rest): the one-request record page accepts a related list keyed by a `user` field again + + Clause-②: no + Metadata-protocol: none + + - **Regression in 18.3.0 (#29946, duly#185).** `POST /data/:object/:id/page` (#27713) checked each related list's field against a private `lookup`/`master_detail` list and read `reference` raw, so a related list on `sys_user` keyed by a `user` field (an owner, an assignee) answered `400 VALIDATION_FAILED … is not a lookup or master-detail field that references 'sys_user'`, and the console's full-page record view showed "could not load this record". The related list's own row query (`{ [field]: id }`) follows that field, and 18.2.0 read each related list through that list read, with no type check. + - The check now asks the spec's one arbiter of a field's target, `referenceTargetOf` (`lookup`, `master_detail`, `user`, `tree`; a `user` field's target is fixed to `sys_user` by its type, whether or not its `reference` spells it), the same judgement the engine's `$expand` and delete walk read. A field that does not reference the page's object is still a located `400 invalid_relationship`: a `user` field on any other object's page, a `lookup` to another object, a `tree` on another object, an unreadable `reference` carrier. The message now reads `… is not a lookup, master-detail, user or tree field that references ''.` +- 63b86b3: fix(rest): the approvals, email, sharing, sharing-rule and analytics routes answer from the request's environment kernel when one REST plugin fronts many environments + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the five provider seams the plugin hands `RestServer` for `approvals`, `email`, `sharing`, `sharingRules` and `analytics` read the HOST kernel's service whatever environment they were asked for, and the unscoped (hostname-routed) mount asked them for no environment at all. The environment's own services were never reached: `GET /api/v1/approvals/requests` answered `{ data: [] }`, `GET /api/v1/approvals/requests/:id` answered 501, and the sharing, sharing-rule and email doors answered 501, on every hosted environment. The record page's approvals part answered no requests. + - **What changes.** Given an environment id and a registered `kernel-manager`, each of the five providers now reads its slot on that environment's kernel (`kernelManager.getOrCreate(id)`), the same acquisition the data routes make. The routes pass the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. There is no fallback to the host's occupant: an environment whose kernel carries no such service gets the door's own "not configured" answer. + - **Analytics.** `POST /analytics/dataset/query` already read the environment's service first, and now reads it alone: when the environment's kernel carries no analytics service, or cannot be acquired, the answer is the route's 501 rather than the host's service. + - **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads all five services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. + - **No migration.** No configuration, route, response shape or export changes. +- 1a3ea0c: fix(rest): the `/security` routes, and the auth, settings, i18n and metadata seams, answer from the request's environment kernel when one REST plugin fronts many environments + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `security` provider read the HOST kernel's service whatever environment it was asked for, and the unscoped (hostname-routed) suggested-binding mount asked it for no environment at all. The host has no `security` service, so `GET /security/suggested-bindings` and `POST /security/suggested-bindings/:id/confirm` answered 501 on every hosted environment: no owner could accept an installed app's default permission set, and every invited member read 403 on that app's data. `security/explain` and the record, export and meta doors that read the same service fell back to the host's occupant whenever the environment's kernel did not answer. + - **What changes.** Given an environment id and a registered `kernel-manager`, the `security` provider now reads `security` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, sharing-rule and analytics providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's occupant is no longer a fallback. + - **The audited seams.** The `auth`, `settings`, `i18n` and `metadata` providers are environment-scoped the same way. On a hosted runtime the REST face's execution context now localizes (time zone, locale, currency) from the environment's own settings, as the runtime dispatcher's already did, instead of the host's. An environment whose kernel carries no `auth` service is no longer authenticated by the host's identity store. The `tenancy` provider keeps reading the plugin's own kernel: every consumer reads the bound environment kernel's `tenancy` first. + - **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads every one of these services on the plugin's own kernel exactly as before, for every route and every id. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. + - **No migration.** No configuration, route, response shape or export changes. +- 8eca18f: fix(rest): the transactional `POST /batch`, the `/meta` object-state and published reads, and the meta read gate's disabled-package check use the request's environment's data engine when one REST plugin fronts many environments + + Clause-②: no + Metadata-protocol: none + + - **What was wrong.** On a host that mounts one `RestApiPlugin` in front of many environment kernels (the hosted runtime shape), the `objectql` provider read the HOST kernel's engine whatever environment it was asked for, and the unscoped (hostname-routed) mount asked it for no environment at all. `POST /batch` therefore opened its transaction on the host's engine and sent its update and delete ops through it, while its create ops went to the environment's own protocol, joined to the host's transaction. `GET /meta/object/:name/state/:field` judged the object by the host engine's registry, and `GET /meta/:type/:name/published` and the meta read gate asked the host engine's registry whether an environment's package is disabled. The request's execution context already read the environment's engine, so one request used two kernels. + - **What changes.** Given an environment id and a registered `kernel-manager`, the `objectql` provider now reads `objectql` on that environment's kernel, the same environment-scoped seam the approvals, email, sharing, security and other providers use. Every route that reads it passes the id the shared request→environment resolution answered: the `/environments/:environmentId` param on the scoped mount, else the host's `kernel-resolver` (or the hostname / `X-Environment-Id` chain) on the unscoped one. The host's engine is no longer a fallback: an environment whose kernel carries no engine answers `POST /batch` with `501 NOT_IMPLEMENTED`. + - **Unchanged.** A single-kernel deployment (no `kernel-manager`) reads the engine on the plugin's own kernel exactly as before, through the same factory-aware lookup: a factory that fails to build is still a loud `503`, and only an engine that was never registered reads as absent. The reserved `platform` id and a request the resolver places in no environment also read the plugin's own kernel. + - **No migration.** No configuration, route, response shape or export changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/service-package@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index dceb3eba85b..5324095798c 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "18.3.0", + "version": "18.4.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 af14d247ab8..f7b79dcd873 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,132 @@ # @objectstack/runtime +## 18.4.0 + +### Minor Changes + +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. + +### Patch Changes + +- 3b8c724: fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not + + Clause-②: yes (widening) + Metadata-protocol: none + + A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages//export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension. + + - **The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`. + - **The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in. + - **Every served read is unchanged**: `GET /api/v1/meta/object/`, the object list, the console and the data API keep serving the object with every extension folded in. + + `SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller. + + Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it. +- 30dcd8a: fix(runtime): a per-record action called with no recordId is refused 400 naming it, and an unknown id 404s at the door, before its flow runs + + Clause-②: no — every call refused now already failed on `main`: the flow behind the action ran and answered `400 FLOW_FAILED` naming its internal node, with no id or with an id the caller cannot resolve. + Metadata-protocol: none + + An action declared `execution: 'perRecord'` (an api action or a flow action) called through the REST `/actions` door or MCP `run_action` with no `recordId`, or an empty one, is refused `400 VALIDATION_ERROR` naming `recordId`, and nothing is forwarded or started. The same api action called with an id the caller's scope does not resolve answers the door's own `404 RECORD_NOT_FOUND` instead of being forwarded, as a flow or script action already did. Actions declared `aggregate`, or declaring no `execution`, run record-less as before (#29173). Fixes #29881. +- Updated dependencies [31fe289] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [88cd0ab] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [f9e5bef] +- Updated dependencies [63b86b3] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [343ddb2] +- Updated dependencies [15f808a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [474c030] +- Updated dependencies [85c7374] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/lint@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/driver-turso@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/service-cluster@18.4.0 + - @objectstack/service-i18n@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index cb7b3c9bdb6..2e8ad0f43f0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "18.3.0", + "version": "18.4.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 232c811d197..d8a10c82a6b 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,9 @@ # @objectstack/sdui-parser +## 18.4.0 + +No changes in this release. + ## 18.3.0 No changes in this release. diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 9816e75e413..584d16e432c 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "type": "commonjs", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index e7b0867e8cc..df9232b347e 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog — @objectstack/service-analytics +## 18.4.0 + +### Patch Changes + +- a35fc66: fix(service-analytics): the refusal of an ungrouped aggregate over a record-currency amount names the home field as the remedy for a consolidated total (ADR-0144 D6) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Now.** A `sum`, `avg`, `min` or `max` over a `currencyMode: 'record'` amount that does not group by its code field is still refused `INVALID_FIELD` / 400 before anything is read, with the same code, status, member, field and object. Its message now names both remedies. For one total in the home currency, point the measure at the amount's home field: by name (`homeAmount.field`) when the host relays it, or else as "the field its `currencyConfig.homeAmount.field` names", with the declaration to add if it has none. For one row per currency, add the dimension over the code field. Before, the message named only the grouping. + - **Unchanged.** A measure over the home field is no record-mode amount, so it is served unchanged: ungrouped, grouped by the code field, and as a `totals` grand total, labelled with the home currency. + - **The widening.** `AnalyticsServiceConfig.sourceFieldMeta` may answer one more optional key, `homeAmountField`: the amount's `currencyConfig.homeAmount.field`, beside a `currencyField` only. `AnalyticsServicePlugin` relays it. A host that does not answer it gets the message without the field's name. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 71e27ba417e..ee161be5b0e 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "18.3.0", + "version": "18.4.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 7965aac4a0e..57449708c61 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/service-automation +## 18.4.0 + +### Minor Changes + +- 04f36b6: feat(service-automation): a record-triggered flow's notification opens the record that started the run, without the author writing `sourceObject` and `sourceId` + + Clause-②: no — no accepted key, value or exported type changes; the node's existing `sourceObject` / `sourceId` / `actionUrl` keys keep their meaning when written. + Metadata-protocol: semantic + + - **Before.** A `notify` node built its click-through link only from an explicit `sourceObject` + `sourceId` pair (or an `actionUrl`). The obvious record-triggered flow — `recipients`, `title`, `message` — passed `os validate` and sent a notification whose link was empty, so a click opened the notification list instead of the record. + - **Now.** A `notify` node that writes none of `sourceObject`, `sourceId` and `actionUrl` links to the run's trigger record when the run has one: a record change (create or update), a time-relative sweep's row, or an action started on a record. The notification stores that record as its `source_object` / `source_id`, and the inbox opens `/OBJECT/ID`. + - **Unchanged.** An explicit pair or an explicit `actionUrl` wins, as before. A half-written pair is still dropped rather than defaulted. A run with no trigger record (a schedule, an inbound API post, a manual start with no record) links nowhere, as before. A delete event is not linked: its record is gone once the write lands. + - **To link elsewhere** from a record-triggered flow, write the `actionUrl` or the `sourceObject` + `sourceId` pair the notification should open. + +### Patch Changes + +- 4be9734: fix(automation)!: a `currency` or `percent` screen field refuses a non-number on resume, as a `number` field does, and the console renders `percent` as a numeric input (#30178) + + Clause-②: yes (narrowing) — `POST …/runs/RUN/resume` refuses a string under a `type: 'currency'` or `type: 'percent'` screen field, a bag it accepted before. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the screen-flow resume door. + + - **Before.** A `type: 'percent'` or `type: 'currency'` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too. The bound compares numbers, so it never judged a string, and the run consumed its suspension on the value. The console rendered `percent` as a text input and posted a string, so a `percent` field's `min` / `max` held for no console user. + - **Now.** `number`, `currency` and `percent` are one value domain: a present value that is not a finite JSON number is refused with `invalid_type` inside `400 INVALID_SCREEN_INPUT`, the suspension is kept, and a corrected resume still lands. A numeric string is not read as the number it spells. The console renders `percent` as a numeric input carrying `min` / `max`, so it posts a number and the browser stops an over-limit value before any request. + - **Unchanged.** An absent value and a hidden field are not judged; `rating`, `slider`, `progress`, `text` and untyped fields still take a string; a JSON number on `currency` / `percent` is judged against the bound as before. + - **For a caller that skips the console.** The resume bag carries the JSON number (`60`), not a string; the refusal names the field and says it must be a number. No stored metadata changes, so there is nothing for `objectstack migrate meta` to rewrite. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [88cd0ab] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [07685ae] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [15f808a] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [713beba] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-protocol@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/formula@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 993c3cbebee..5c768708ad0 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "18.3.0", + "version": "18.4.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 a7f60c18fc0..1253daabfe3 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/service-cache +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/observability@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 09f1f8e34fb..8f02c70e49b 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "18.3.0", + "version": "18.4.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 aa8e2bcba77..979819456fc 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,36 @@ # @objectstack/service-cluster-redis +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/service-cluster@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 9efd0a0d264..c72ca0a410e 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": "18.3.0", + "version": "18.4.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 d23a744fc4b..334e1e4c20e 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/service-cluster +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index 8652415ff2c..61d682f78e2 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "18.3.0", + "version": "18.4.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 b140aebcc7c..1f11c1be719 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-external-datasource +## 18.4.0 + +### Patch Changes + +- 5cdaddb: fix(service-datasource)!: a datasource update whose new configuration fails to connect stores none of it, and a whitespace-only `secret` is refused like an empty one + + Clause-②: yes (narrowing) — `PATCH /api/v1/datasources/:name` and `POST /api/v1/datasources` refuse one more body that used to answer 200 / 201: a `secret` whose value is whitespace only (`" "`, a tab and a newline), now `400 DATASOURCE_ADMIN_ERROR` with the empty-secret answer. The widening half: the `reregisterPool` member of the exported `DatasourceAdminServiceConfig` may now answer `{ previousPoolKept: boolean }` (still optional; a seam that answers nothing reads as before). + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the datasource admin doors (`@objectstack/service-datasource`): a whitespace-only `secret` is refused. + + - **Before.** A `PATCH` whose new configuration failed to connect (a wrong password, a blank `secret`, a changed `config` that does not reach the database) answered `200` with `status: "error"` and a reason saying the previous pool was kept and the new configuration was NOT in force. It had already stored the new record, repointed `external.credentialsRef` at a new `sys_secret` row and removed the working one, so the next restart connected the configuration that failed. A whitespace-only `secret` passed the empty-secret guard and reached this path. + - **Now.** The update rebuilds the pool before it stores anything. When the previous pool is kept, nothing of the patch is stored: the record, its `credentialsRef` and every other key of the patch stay as they were, and the `sys_secret` row sealed for the patch is removed again. The answer is unchanged (`200`, `status: "error"`, "the new configuration is NOT in force"), and now true: a restart connects the configuration that was serving. A configuration that connects is stored and in force, and the replaced secret is retired, as before. A whitespace-only `secret` is refused before anything is written, by the same guard and with the same message as the empty string; a value with whitespace around other characters is still sealed as sent. + - **Who moves.** The console sends `secret` only when the field holds a value. A script or SDK call that sends a whitespace-only `secret` gets the 400. A host that wires its own `reregisterPool` seam keeps today's behaviour until it answers `{ previousPoolKept: true }` for a rebuild that kept the previous pool. + + **The remedy.** To keep the stored credential, leave `secret` out of the request (or send `null`). To replace it, send the new credential. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [dcb5fb3] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/driver-turso@18.4.0 + - @objectstack/driver-sql@18.4.0 + - @objectstack/driver-mongodb@18.4.0 + - @objectstack/driver-memory@18.4.0 + - @objectstack/driver-sqlite-wasm@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 54ea590dcd5..cdb9b86ad68 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "18.3.0", + "version": "18.4.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 74bb0c50dea..d25257185fa 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/service-i18n +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index a5306930196..311c3a8d634 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "18.3.0", + "version": "18.4.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 73509f42b52..f09eb30922e 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/service-job +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 485db75343c..05e6b6812dc 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "18.3.0", + "version": "18.4.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 5a724e7c792..63439f82487 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/service-knowledge +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 05d2ab6bf5d..88d3240582a 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "18.3.0", + "version": "18.4.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-mailbox/CHANGELOG.md b/packages/services/service-mailbox/CHANGELOG.md index fe2e0bb0ffb..8b2c46f5243 100644 --- a/packages/services/service-mailbox/CHANGELOG.md +++ b/packages/services/service-mailbox/CHANGELOG.md @@ -1,5 +1,43 @@ # @objectstack/service-mailbox +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + - @objectstack/connector-imap@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-mailbox/package.json b/packages/services/service-mailbox/package.json index a7aa782e0d3..4e380582ce2 100644 --- a/packages/services/service-mailbox/package.json +++ b/packages/services/service-mailbox/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-mailbox", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Mailbox Service for ObjectStack \u2014 inbound mail: the mirrored message (sys_mail_message), its record associations and their read gates.", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index acc832a5ef1..4a823a24bee 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,61 @@ # @objectstack/service-messaging +## 18.4.0 + +### Patch Changes + +- 8aa2e91: feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). + - **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. + - **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. +- 771b2db: fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. + - **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). + - **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. + - **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. + + Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index bfa39126f83..1ac92f17169 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "18.3.0", + "version": "18.4.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 43a675b1f0b..ac425be638b 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,39 @@ # @objectstack/service-package +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index b6c92f8f928..6c1423bf04d 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-print/CHANGELOG.md b/packages/services/service-print/CHANGELOG.md index e45fabb312c..1804f08e756 100644 --- a/packages/services/service-print/CHANGELOG.md +++ b/packages/services/service-print/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/service-print +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-print/package.json b/packages/services/service-print/package.json index 26e6ae58b11..e94e97d4852 100644 --- a/packages/services/service-print/package.json +++ b/packages/services/service-print/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-print", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Print Service for ObjectStack — renders a record's print page in headless Chromium under the requesting user's own principal and archives the PDF as a file attached to the record.", "type": "module", diff --git a/packages/services/service-push/CHANGELOG.md b/packages/services/service-push/CHANGELOG.md new file mode 100644 index 00000000000..a9bd4d055dc --- /dev/null +++ b/packages/services/service-push/CHANGELOG.md @@ -0,0 +1,69 @@ +# @objectstack/service-push + +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. + +### Patch Changes + +- 8aa2e91: feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). + - **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. + - **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 diff --git a/packages/services/service-push/package.json b/packages/services/service-push/package.json index ffeaad0e15c..ec81b8cde8c 100644 --- a/packages/services/service-push/package.json +++ b/packages/services/service-push/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-push", - "version": "18.2.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Push service for ObjectStack — IPushService over the APNs (token auth) and FCM HTTP v1 transports, configured by PushServiceConfigSchema with credentials by reference.", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index f2e56201912..206862de240 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,63 @@ # @objectstack/service-queue +## 18.4.0 + +### Patch Changes + +- 771b2db: fix(core,service-messaging,service-queue): an idle dispatch loop wakes at the next due time, and a sole writer's loop no longer re-runs the idle ramp after every boot + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/core`: `DispatchLoop` wakes at the next due time.** A new optional `DispatchLoopOptions.nextDueAt()` answers the earliest instant work becomes claimable (a deferred row, a claim's expiry). The loop asks it once per idle streak, after the first empty tick, and never sleeps past the answer, so a deferred row goes out within one tick of coming due whatever `maxIdleIntervalMs` is. The answer is remembered rather than re-read on every ceiling tick, so an idle loop's steady-state cost is unchanged; the new `DispatchLoop.wakeAt(dueAtMs)` lets a producer in the same process announce a deferred row at no read. An answer at or before the tick that found nothing is not chased, and a rejected tick or read still counts as idle, so neither can make the loop spin. + - **`@objectstack/core`: `DispatchLoopOptions.soleWriter`** (default `false`). On a host where no other process writes the store, an empty tick sleeps straight to the next due time or the ceiling instead of re-running the ramp from `intervalMs`, which only ever bought latency on rows another process wrote. A fresh sole-writer loop over an empty store now ticks once in its first minute; before, every boot re-ran the ramp (six ticks in the first minute at the dispatchers' 500 ms base, five at the queue's 1 s). + - **`@objectstack/service-messaging`:** `INotificationOutbox.nextDueAt` / `IHttpOutbox.nextDueAt` (optional; both built-in stores implement it: two reads, the earliest future `next_attempt_at` / `next_retry_at` and the oldest `in_flight` claim plus the claim TTL), wired into `NotificationDispatcher` and `HttpDispatcher`; `soleWriter` on both dispatchers' options, and `dispatchSoleWriter` on `MessagingServicePlugin`. Retries, quiet hours, digest windows and a crashed node's claims are taken at their due time instead of up to `dispatchMaxIdleIntervalMs` late. + - **`@objectstack/service-queue`:** `DbQueueAdapter` reads the earliest future `scheduled_for` of its subscribed queues (one read per idle streak), a deferred `publish()` announces its due time to the loop through `wakeAt` instead of waiting out the backoff, and `DbQueueAdapterOptions.soleWriter` (reachable as `QueueServicePlugin` `db.soleWriter`) skips the ramp. + + Nothing changes for a host that sets neither `soleWriter` flag except the due-time wake: the ramp keeps its shape, cut short only when a due row is earlier, and an idle stretch costs one extra read at its start. +- 8b117c0: fix(service-queue): a job whose worker died mid-handler is reclaimed once its lease runs out, instead of staying `running` forever + + Clause-②: no — restores the documented `leaseMs` reclaim; no accept set or export widens + Metadata-protocol: none + + `DbQueueAdapterOptions.leaseMs` documented a lease "before another worker may reclaim", but the claim read only `pending` rows, so a worker that crashed, was OOM-killed or was redeployed mid-handler left its `sys_job_queue` row `running` for good: never retried, never dead-lettered. + + - **Reclaim.** A poll tick reads the table's `running` rows (one read for every queue, at most once per lease window) and returns each whose `locked_until` has passed to the retry path. The lost run counts as an attempt: the row goes back to `pending` on its backoff, or to `dlq` once `maxAttempts` is spent, with `last_error` saying the lease expired. The write is a compare-and-set on the expired lease, so a holder that renewed in the meantime keeps its run. + - **Renewal.** While a handler runs, its worker moves its lease a full window ahead every third of `leaseMs`, so a slow but live handler is never reclaimed and run twice. A handler must not block the event loop for most of a lease window. + - **Sole writer.** With `soleWriter` no other process claims rows, so the reclaim reads once per boot (finding what a crashed previous process left behind) and again only when one of this process's own claims was left unsettled by a failed write and its lease runs out. `nextDueAt` folds in those leases and the earliest live lease the last read saw, at no read of its own, so the loop wakes for them instead of at the idle ceiling. + - **Cost.** Without `soleWriter`, an idle queue pays one more indexed read per lease window (default 30 s): an idle hour at the defaults goes from 125 to 246 reads. With `soleWriter`, one more read per boot: a fresh kernel's first minute goes from 2 to 3 reads, and an idle hour carries no other. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index c544200877d..a9b64f1a942 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "18.3.0", + "version": "18.4.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 b44a141d330..302e3b7dd40 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/service-realtime +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 457e44efaac..8d05b53ab0c 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "18.3.0", + "version": "18.4.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 ca474553258..f7a6e7e64a3 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,92 @@ # @objectstack/service-settings +## 18.4.0 + +### Patch Changes + +- 8d5aab3: fix(cli): `os dev` and `os start` share one data-key file, so a secret saved under `npm run dev` reads back under `os start` + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/cli`.** Both commands open the same project database, but `os start` used to mint its own data key under its own home (`PROJECT/.objectstack/dev-crypto-key` in project mode) while `os dev` kept the key at `$HOME/.objectstack/dev-crypto-key` (or `$OS_HOME`'s). Every secret written during development then read back as unset under `os start`. Both commands now resolve the key file the way `os dev` always has (`os start --home` counts as `OS_HOME`) and hand it to their `serve` child on the internal `OS_INTERNAL_CRYPTO_KEY_FILE` channel. The project-local file an earlier `os start` minted is not orphaned: when it alone holds a key it is still read, and the boot says which file; when it and the shared file hold different keys, the shared one is used and a warning names both paths. An explicit `OS_SECRET_KEY` / `OS_DEV_CRYPTO_KEY` still wins, unchanged. + - **`@objectstack/service-settings`.** New exports `resolvePersistedKeyFile` (the one resolution both commands take, beside the provider's own key-file path) and `KEY_FILE_CHANNEL_ENV`; the local crypto provider's file tier reads the file that channel names, ahead of `OS_HOME`. +- f6f663a: fix(cli): when two data-key files hold different keys, `os start` and `os dev` read with the one that opens the project's stored secrets, or a production boot refuses + + Clause-②: yes (narrowing) — a production `os start` / `os serve` boot whose two candidate key files (the shared `$HOME/.objectstack/dev-crypto-key` or `$OS_HOME`'s, and the project's `.objectstack/dev-crypto-key`) both fail to open the database's newest stored secret now refuses to start, naming both files; it used to start and serve every stored secret as unset. The widening half: `@objectstack/service-settings` gains new exports (`chooseKeyFileByTrialOpen`, `settleKeyFileChannel`, `storedSecretReaderOverEngine`, `deferredLocalCryptoProvider`, `DataKeyOpensNothingError`, `KEY_FILE_CANDIDATE_CHANNEL_ENV` and their types). + Metadata-protocol: none + + + + **BREAKING** boot narrowing (`@objectstack/cli`, `@objectstack/service-settings`): a production boot whose candidate data keys open none of the stored secrets refuses to start. + + - **`@objectstack/cli`.** When the shared key file and the project's own key file (the one an earlier `os start` minted) hold different keys, the CLI no longer picks one. It hands the `serve` child both, and the child decides once the database is open: it opens the newest stored `sys_secret` value under each key and reads with the key that opens it, and the boot says which file it took. A project that only ever ran `os start`, on a machine whose home holds a key from `os dev` in another project, reads its secrets back again. With no stored secret yet, the shared file is used, as before. When neither key opens the stored secrets, a production boot (`os start`, `os serve`) refuses to start and names both files; a development boot (`os dev`) starts and prints a boot error naming both files. The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) now read the same file by the same rules: the project's own file when it alone holds a key, and the key that opens a stored secret when both do. + - **`@objectstack/service-settings`.** The settings service makes that choice in its `start()`, before any reader decrypts, when the CLI names a second candidate on the internal `OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE` channel. A leading `~` in `OS_HOME` (or `OBJECTSTACK_HOME`) is now the home directory when the provider finds its key file, as `os start` already read it; it used to name a directory called `~` under the working directory. + + **The remedy.** If a production boot refuses with "neither data key opens this database's stored secrets", find the key those secrets were sealed under (a backup of either file, or the `OS_SECRET_KEY` the deployment once ran with), keep a copy of both files, and put that key at the shared file the message names, or set `OS_SECRET_KEY` to it. + + Supersedes, in the same release: the note for #29934 says that when the shared file and the project's file hold different keys, the shared one is used and a warning names both paths. After this change the boot uses whichever of the two opens the stored secrets, and refuses in production when neither does. +- 159930a: fix(service-settings): when two data-key files hold different keys, the boot judges the newest stored secrets as a page, not only the newest one, and reports a database split between the two keys + + Clause-②: no — an internal boot heuristic and its notice text. The shapes it changes (`StoredSecretReader` now answers the page, `KeyFileChoice` gains the `mixed` reason and a `tally`, `DataKeyOpensNothingError` gains `judged`) were added by #30042, whose note is still pending, so they first ship together in this release; no released export changes. + Metadata-protocol: none + + - **`@objectstack/service-settings`.** When the shared key file and the project's own key file hold different keys, the boot now trial-opens each of the newest stored `sys_secret` rows (up to ten) under both keys and reads with the key that opens more of them, on a tie the one that opens the newest. A row that opens under neither key (a corrupt or foreign row) no longer decides the boot: if an older row opens under one key, that key is used, and only a page where no row opens under either key keeps the production refusal. When each key opens some of the rows, the database holds secrets under both keys: the boot uses the key that opens more of them and prints an error naming both files, how many rows each key opened, and the fix. The boot's notices now say what was measured ("opens the 3 newest stored secrets", "opens 2 of the 3 newest stored secrets") instead of claiming the chosen file opens all of the database's stored secrets. + - **`@objectstack/cli`.** The one-shot commands (`os migrate`, `os secret rewrap`, `os secret orphans`, `os storage orphans`) share the same trial, so they make the same choice. + + **The remedy for a split database.** Keep a copy of both files, then save again each secret that reads as unset: it is sealed under the key the boot uses. Once nothing reads as unset, put that key at the shared file (or set `OS_SECRET_KEY` to it). + + Supersedes, in the same release: the note for #30042 says the boot opens the newest stored secret under each key and quotes the refusal as "neither data key opens this database's stored secrets". After this change the boot judges the newest rows as a page, and the refusal reads "neither data key opens any of the N newest stored secrets". +- 977be38: fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204) + + Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`. + + - **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it. + - **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged. + - **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels). + - **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will. + + The one-line fix for a host: + + - FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 10a252ba734..bfafc47cde9 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "18.3.0", + "version": "18.4.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 16c9a5cde2c..86946f19a0b 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,45 @@ # @objectstack/service-sms +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/plugin-auth@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index f4fce1b63fa..2c97bc484ab 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "type": "commonjs", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index 2526da9231d..2cb86c38fb5 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,57 @@ # @objectstack/service-storage +## 18.4.0 + +### Patch Changes + +- 59e6463: fix(service-storage)!: a stored file's uploader is the user who created it, as an attachment's and a comment's author are + + Clause-②: no (narrowing) — a non-system create of `sys_file` records the session's user as `owner_id`, whatever the request carries, and one whose session names no signed-in user is refused. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing on `sys_file` creates (`@objectstack/service-storage`), served by every data door (REST, MCP, the console). + + - **Now.** A stored file's uploader is the user who created it, as an attachment's (`sys_attachment.uploaded_by`) and a comment's (`sys_comment.author_id`) author are. A non-system create of `sys_file` records the session's user as `owner_id`, over whatever the request carries. The column was already read-only on update, so the stamp is the row's uploader for life. + - **Refused.** A non-system create of `sys_file` whose session names no signed-in user is refused with 403 `RECORD_NOT_ACCESSIBLE`, the answer an attachment or comment create with no signed-in user already gets: there is no user to record as the uploader. + - **Unchanged.** The upload doors (presigned, chunked, complete) already record the signed-in user under the system context, and keep doing so. System-context writes (seeds, imports, migrations) keep the `owner_id` they supply, as does a programmatic create with no session at all on a bare kernel. An update of `owner_id` is dropped and reported in `droppedFields`, as before. Stored rows keep what they were written with. The console needs no change: it never creates `sys_file` rows through the data API. + + **The remedy.** None for the console or the upload doors. A client that creates a `sys_file` row through the data API gets the row recorded under its own user, whatever `owner_id` it sends; to record a different uploader, write from a system context (a migration or an import). A server-side caller that creates `sys_file` rows with no signed-in user runs as the system principal or names the user it acts for. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [977be38] +- Updated dependencies [eca7cd2] +- Updated dependencies [7b2bb66] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/observability@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index 684c1b4716a..10af8fb5ad4 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "18.3.0", + "version": "18.4.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/skills/CHANGELOG.md b/packages/skills/CHANGELOG.md index 3c1e21191db..01153da9b2e 100644 --- a/packages/skills/CHANGELOG.md +++ b/packages/skills/CHANGELOG.md @@ -1,5 +1,115 @@ # @objectstack/skills +## 18.4.0 + +### Minor Changes + +- d59a1eb: feat(spec): a `page:header` declares `placedActions: true`, the explicit opt-in to draw every action of the bound object placed at `record_header` / `record_more`, read live (#30018) + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`placedActions`** on a `page:header` component's `properties` (`PageHeaderProps`, the `ComponentPropsMap['page:header']` row): `true` asks the header to draw every action of the page's object whose `locations` include `record_header` or `record_more`, read live from the object's metadata, in place of an authored `actions` id list. An action another package places on the object is then drawn without editing the page. Optional with no default: an unset key stays unset in the parsed output. + - **The `actions` list keeps its meaning.** An authored list is the whole set the header draws, and a header with neither `actions` nor `placedActions: true` draws none of the object's actions. `placedActions: false` beside a list is accepted and changes nothing. + - **Refused together.** `placedActions: true` beside an authored `actions` list, an empty one included, is refused at `properties.placedActions` by `os validate`, `os build`, `os lint` and the page save door (`component-props-invalid`, an error). The message names both keys and asks the author to keep one: `actions` for a curated list that is the whole set, or `placedActions: true` for every placed action. Nothing that parsed before is refused: until this release `placedActions` was an unknown key on the closed shape. + - **Declared ahead of its reader.** The console's header renderer does not read the key yet, so a header that sets it draws what the same header without it draws (none of the object's actions unless `actions` names them). The key's describe opens with `[PLANNED — not yet rendered]` until the renderer reads it. + - The published JSON Schema `ui/PageHeaderProps` still accepts the pair: the rule depends on the value `true`, which no projected refinement pattern states, so the parse refuses it and `dropped-refinements.baseline.json` records the site. + - `@objectstack/skills`: `objectstack-ui` `rules/pages.md` names the key in the component catalogue, marked not drawn yet, and drops a refused-shape clause from the print-page paragraph that the sentence before it already states. + +### Patch Changes + +- bc9eaf6: docs(skills): the automation skill's OAuth recipe teaches the installer-supplied client + + Clause-②: no + Metadata-protocol: none + + Recipe 6 of `objectstack-automation/references/integration-recipes.md` now shows `clientSource: 'installer'` beside a declared `clientId` + `credentialRef`, with when to use which: a declared client for an app its publisher deploys, the installer form for one installed on deployments the publisher never sees. The administrator registers a client with the vendor against the `redirectUri` the connection's status read answers, connects with `{ clientId, clientSecret }` (no secret for a public client; a reconnect naming no client keeps the stored one), and the connection reads back `client: { source: 'installer', clientId, hasSecret }`, never the secret. +- 0d40aa1: docs(skills): with no `searchableFields` declared, a field marked `searchable: true` joins the default search set + + Clause-②: no + Metadata-protocol: none + + - **Before.** `objectstack-data`, `objectstack-query` and the `objectstack-ui` list-views rule taught the record-search auto-default as type-only: the name field plus the text-like and `select` columns. A field marked `searchable: true` was described as doing nothing, and a `$searchFields` override naming one as refused. + - **Now.** Each says what the platform resolves: with no `searchableFields` declared, the set `search` scans, and the set a `$searchFields` override may name, is the type-based default plus every field marked `searchable: true`, of any type except `secret`, `password`, `vector` or a virtual `formula`. Each also says what does not change: `searchable: false`, the default, never removes a field the type default scans, and declaring `searchableFields` chooses the set. The enum half of the type default is spelled `select` alone, as the resolver has it. + - No schema, key or behaviour changes. +- 89803c5: docs(skills): the platform skill teaches the `contributions` member of the package customizations read + + Clause-②: no + Metadata-protocol: none + + `skills/objectstack-platform/SKILL.md` now names `contributions` beside the overrides, extensions and switched-off items that `GET /packages//customizations` lists: one entry per item another package adds to the base, stamped with its `packageId`; a `field` or `flow` entry names the base's `object` it is on, a `nav` entry names the base's `app`, its `group` when it targets one, and the item id as `name`; `counts.contributions` counts items, not packages. The text is paid for by deleting three restatements in the same file, so the token ceiling is unchanged. +- 322b6bf: docs(skills): objectstack-automation says a flow's record nodes treat a computed filter operand as a value, never as a query operator + + Clause-②: no + Metadata-protocol: none + + - **Now.** § Filter operands states the rule from the service-automation change: a computed operand is a string, number, bigint, boolean or date, or at an `$in` / `$nin` / `$between` comparand a list of them with no empty entry. Anything else fails the node as a guard before the query runs. The fix: make the expression evaluate to the compared value (for a record, its `id`), and write conditions in the filter itself. The table of failures a `fault` edge does not route now names that refusal beside the `null` operand. + - `SKILL.md` stays under its token ceiling (no ceiling moves). No schema, key, command or behaviour changes. +- dd9add6: docs(skills): objectstack-platform says the D11 close refuses a package-less write only where the row moves as a declared override, and an added email-template locale keeps that write + + Clause-②: no + Metadata-protocol: none + + - **Before.** The ADR-0146 customization paragraph said a pre-D11 unbound Setup row's package-less save, reset and rollback are refused once it is movable. That is false for a row of an email-template locale the base does not ship: `os migrate packageless-overlays` moves it as the pair's own member (`addedMember`, no `overrides`), and the close judges only the declared-override move, so that row's package-less write stays open before and after its move. The clause also left out publish, which the close refuses too. + - **Now.** The clause says the close covers a row movable as a declared override, lists save, publish, reset and rollback, and says an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write. The file stays at its token ceiling: a duplicate "CLI Commands" pointer section is cut, its home the Part 3 pointer to `references/operations.md`. No rule is removed; no schema or behaviour changes. +- 987ff33: docs(skills): teach that a save and `os validate` refuse `titleFormat` + + Clause-②: no + Metadata-protocol: none + + - `objectstack-data` (object properties table) and `objectstack-formula` (template carriers) no longer call `titleFormat` deprecated: since #29499 the metadata save gate and `os validate` refuse an object that declares it (`title-format-retired`), and it moves to `nameField`. The formula skill drops the sentence that restated the template grammar cell beside it. + - ADR-0079 D3 gains a dated amendment note citing ruling 6574393684 and #29499; the decision is unchanged. +- cde9a80: docs(skills): the automation skill teaches that an approval's decision outputs are enforced at the decision + + Clause-②: no + Metadata-protocol: none + + - **Before.** `skills/objectstack-automation/references/state-machines-and-approvals.md` said only that a typed `decisionOutputs` entry renders the matching record picker in the decision dialog (`multiple` collects an id array). An agent writing an approval flow from it could read `type` and `multiple` as widget hints and not know that a value the next step cannot use is refused. + - **Now.** The *Recording a decision* section says what the approvals service enforces, on approve and reject alike and before any write: a picker value must resolve through the `expression` approver's resolvers (`user` by any account's record id, `department` by record id in the request's organization, `team` by record id not owned by another organization, `position` by the machine name of a position someone in that organization holds); `multiple: true` takes a list of distinct values for every kind, bare keys included, and without it one value; any other non-blank value is refused with `400 VALIDATION_FAILED` naming `outputs.KEY`. The Console sends a list for a `multiple: true` text output. The existing `lead_review` node is the example. The file stays within its token ceiling (5724 of 5730). +- 6a4737a: docs(skills): objectstack-data teaches `relatedList` placement by each value's role, not by record-page tabs or columns + + Clause-②: no + Metadata-protocol: none + + - **Before.** `rules/relationships.md` said `relatedList: 'primary'` draws "a full-width table in the details column after the first field section (in the Details tab, no tab of its own)" and that `true` / omitted is "a side card (the shared **Related** tab)". The spec's `relatedList` describe no longer names any tab or column, so the skill taught a page layout the record page does not draw. + - **Now.** The three bullets follow the spec describe: `'primary'` is part of the record's main content, a full-width table in the record's detail body (still the default for an inline-edited `master_detail` FK); `false` means the child object is not shown on the parent record page; `true` or omitted is an auxiliary panel on the record page. Layout stays ADR-0085's to define. + - The file gets smaller and stays under its token ceiling; no ceiling moves, no schema, key or behaviour changes. +- 7bafab3: docs(skills): `objectstack-ui` no longer says `page:header`'s `placedActions: true` is not drawn, and an eval exercises it (#29951) + + Clause-②: no + Metadata-protocol: none + + - `rules/pages.md`: the component catalogue's `page:header` row says `placedActions: true` draws every placed action, read live. The console's header renderer reads the key since #29951's ui half, so the "not drawn yet" qualifier #30018 wrote is dropped. + - `evals/record-page.json`: a third case asks for a custom full record page whose header shows every action packages place on the object; the expected answer keeps the page and its tab strip and sets `placedActions: true` on the `page:header` node, with no `actions` id list beside it. +- b83e1d0: test(skills): the platform skill's evals ask for push notifications on an `os serve` deployment + + Clause-②: no + Metadata-protocol: none + + `skills/objectstack-platform/evals/config-plugins-ops.json` gains case 12: the prompt asks to turn on push notifications for an `os serve` deployment, and the expected answer declares `requires: ['push']` with a top-level `push:` key on the stack definition and writes no secret inline (the APNs signing key and the Firebase service-account JSON are named by reference). The assertions stay with what `objectstack-platform/SKILL.md` teaches today, the `push` token and the top-level key; the push block's transport and vendor shape are not asserted until the skill teaches them. +- 0ce44c9: docs(skills): the i18n skill lists a lookup or master-detail field's translatable `relatedListTitle` + + Clause-②: no + Metadata-protocol: none + + - **Before.** The `objectstack-i18n` object-level table listed `label`, `help`, `placeholder`, `options` and `inlineTitle` under `fields.{field_name}`, so an author had no word that the related-list title on a parent's record page is translatable. + - **Now.** The row adds `relatedListTitle`: the title of the related list a `lookup` or `master_detail` field draws on its parent's record page. The key sits on the child object's field, and an untranslated title keeps its authored text, never the child's label. + - The paragraph under *Diff & Coverage Schemas* saying `getCoverage()` / `suggestTranslations()` have no shipped implementation is cut; *Service Contract* in the same file says the same, and the section's opening sentence now names the two schemas as those methods' return shapes. No schema, key or behaviour changes. +- f9e5bef: feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. + - **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. + - **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. +- 07595c0: docs(skills): the `page:header` title row drops the retired `breadcrumb` and names the live `placedActions: true` + + Clause-②: no + Metadata-protocol: none + + - `skills/objectui/guides/page-builder.md`: the `page:header` sentence named `breadcrumb` among the title row's keys. `PageHeaderProps.breadcrumb` is a `retiredKey` tombstone since @objectstack/spec 17 (ADR-0087 D2), and the props lint refuses an authored one. The row now lists `title`, `subtitle`, `actions` or `placedActions: true`; the last two are refused together. + ## 18.3.0 ### Minor Changes diff --git a/packages/skills/package.json b/packages/skills/package.json index d49716a1e89..0645d23481e 100644 --- a/packages/skills/package.json +++ b/packages/skills/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/skills", - "version": "18.3.0", + "version": "18.4.0", "description": "The published ObjectStack skills catalog \u2014 the repository's skills/** tree, shipped at the version of the @objectstack/* packages it teaches.", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/skills", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 33c8e59ce24..674e8d34455 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,360 @@ # @objectstack/spec +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. +- 140d8ce: fix(spec,metadata-protocol,mcp)!: a field marked `sortable: false` is no longer offered or performed as a sort — the sortability projection reports it unsortable, and the Data API and the stdio MCP `query_records` refuse a sort on it with `400 INVALID_SORT` (#29896) + + Clause-②: yes (narrowing) — `GET /api/v1/data/:object` (and every request that reaches the same list door: `POST /api/v1/data/:object/query`, the export route, the RPC dispatcher, the record-page `related[].sort`) and the stdio MCP `query_records` refuse a sort on a field whose author declared `sortable: false`, which they used to answer sorted by the column; a stored list view `sort` or `record:related_list` `sort` naming such a field is now refused when it is read. The widening half: `@objectstack/spec/api` exports `FIELD_UNSORTABLE_AUTHOR_DECLARED`, and `FieldSortabilitySchema.reason` accepts `'author-declared'` beside `'virtual-type'`. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field" and `INVALID_SORT` as "the field must exist and have `sortable: true`", which is what the platform now does. The prescription ships in the refusal text and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the Data API list door (`@objectstack/metadata-protocol`) and the stdio MCP `query_records` (`@objectstack/mcp`): a sort on a field marked `sortable: false` is refused. + + - **Before.** An author who marked a field `sortable: false` (a long text, an unindexed column on a large table) got nothing for it. The sortability projection served on `GET /api/v1/meta/object/NAME` reported the field `{ sortable: true }`, so the list header and the Sort picker offered it, `GET /api/v1/data/NAME?sort=FIELD` answered 200 sorted by it, and so did the stdio MCP `query_records` with that `orderBy`. + - **Now.** The projection reports `{ sortable: false, reason: 'author-declared' }` for that field, so the grid header and the Sort picker, which read the projection, stop offering it. The Data API judges the sort by the same projection and refuses it with `400 INVALID_SORT` before any read, the answer a sort on a `formula` field already gets; the stdio MCP `query_records` refuses it the same way before the engine runs. Only the literal `false` counts: a field with no `sortable` key, or with `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its `reason: 'virtual-type'` answer. + - **Stored sorts.** A list view whose `sort` names such a field, or a page `record:related_list` block whose `sort` does, used to load sorted and is now refused when it is read. Lint does not yet judge these at authoring. The upgrade-guide entry `field-sortable-false-sort-refused` lists them. + - **Not changed.** In-process callers that reach `ObjectQL.find` directly (hooks, flows) keep sorting by the stored column: the engine door judges only whether a column is stored, and `sortable: false` governs what the platform offers callers, not what its storage can do. + + **The remedy.** Drop the sort on that field or sort by another one, in requests and in stored view and related-list sorts. An author who wants the field sortable removes `sortable: false` from it. +- 699ebd8: fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares, instead of storing it and then refusing every write to the field (#29898) + + Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an `object` or `object_extension` whose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: no move — `skills/**` teaches no picklist reference verdict and `content/docs/**` carries no severity table for it; the prescription ships in the refusal text, the FROM → TO below and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at the metadata save and publish doors: `PUT /api/v1/meta/object/NAME` and `PUT /api/v1/meta/object_extension/NAME` (draft and live), `POST /api/v1/packages/PKG/publish-drafts`, the per-item publish, and the rollback / revert restore doors. + + - **Before.** A select (or multiselect) field with `picklist: 'nope_list'`, a name no loaded package declares, saved `200` with its draft reading `_diagnostics.valid: true`, and `publish-drafts` answered `outcome: published`. The field was then served with no options, and every create or update that set it answered `400 VALIDATION_FAILED` ("takes its values from picklist \"nope_list\", which no loaded package declares"). The boot and a late install already refuse the same shape for a package's own objects, and `os validate` / `os build` refuse it for a stack's own fields. + - **Now.** The doors refuse it `422 INVALID_METADATA`, one finding per unknown binding at `fields.FIELD.picklist`, naming the field and the list. A list is declared when the engine's registry resolves it, the same read that serves the field's options: the package's own, a dependency's, the base package's of a `.custom` pair, or any other package installed here. + - **Unchanged.** A binding the object already holds is not judged again on a re-save, so an object published before this release keeps saving; the stored-metadata healing pass (`os migrate meta --stored`) is not judged. A draft stored before this release is judged when it is published. + - **`@objectstack/spec`.** The upgrade guide entry `picklist-binding-unknown-refused` (a semantic migration: no automatic rewrite, because whether the author meant another list, one still to be declared or inline options is a judgement about the field). + - **`@objectstack/cli`.** A source comment in the `os validate` picklist judge no longer says the runtime write path is unjudged. + + **FROM → TO.** `fields.t: { type: 'select', picklist: 'nope_list' }` → `picklist: 'NAME'` of a list a package installed here declares, or declare `nope_list` in a code package (a `*.picklist.ts` file, or `defineStack({ picklists })`), or `options: [...]` in place of `picklist`. +- 4c2cd29: **Shipped in 18.3.0**, whose CHANGELOG missed this entry; listed here one release late. + + fix(lint)!: publish refuses a validation rule (or another fail-closed predicate) that orders a text or boolean field against a number, instead of warning that it "evaluates to null" and then refusing every write (#29904) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the object save door and the package publish) refuses a `type-mismatch` it used to pass with a warning, at the predicate sites that fail closed. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate`, the object save door and `POST /api/v1/packages/:id/publish-drafts`. + + - **Before.** `record.name > 5` on a text field, written as a validation rule's `condition`, published with one `expression-invalid` advisory at `severity: warning` saying the expression "silently evaluates to null". A validation rule fails closed, so from then on every create and update whose `name` was not numeric text was refused: `400 VALIDATION_FAILED` "Validation rule 'name_gt' could not be evaluated (runtime: no such overload: dyn > int) — write rejected". + - **Now.** The same rule is refused at publish as `expression-invalid` at `severity: error`, at the rule's `condition` path, with a message that says the write would be refused and how to compare like with like. This is the severity the nullable-comparison verdict (`record.qty > 1000` on a nullable number) already had at the same slot. + - **Which sites.** Those whose runtime refuses the write (or aborts the operation) when the expression faults: a validation rule's `condition` and `when`, nested `then` / `otherwise` rules included; a field's `requiredWhen` and `readonlyWhen`; an option's `visibleWhen` (a write that picks the option); a hook's `condition`. The `validate_expression` MCP tool answers the same for `site: 'validation'`. + - **Unchanged.** A formula field's `expression` keeps the warning: there the fault reads null, which is what the warning says. So do flow conditions (the run fails, the write that fired it lands), a field's display `visibleWhen` / `settledWhen`, field group and action predicates, and `conditionalFormatting`. Sharing-rule conditions compile to a query filter and are not judged by this verdict. + - **`@objectstack/spec`.** The upgrade guide entry `validation-type-mismatch-refused` (a semantic migration: no automatic rewrite, because whether the author meant the length, a number stored as text or another field is a judgement about the rule). + - **`@objectstack/formula`.** `ExprSchemaHint` gains `faultOutcome?: 'fail-closed'`, which the caller sets for a fail-closed site; the `type-mismatch` refusal's `params` carry `outcome: 'fail-closed'` when it is graded an error there. + + **FROM → TO.** `condition: 'record.name > 5'` on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text (it faults, and refuses the write, on any other). A boolean: `record.active > 0` → `record.active == true`. +- 4be8a2e: feat(spec)!: retire `emailAndPassword.revokeSessionsOnPasswordReset` — a completed password reset always ends every session the account holds, and there is no setting (#29909) + + Clause-②: no (narrowing) — `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` becomes a `retiredKey()` tombstone, so the schema parse and `tsc` refuse it at any value, and plugin-auth's entry guard refuses it at any value too, `true` included, which it accepted before. Nothing is widened. + Metadata-protocol: narrowed + + + + **BREAKING** for a configuration that still writes `emailAndPassword.revokeSessionsOnPasswordReset`, at any value. + + - **`@objectstack/spec`.** `EmailAndPasswordConfig.revokeSessionsOnPasswordReset` is retired (ADR-0049 enforce-or-remove). A completed self-service password reset already ended every session the account holds, on every reset door, and an explicit `false` was already refused at boot, so the key could hold one value only. It is now a tombstone: a typed configuration that writes it fails `tsc`, and `AuthConfigSchema` / `EmailAndPasswordConfigSchema` refuse it with the prescription. The authorable-surface row reads `[RETIRED]`, `RETIRED_KEYS_BY_MAJOR[18]` lists `system/EmailAndPasswordConfig:revokeSessionsOnPasswordReset`, and the upgrade guide carries the semantic entry `email-and-password-revoke-sessions-on-reset-retired`. The prescription is exported as `REVOKE_SESSIONS_ON_PASSWORD_RESET_RETIRED` (`@objectstack/spec/system`). No ADR-0087 conversion: the key lives in the configuration a host hands the auth plugin, never in a stack or a stored row, so `os migrate meta` has nothing to rewrite. + - **`@objectstack/plugin-auth`.** The auth manager takes its configuration unparsed, so it refuses the retired key itself, where the configuration enters: at construction, which stops the boot, and on a configuration patch, where the standing configuration keeps ruling. It now refuses `true` as well as `false`, with the tombstone's own prescription. `true` asked for what the platform does anyway; the key is to be deleted either way. The auth instance still sets better-auth's own option of that name itself, so a completed reset ends the account's sessions exactly as before. + - **Unchanged.** What a password reset does: every self-service reset door ends every session the account holds, and each ended session is stamped `revoke_reason: password_reset`. An absent key is accepted, as before. + + **The remedy.** Delete `revokeSessionsOnPasswordReset` from the `emailAndPassword` block of the auth configuration, whatever it holds; nothing replaces it. +- 9037b13: fix(lint)!: a text-vs-number comparison in a record-triggered flow condition is refused with what really happens (the run fails, the write lands), and in an advisory validation rule the refusal no longer says the write is refused (#29926) + + Clause-②: no (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses a `type-mismatch` in a record-triggered flow's start or edge condition that it used to pass with a warning; and the fail-closed arm of the verdict is served under its own code `type-mismatch-fail-closed` instead of `type-mismatch` with `params.outcome`. Added beside them: the codes `type-mismatch-run-fails` and `type-mismatch-fail-open`, and the exported type `ExprFaultOutcome`. + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names each outcome) and the MCP skill text in `packages/mcp/src/skill-md.ts` (`flow_condition` now errors too); `skills/**` teaches no text-vs-number comparison in a flow condition or an advisory rule, so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flow conditions — before.** `name > 5` on a text field, as the start node's `condition` or an edge `condition` of a flow whose start `triggerType` is `record-*`, published with an `expression-invalid` warning saying the expression "silently evaluates to null". It does not: the engine's `evaluateCondition` throws on the fault, so the run fails at that condition, and the record-change trigger isolates the write that fired it, so the write lands and the flow's work is not done. + - **Now.** The same condition is refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with a message saying the run fails at the condition and the triggering write still lands. This is the severity and outcome the nullable-comparison verdict already has on those conditions (#29026). A flow that is not record-triggered, and a node `condition` other than the start node's (inert), keep the warning. The `validate_expression` MCP tool answers the same for `site: 'flow_condition'`. + - **Advisory validation rules** (`severity: 'warning'` / `'info'`). Still refused at publish, as the nullable twin refuses them, but under `type-mismatch-fail-open` with a message that says what happens: the rule is skipped, the write proceeds, and the rule never gives its advisory. The nullable twin's message on the same rule now says the same instead of "the write is rejected fail-closed". A `severity: 'error'` rule keeps both refusals and their wording. + - **Refusal codes (`@objectstack/formula`).** A code names one message template, so each outcome has its own: `type-mismatch` (warning, reads null), `type-mismatch-fail-closed`, `type-mismatch-run-fails`, `type-mismatch-fail-open` (all errors). `ExprSchemaHint.faultOutcome` takes `'fail-closed' | 'run-fails' | 'fail-open'` (exported as `ExprFaultOutcome`); the `outcome` param on `type-mismatch` is gone. + - **`@objectstack/spec`.** Upgrade guide entry `flow-condition-type-mismatch-refused` (semantic: no mechanical rewrite). The `validation-type-mismatch-refused` entry's acceptance criteria now say a create answers 201, the data door's status. + + **FROM → TO.** Flow condition `name > 5` on a text field → `size(name) > 5` for its length, `score > 5` on a number field for a value, or `double(name) > 5` only when every value is numeric text. A boolean: `active > 0` → `active == true`. A consumer keyed to the code: `type-mismatch` with `params.outcome === 'fail-closed'` → `type-mismatch-fail-closed`. + + **The renamed code shipped in 18.3.0.** `@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` already serve #29904's fail-closed refusal as `type-mismatch` with `params.outcome: 'fail-closed'` (the `outcome` param is in the 18.3.0 `ExpressionRefusalParams` type), although 18.3.0's changelog has no line for it: its changeset (`29904-type-mismatch-fail-closed.md`) was not consumed by that release. This release renames that arm `type-mismatch-fail-closed` and removes the `outcome` param, so a consumer keyed to 18.3.0's `type-mismatch` + `outcome` moves to the new code (FROM → TO above). The same changeset also says flow conditions keep the warning; a record-triggered flow's start and edge conditions are now refused, as above. +- e3b56b2: feat(spec): a lookup or master-detail field's related-list title is translatable + + Clause-②: yes + Metadata-protocol: added + + A related list the record page derives from a lookup or master-detail field is titled by the field's `relatedListTitle`, or by the child object's label when it has none. That title had no translation slot, so a zh-CN reader saw it in the authoring language, and `os i18n check` could not see the gap. A lookup from an object back to itself was the plainest case: with no title of its own, a contract's record page listed its renewals under 「合同」, the same word as the contract beside it. + + - `FieldTranslationSchema` declares `relatedListTitle`: `objects..fields..relatedListTitle`, on the child object whose field declares the title. `translateObject` overlays it on the served child object (`GET /meta/object`, and the app bundle's object members), which is what the console derives the related list's title from, on the side card and on the body table alike. + - It follows the packaged-base rule (ADR-0029 D9.2a): a title an org re-authored on top of the packaged declaration keeps its text. An authored `relatedListTitle` with no entry keeps its authored text; the child object's label is never substituted for it. + - `os i18n extract` scaffolds the key and `os i18n check` counts it under the field bucket. A field without a `relatedListTitle` is recorded without being demanded. + - The object save door puts a served (translated) `relatedListTitle` back to its source text, as it does for the field's other translated slots. +- e8afd1e: fix(lint)!: every flow condition's text-vs-number comparison gets a verdict that says what the run does — a record-triggered flow's `record.` read and `decision` branch are refused like its bare name, a time-relative sweep of the flow's object is refused like a record-triggered flow, a bare name a flow variable holds is no longer typed as the field, and any other flow's warning says the run fails instead of "evaluates to null" (#29959) + + Clause-②: yes (narrowing) — the author-time gate (`os build`, `os validate`, the package publish and the flow publish door) refuses, on a record-triggered flow, a `type-mismatch` in a `decision` branch and in a `record.` / `previous.` read of a start or edge condition, which it passed with no verdict before; refuses the same on a time-relative flow sweeping the object its conditions are typed against, where 18.3.0 served a warning; and a flow condition's `type-mismatch` warning is served under its own code `type-mismatch-run-may-fail` instead of `type-mismatch`. Added beside them (the widening half): the optional `severity` and `triggerKind` inputs of the `validate_expression` MCP tool, the code `type-mismatch-run-may-fail` in `@objectstack/formula`'s `ExpressionRefusalCode`, the optional `flowVariables` key of `@objectstack/formula`'s `ExprSchemaHint`, and a bare flow-condition name that a declared flow variable or node id holds is no longer typed as the field it shadows (no verdict where 18.3.0 served a `type-mismatch` warning). + Metadata-protocol: narrowed + Skills-docs: updated here — `content/docs/data-modeling/formulas.mdx` (the type-soundness row names the decision branch, the `record.` read and the warning on other flows) and the MCP skill text in `packages/mcp/src/skill-md.ts` (the two new `validate_expression` inputs); `skills/**` teaches no text-vs-number comparison in a flow condition (grep for `type mismatch`, `evaluates to null`, `validate_expression` and the decision examples), so no skill or eval moves. + + + + **BREAKING** accept-set narrowing at the author-time expression gate, served by `os build`, `os validate` and `POST /api/v1/packages/:id/publish-drafts`. + + - **Record-triggered flows — before.** The start and edge conditions refused `name > 5` on a text field (`type-mismatch-run-fails`), but `record.name > 5` in the same slot, and `name > 5` in a `decision` node's `config.conditions[].expression`, published with no verdict at all. Both fail the run the same way: the engine's `evaluateCondition` throws on the fault, the run binds the trigger record both as `record` and flattened to bare names, and a decision branch is evaluated over the same variables as an edge. + - **Now.** Both are refused as `expression-invalid` at `severity: error`, code `type-mismatch-run-fails`, with the message the bare name already gets: the run fails at the condition and the triggering write still lands. `previous.` is judged the same way. The date verdicts stay on the bare name only, as before. + - **Time-relative flows.** A sweep launches each run with the swept row as its record, so on a flow whose `timeRelative.object` is the start node's `objectName` (or is omitted) the start, edge and `decision` conditions get the record-triggered verdict above, bare name included: `type-mismatch-run-fails`, where 18.3.0 served a `type-mismatch` warning. A sweep of another object keeps the warning below. + - **Any other flow** (no `triggerType`, a `schedule` flow, an `api` flow) and an inert node `condition` keep a warning. Its text said the expression "silently evaluates to null", which is false on every flow: a condition that faults fails the run. It is now `type-mismatch-run-may-fail`, worded for the failed run, and a warning because such a flow is handed whatever its entrance carries (an API request body, an action's partial record, a parent flow's record, or nothing), so the field's type is not proven to be what the name holds. A `record.` read and a `decision` branch there get the same warning. The flow registration log line reads the same text. + - **A bare name a flow variable holds.** The run seeds a flow's declared variables (and each node's outputs, under its id) before it flattens the record, and the flattening skips a name already bound. So in a flow that declares `variables: [{ name: 'title', type: 'number' }]` over a text field `title`, a bare `title > 5` reads the number and runs; it was refused as `type-mismatch-run-fails` on the head that added that error, and warned on 18.3.0. It now gets no type verdict (the `flow-variable-shadows-field` warning still names the shadowing). `record.title > 5` reads the field and stays refused. + - **`validate_expression` (MCP).** Two optional inputs, so the tool answers what the gate will: `severity` (`error` | `warning` | `info`, site `validation`; default `error`) gives an advisory rule the gate's `type-mismatch-fail-open` instead of the fail-closed verdict, and `triggerKind` (`record_change` | `time_relative` | `schedule` | `api` | `none`, site `flow_condition`; default `record_change`) gives a `schedule`, `api` or `none` flow the gate's warning instead of the run-fails error (`time_relative` answers run-fails, its record being a row of `objectName`). Either input on a site it does not describe is refused as a tool error. A call that passes neither answers as before. + - **`@objectstack/spec`.** Upgrade guide entry `flow-record-read-and-decision-type-mismatch-refused` (semantic: no mechanical rewrite). + + **FROM → TO.** In a record-triggered flow, or a time-relative flow sweeping its own object: `record.name > 5` (or a decision branch `name > 5`) on a text field → `size(record.name) > 5` for its length, `record.score > 5` on a number field for a value, or `double(record.name) > 5` only when every value is numeric text. A boolean: `record.active > 0` → `record.active == true`. A consumer keyed to the code of a flow condition's warning: `type-mismatch` → `type-mismatch-run-may-fail` (`@objectstack/formula@18.3.0` and `@objectstack/mcp@18.3.0` serve `type-mismatch` there; the record-scoped `type-mismatch`, on a formula field or a display predicate, is unchanged). +- 217ba78: feat(spec): an `api` flow's start node declares a third-party sender's signature scheme (`config.signature`), keyed by `config.secretRef` (#29983) + + Clause-②: yes (widening) + Metadata-protocol: added + + + + An inbound `api` hook admitted a post only by ObjectStack's own `x-objectstack-signature`, so a vendor that signs its callbacks its own way (DocuSign Connect, a Stripe-style `t=…,v1=…` header, a GitHub-style `sha256=` header) could not be admitted at all. The start node's `config` now carries `signature`, which declares how such a sender signs: + + - `signs: 'body'`: the raw body alone, the whole header value (after an optional `prefix`) is the digest. DocuSign Connect: `{ signs: 'body', header: 'X-DocuSign-Signature-1', algorithm: 'hmac-sha256', encoding: 'base64' }`. + - `signs: 'timestamp.body'`: `T.RAW_BODY`, the header being `NAME=value` elements whose `timestampElement` carries `T` and whose `digestElement` carries the digest. Stripe-style: `{ signs: 'timestamp.body', header: 'Stripe-Signature', algorithm: 'hmac-sha256', encoding: 'hex', timestampElement: 't', digestElement: 'v1' }`. + + The key is the start node's `config.secretRef`, as for an ObjectStack-signed hook. Declared, the scheme REPLACES the `x-objectstack-signature` check for that hook (one verifier per hook, never optional). The flow parse refuses a `signature` with no `secretRef`, one beside a literal `secret`, an algorithm other than `hmac-sha256`, an encoding other than `hex` or `base64`, a `timestamp.body` scheme with no `timestampElement` (or no `digestElement`, or the same name for both), a body-only scheme that names a timestamp element, a `header` naming `x-objectstack-signature` or another reserved header (`authorization`, `proxy-authorization`, `cookie`, `host`, `content-length`, `transfer-encoding`, `x-idempotency-key`) in any case, and one on a flow that binds no `api` trigger. + + New exports from `@objectstack/spec/automation`: `ApiTriggerSignatureSchema`, `ApiTriggerSignature`, its two arms `ApiTriggerBodySignatureSchema` / `ApiTriggerBodySignature` and `ApiTriggerTimestampedSignatureSchema` / `ApiTriggerTimestampedSignature`, `API_TRIGGER_SIGNATURE_KEY`, `API_TRIGGER_SIGNATURE_RESERVED_HEADERS`, `apiTriggerVerifierOf` (the one reader of which verifier a hook has: `objectstack-signature`, `declared` or `unusable`), `ApiTriggerVerifier`, `apiTriggerSignatureRefusals` and `ApiTriggerSignatureRefusal`. The `ITriggerApiService.handleInboundHook` contract text states what the implementer owes: a constant-time compare of the decoded 32 bytes, the same 300-second window as `x-objectstack-signature` for a scheme that signs a timestamp, no replay window for a body-only scheme, a repeated header read as its one comma-joined value, and the one `401 INVALID_SIGNATURE` for every failure, an unusable declaration included. + + This release declares the key; the `api` trigger does not verify it yet. Until it does, a hook that declares `signature` is still verified by `x-objectstack-signature` alone, so every post its vendor sends is refused `401`, and none is admitted unverified. +- d59a1eb: feat(spec): a `page:header` declares `placedActions: true`, the explicit opt-in to draw every action of the bound object placed at `record_header` / `record_more`, read live (#30018) + + Clause-②: yes (widening) + Metadata-protocol: added + + - **`placedActions`** on a `page:header` component's `properties` (`PageHeaderProps`, the `ComponentPropsMap['page:header']` row): `true` asks the header to draw every action of the page's object whose `locations` include `record_header` or `record_more`, read live from the object's metadata, in place of an authored `actions` id list. An action another package places on the object is then drawn without editing the page. Optional with no default: an unset key stays unset in the parsed output. + - **The `actions` list keeps its meaning.** An authored list is the whole set the header draws, and a header with neither `actions` nor `placedActions: true` draws none of the object's actions. `placedActions: false` beside a list is accepted and changes nothing. + - **Refused together.** `placedActions: true` beside an authored `actions` list, an empty one included, is refused at `properties.placedActions` by `os validate`, `os build`, `os lint` and the page save door (`component-props-invalid`, an error). The message names both keys and asks the author to keep one: `actions` for a curated list that is the whole set, or `placedActions: true` for every placed action. Nothing that parsed before is refused: until this release `placedActions` was an unknown key on the closed shape. + - **Declared ahead of its reader.** The console's header renderer does not read the key yet, so a header that sets it draws what the same header without it draws (none of the object's actions unless `actions` names them). The key's describe opens with `[PLANNED — not yet rendered]` until the renderer reads it. + - The published JSON Schema `ui/PageHeaderProps` still accepts the pair: the rule depends on the value `true`, which no projected refinement pattern states, so the parse refuses it and `dropped-refinements.baseline.json` records the site. + - `@objectstack/skills`: `objectstack-ui` `rules/pages.md` names the key in the component catalogue, marked not drawn yet, and drops a refused-shape clause from the print-page paragraph that the sentence before it already states. +- 3a27208: fix(lint)!: a stored sort naming a field that declares `sortable: false` is refused by `os validate` and at publish, judged by the same sortability projection the Data API list door refuses it by — on a list view and on every stored page sort (#30023) + + Clause-②: yes (narrowing) — `sort-field-unsortable` (error) now also fires on a sort naming a field whose author declared `sortable: false`, on the list-view rungs it already walked and on every stored page sort it did not walk before (a block's own `sort`, every element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort`, an app page's `record.sort`, where a `formula` sort is now refused too), so `os validate` / `os build` / `os lint` and the `page` and `view` publish doors refuse metadata they used to accept. The widening half: `@objectstack/lint` exports `indexObjectSortability` and the `SortabilityFields` type, and `checkSortDeclaration` takes an optional trailing `sortability` index. + Metadata-protocol: narrowed + Skills-docs: none — `skills/**` names no `sortable` field key and no page or view `sort` rule; `content/docs/**` already teaches `sortable: false` as "do not allow sorting by this field". The prescription ships in the finding's fix line, `os explain sort-field-unsortable` and the upgrade-guide entry. + + + + **BREAKING** accept-set narrowing at authoring (`@objectstack/lint`): a stored sort the Data API list door refuses is refused before it is stored. + + - **Before.** A list view whose `sort` named a field marked `sortable: false` passed `os validate` and the `PUT /api/v1/meta/view` door, then answered `400 INVALID_SORT` on its first fetch and on every fetch after it. No page sort was judged for sortability at all: a `record:related_list`, `record:line_items`, `list-view` or `object-*` block sorting by such a field (or by a `formula` field), an element `dataSource.sort`, a related list's `parentDataSource.sort`, a list page's `interfaceConfig.sort` and an app page's `record.sort` all saved green and failed at read. + - **Now.** `validateSortableFields` judges each of those positions by `resolveObjectSortability`, the projection the list door refuses by, and reports `sort-field-unsortable` at `error` with the door's own clause ("a field on object X that declares `sortable: false`") at the slot's path. The reference-integrity suite now runs on a `page` write for this one member, so the Studio, REST `/meta` and MCP page doors give the same refusal (422). Only the literal `false` counts: a field with no `sortable` key, or `sortable: true`, sorts as before. A `formula` field also marked `sortable: false` keeps its virtual-field verdict. Existence on page sorts stays `page-field-unknown`. + - **Not changed.** A stored view or page already carrying such a sort keeps loading and serving; only its next save is judged, and its first fetch still answers 400 as it did since the list door started refusing it. + + **The remedy.** Sort by another field, or remove `sortable: false` from the field to make it sortable. + + **Supersedes, in the same release.** The `@objectstack/spec` / `@objectstack/metadata-protocol` / `@objectstack/mcp` note for #29896 says "Lint does not yet judge these at authoring." From this release it does: `os validate` and the publish doors refuse every stored sort that note lists, and the stored page sorts it did not list. +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- fc869c2: feat(spec)!: a seed is create-only by default — an omitted `mode` is `ignore`, so a restart, an install or an upgrade never writes a seeded value back over a customer's edit (#30081) + + Clause-②: yes (narrowing) — the declared default of `Seed.mode` (and of `SeedLoaderConfig.defaultMode`, now the same field) moves from `upsert` to `ignore`. The accept set is unchanged and every authored `mode` parses exactly as before; what narrows is what a replay does for a dataset that declares no mode: it no longer updates an existing row. + Metadata-protocol: semantic + + + + **BREAKING** for a seed dataset that declares no `mode` and relies on a replay to write its values back onto existing rows. + + - **Before.** A seed with no `mode` replayed as `upsert`. A seed loads again on every single-tenant boot (production included, under `os start` and `os serve`), on every package install or upgrade and on every publish, so each replay wrote the shipped value back over a customer's edit to a seeded row, with no warning. Measured on a production boot before the change (card comment 6633189577): an API edit to a seeded row went back to the shipped value on the next restart, after a boot-artifact upgrade, and after an `os package install` upgrade. + - **Now.** `SeedSchema.mode` defaults to `ignore`: a replay inserts a record whose row is missing and leaves an existing row exactly as it is. A row nobody edited is not rewritten either, so its `updated_at` does not move, and a value changed in a later seed version reaches only a row the load creates. Maintainer ruling 6631016988 (option A). + - **One default, stated once.** `SeedLoaderConfig.defaultMode` is `SeedSchema`'s own `mode` field, default included. The six replay sites (`@objectstack/runtime`: the inline boot seed, the per-organization replayer, the dev hot reload and the published-seed apply; `@objectstack/metadata-protocol`: the publish batch; `@objectstack/cloud-connection`: install, reseed and the rehydrate healer) stop restating `defaultMode: 'upsert'`, and the save door reads the schema's default for a seed item that declares no mode. + - **Compiled artifacts.** `os build` stores the parsed seed, so a package built before this change carries a written `mode: "upsert"` on every dataset. The loader cannot tell that value from an authored one, and nothing converts it: such a package keeps today's replay until it is rebuilt. + - **Installed packages.** A package installed with `os package install` (`@objectstack/cloud-connection`'s install-local) keeps the body it was installed with in its install ledger, and the sample-data reseed (`POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`) replays that body's datasets in their stored mode. So an entry installed from a pre-change artifact keeps writing the shipped values back over edited rows on every reseed until the **rebuilt artifact is re-installed**; rebuilding alone changes nothing for it. An entry installed from a rebuilt artifact reseeds create-only: it puts back the rows that are missing and leaves an edited row as it is. The ledger's other replays never overwrite: the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization. + - **Stored `seed` items.** A `seed` metadata item stored with no `mode` is not rewritten either. Its next publish parses it to `ignore`, so that publish inserts only the rows it is missing where it used to write its values back over existing rows, with no signal. + - **Unchanged.** An explicit `mode: 'upsert'` still writes every declared column back on each replay, and `columns: { FIELD: { applyOn: 'create' } }` still exempts the columns a customer or a job owns. `insert`, `update` and `replace` behave as before. + + **The remedy.** Rebuild every package so its compiled artifact carries the mode its source declares, and re-install the rebuilt artifact wherever the package was installed with `os package install`, so its install ledger carries that mode too. To put an installed package's sample data back to the shipped values, purge it (`…/purge-sample-data`) and then reseed. For a dataset that is reference data the package must keep current, declare `mode: 'upsert'`, and mark the columns the customer or a job owns `applyOn: 'create'`. Leave `mode` unset for business and starter data the customer owns once it is loaded. +- 21510bb: feat(plugin-security)!: under a walled tenancy posture every external object states its organization scope + + Clause-②: no (narrowing) — under the `isolated` and `group` postures an external object (an `external` block) that states no organization scope is refused by name when the boot completes, and one registered after the boot answers no row and accepts no write; nothing is widened, and the `single` posture is unchanged. + Metadata-protocol: narrowed + + + + **BREAKING** for a deployment running the `isolated` or `group` tenancy posture (`OS_TENANCY_POSTURE`) with an external object that states nothing about its organization scope. + + - **The rule (ADR-0015 §10 as amended, ADR-0095 D1).** The platform provisions no storage for an external object, so the `organization_id` it injects into the registered schema is a column the remote may not carry. Whose rows an external object serves — one organization's or every organization's — is a fact only its author knows, and under a walled posture the object states it, in one of three ways: its own `organization_id` field (the remote rows carry the organization column and the driver returns it; a remote column of another name is mapped onto it with `external.columnMap: { : 'organization_id' }`); `tenancy: { enabled: false }` (shared by every organization); or `systemFields: { tenant: false }` (the same opt-out on the system fields). These are the wall's own two opt-out clauses and the one column it reads; the whole-block `systemFields: false` states nothing. + - **What the rule does.** `SecurityPlugin` refuses the boot from its `kernel:ready` handler, beside the ADR-0131 D5 refusal, naming every external object that states nothing and the three ways out (`ExternalObjectOrganizationScopeUndeclaredError`). An external object registered after the boot — a Studio import, a runtime datasource — that states nothing answers no row and accepts no write: the Layer 0 verdict is `deny` (`computeTenantLayer0Verdict`), the write wall refuses every insert, update and delete with a sentence naming the undeclared scope and the three ways out, and the plugin says so once in a warning. An object declaring its own `organization_id` is walled: a member of organization A reads only A's rows, through a driver that honours `where` and through one that ignores it (the engine's floor re-applies the predicate), and a row missing the column reads to no one. An object declaring `tenancy: { enabled: false }` or `systemFields: { tenant: false }` is shared. + - **`tenancy.tenantField` on an external object is closed the same way.** The organization wall keys on `organization_id` alone, and a declared tenant field is read only by storage the platform provisions, which an external object has none of; so on an external object a `tenantField` naming another column is refused at boot and denied after it, with the remedy spelled out (`external.columnMap` onto `organization_id`, or `tenancy: { enabled: false }`). Local objects are unchanged. + - **Who moves.** Every walled deployment with an external object stating nothing: declare one of the three and restart. The showcase's two federated analytics objects now declare `tenancy: { enabled: false }`. A `single`-posture deployment owes no declaration and behaves exactly as before. + - **`@objectstack/core`.** `TenantLayer0Input` gains the optional `externalOrganizationScopeUndeclared`; set, the verdict is `deny` under a walled posture, ahead of the platform-admin crossing. Omitted, every existing caller reads as before. + - **`@objectstack/spec`.** The ADR-0087 D3 entry `external-object-organization-scope-declared` tells an upgrading operator what to declare. + + Skills-docs: follow-up #29866 — `content/docs/data-modeling/virtual-objects.mdx` (a multi-organization section), `skills/objectstack-data/rules/datasources.md` (the three declarations), `docs/adr/0015-external-datasource-federation.md` (the §10 and D7 amendment); the sibling docs PR on `claude/sec-hardening-g-docs`, governed (Tier H), landed by the maintainer's hand. +- 0fd363d: fix(spec)!: an object permission that edits or deletes a record also reads it + + Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. + + + + **BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). + + - **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. + - **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. + - **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. + - **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. + - **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. + - Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. + + **FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). +- 63a470a: feat(spec)!: a URL with a special scheme is read for credentials the way a URL parser reads it, however many slashes follow the scheme + + Clause-②: no (narrowing) + Metadata-protocol: narrowed + + + + **BREAKING** accept-set narrowing at the two metadata doors that judge a URL credential: an authored connector instance (`DeclarativeConnectorEntrySchema` — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load, `PUT /api/v1/meta/connector/:name`) and a datasource driver config's URL keys (`config.url` on postgres, mysql, mongo and turso, turso `config.syncUrl`) at every door that parses the datasource. + + - **A special scheme's authority is read as a URL parser reads it.** For `http`, `https`, `ws`, `wss` and `ftp`, in any letter case, the authority starts after the whole run of `/` and `\` that follows the scheme, however long, none included. A connector string holding a userinfo there, and a datasource URL holding a userinfo password there, are refused (`422 INVALID_METADATA` at the save door, located at the string, naming the parts, never the value); a bare username in a datasource URL stays accepted. + - **One grammar at both doors.** The connector entry and the datasource doors read the one userinfo grammar, and both read redactors rewrite exactly what the write refuses, at the position the write judged, serving every other character as stored. + - **Unchanged.** Every other scheme keeps `scheme://` or a scheme-relative `//` (`postgres:`, `libsql:`, `mongodb:` and the rest carry no authority without `//`); `file:` carries no userinfo; a URL spelled `scheme://` reads exactly as before. A stored row is never re-judged: a save of its served form keeps the stored value, and an edit of the served value stores exactly the edit. + + ## FROM → TO + + ```ts + // before — parsed green; now refused at providerConfig.baseUrl + { name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', + providerConfig: { baseUrl: 'https:///billing:…@api.billing.example.com/v1' }, + auth: { type: 'none' } }, + + // after — the URL holds no credential, and the password is a reference + { name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest', + providerConfig: { baseUrl: 'https://api.billing.example.com/v1' }, + auth: { type: 'basic', username: 'billing', credentialRef: 'BILLING_API_PASSWORD' } }, + ``` + + The same holds for `https:billing:…@host`, `https:/billing:…@host` and `https:\billing:…@host`, and for a datasource URL key: spell it `scheme://`, keep it credential-free (a bare username stays accepted) and bind the password through the Setup → Datasources connection form's secret field, or reference it with `external.credentialsRef`. There is deliberately no automatic rewrite: the value in the string may be the only copy of the secret. The upgrade guide entry is `url-credential-special-scheme-read-however-many-slashes`. + +### Patch Changes + +- 8aa2e91: feat(notifications): the `push` channel — a `notify` node naming `push` reaches the recipient's phones through APNs and FCM + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New package `@objectstack/service-push`.** `PushServicePlugin` registers the kernel's `push` service: APNs over HTTP/2 with token-based auth (an ES256 provider token signed with the `.p8` key, reused for fifty minutes) and FCM over the HTTP v1 API (an OAuth access token from the service account). It reads `PushServiceConfigSchema` from its `config` option and refuses an inline secret at boot with the schema's guidance. It serves the direct transport (`transport: 'direct'`, the operator's own APNs key and Firebase project, for a white-label build); the hosted relay, the schema's default and so also an empty config, is refused at init until its sender ships (#29920). Each secret is named by `credentialRef` and resolved when the transport first signs; in the open tier the reference is the name of an environment variable (ADR-0143 D3). + - **`@objectstack/service-messaging`: the `push` channel.** Mounted while a `push` service is registered, as the sms channel is while an `sms` service is. One delivery reaches every enabled device the recipient registered, on each vendor the push service has a transport for. The push carries the notification id and, when it is a same-server path (`PushRouteSchema`), the notification's `actionUrl` as `route`; any other `actionUrl` is left out and the delivery still goes. Each device's push also carries the origin stored on its row (`PushDeliveryDataSchema.origin`) when the row holds one; the registry does not store it yet, so today none does. A token the vendor reports dead (APNs `BadDeviceToken` / `Unregistered`, FCM `UNREGISTERED`) is retired through `invalidateDeviceToken`: the device row is kept, disabled, and the user's other devices still get the push. A recipient with no enabled device, or whose every device was reported dead, is graded `invalid_recipient`; no push service is refused `transport_not_configured`, as the sms and email channels refuse it. + - **`@objectstack/spec`: description and comment text only.** The `[PLANNED]` markers come off `PushServiceConfigSchema`, `PushDirectTransportConfigSchema`, `ApnsTransportConfigSchema`, `FcmTransportConfigSchema` and `PushDeliveryDataSchema`, which now have their reader and producer; `PushRelayTransportConfigSchema` keeps `[PLANNED — not yet served]`, now saying the push service refuses it at boot (#29920). `NotificationChannelSchema`'s JSDoc lists `push` among the registered channels, the docblocks name `@objectstack/service-push` where they named `plugin-notification-push`, and the `NOT_FOUND` synonym waiver's reason names service-messaging's own provenance row. No key, schema shape or export changes. +- 471add3: docs(approvals): say how a request with no submitter leaves the `returned` state + + Clause-②: no + Metadata-protocol: none + + - **`@objectstack/spec`: comment text only.** The `IApprovalService` docblocks on `recall`, `sendBack` and `resubmit` now state the path for a request with no submitter (its run's trigger resolved no user): nobody can recall it once it is `returned`, a system-context `resubmit` is its way out, and the next round opens `pending` with no submitter, where the system or the override actor may recall it. The `resubmit` docblock now names the system context it already admits. No key, type, schema or behaviour changes. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- cde492d: feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: + + - A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. + - Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. + - A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. + - A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. + + An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. +- eee4149: feat(spec/system): export the platform locale table `PLATFORM_LOCALES` and its read aliases + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`PLATFORM_LOCALES`** (from `@objectstack/spec/system`): the one list of languages the platform supports, as frozen rows `{ tag, label, rtl }` in menu order: `en`, `zh-Hans`, `zh-Hant`, `es`, `ar` (right to left), `fr`, `pt-BR`, `ru`, `de`, `ja`, `ko`, each labelled with its own native name. `PLATFORM_LOCALE_TAGS` is the tag list and `PlatformLocaleTag` the literal union of the tags. + - **`PLATFORM_LOCALE_READ_ALIASES`** (typed on its ten literal keys, `PlatformLocaleReadAlias`) and **`platformLocaleTagOf(code)`**: the codes stored values already carry, mapped to the table tag they mean (`zh`, `zh-CN`, `zh-SG` to `zh-Hans`; `zh-TW`, `zh-HK`, `zh-MO` to `zh-Hant`; `ja-JP` to `ja`; `es-ES` to `es`; `pt` to `pt-BR`; `en-US` to `en`). A table tag answers itself and the match ignores case (`zh-cn` reads as `zh-Hans`). Any other code answers `undefined`; no region fallback is guessed. + - The table is the target every surface is backfilled to; today the Console ships ten packs without `zh-Hant`, and platform metadata and the built-in emails ship four locales. Nothing reads the table yet and no schema refuses a tag that is not on it: every locale value that parsed before still parses. The protocol page `i18n-standard` gains the table, the read aliases and the rule (same-PR follow; a language is added or removed by changing the table and backfilling once). +- 4be9734: docs(spec)!: the screen-field contract names `number`, `currency` and `percent` as the numeric value domain the resume enforces (#30178) + + Clause-②: yes (narrowing) — the resume door refuses a string under a `type: 'currency'` or `type: 'percent'` screen field (`@objectstack/service-automation`); this package's `ScreenFieldSpec` and `ScreenFieldConfigSchema` text now states that rule. No schema shape changes. + Metadata-protocol: none + + + + **BREAKING** (described here, enforced in `@objectstack/service-automation`): a `currency` or `percent` screen field takes a finite JSON number on resume, as a `number` field does; a numeric string is refused with `invalid_type`, never coerced. `ScreenFieldSpec.type` / `.min` / `.max` and `ScreenFieldConfigSchema.type` / `.min` / `.max` say so, and the `type` describe text names the three kinds. +- bb86ff9: feat(i18n): one label order on an overridden item, for all eight document translators — the pair's translation, then the override's own label, then the base's translation + + Clause-②: yes (widening) + Metadata-protocol: none + + - **New option `TranslateDocumentOptions.pairBundle`.** The translations the served item's paired `.custom` package ships itself (ADR-0146 D2/D3), locale-keyed as `bundle` is. The serving layer's `bundle` is one merged catalog per locale with the package ids dropped, so only this option can tell a translation the pair ships from the base's. + - **The order, per translatable string.** A string the override changed (`valueOverridesPackagedBase` against `packagedBase`, the comparison every translator already makes) reads the pair's translations only, along the locale chain, and otherwise keeps its own text: the base's translation is never read for it. A string the override left alone reads, locale by locale, the pair's translation and then the deployment catalog, so a pair that ships only `en` never displaces the base's `zh-CN` on an untouched label. Nested labels follow the same rule: widget titles, filter and option labels, view bulk-action defs and form sections, report blocks, field labels, help and options, page component copy, tab items and the header, dataset members, app navigation nodes and book groups. + - **`translatePage`, `translateDataset`, `translateApp` and `translateBook` now read `packagedBase`** (their third parameter widens from `ResolveOptions` to `TranslateDocumentOptions`), each string judged against the base's counterpart: the page's components by id and its header, the dataset's members by `name`, the app's navigation nodes by `id`, the book's groups by `key`. A page header title the override left as the base wrote it translates as the base's label. + - **Nothing moves for an existing caller.** With no `pairBundle` (or one that answers none of the chain's locales) every translator gives exactly the answer it gave before, and with no `packagedBase` nothing counts as changed. Passing both for a `.custom` override is the serving layer's half (#30062). + - An object's `validations[].message` and its inline `actions` keep reading the merged catalog: the rule evaluator answers the same message from `i18n.t()` with no pair input, and the refusal text must equal the served text. +- eca7cd2: feat(spec): `PLATFORM_TOOLS_BY_PACKAGE['service-ai-studio']` lists `create_record`, `update_record` and `remove_sample_data`; `draft_record` is retired + + Clause-②: no + Metadata-protocol: none + + The cloud chat agents now create or change ONE record of the user's data after the user confirms it on a card, and remove the sample rows a seed replay recorded after a card listing them (cloud#3565, the maintainer's ruling of 2026-11-04). The three tools replace `draft_record` (cloud#3383), whose "draft it and the user saves it in a form" ending was never built. The registry lists the three names in their alphabetical order and drops the retired one, so `PLATFORM_PROVIDED_TOOL_NAMES` carries them and a skill whose `tools[]` names them resolves under ADR-0109 reference integrity, while a skill still naming `draft_record` is reported as a fictional tool (`ai-skill-tool-unresolved`). That is the whole change: three accepted `skill.tools[]` values in, one out. No schema shape changes, no export is added or removed. + + **Reach of the retirement.** `draft_record` was a published constant, so removing it breaks any skill whose `tools[]` still names it. Measured before this landed: no skill in this repo names it (`validate-ai-tool-references.test.ts` names it only as the control proving the report fires; the two spec page fixtures called `draft_record` are page names, not tools); the one skill anywhere that names it is the cloud chat's `record_drafting` skill, which the cloud half of cloud#3565 (cloud#3574) moves to `create_record` / `update_record` in the same landing. A third-party skill that still names it reports `ai-skill-tool-unresolved` at validation and is not silently dropped. That is why this is graded `patch` and not left as a silent one. + ## 18.3.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index c60250de9cd..d851b73d825 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "18.3.0", + "version": "18.4.0", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "type": "commonjs", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index d65db0e64d8..9ccce6c1668 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/trigger-api +## 18.4.0 + +### Patch Changes + +- f9e5bef: feat(trigger-api): an `api` hook verifies the vendor signature its start node declares (`config.signature`) + + Clause-②: yes (widening) + Metadata-protocol: none + + - **`@objectstack/trigger-api`.** `start()` reads the hook's one verifier with `apiTriggerVerifierOf(binding.config)` (`@objectstack/spec/automation`). A start node that declares its sender's scheme (`config.signature`: DocuSign Connect's base64 `X-DocuSign-Signature-1`, a Stripe-style `t=…,v1=…`, a GitHub-style `sha256=`) now admits a correctly signed post that was answered `401` before. That scheme REPLACES the `x-objectstack-signature` check for its hook: the declared header is read on both faces of the door (the self-hosted mount and `handleInboundHook`) with `Headers.get`, so a repeated field is its one joined value; the HMAC-SHA256 is computed under the key `config.secretRef` names (read at every post through the engine's reader, `503` while it has no value), over the raw body or `.`; each digest is decoded strictly to 32 bytes and compared with `timingSafeEqual`, any one of several `v1` elements matching (a key roll); and a timestamped scheme is held to `HTTP_SIGNATURE_TOLERANCE_SECONDS` from `@objectstack/core`. A body-only scheme has no replay window, as the contract states. A valid `x-objectstack-signature` admits nothing on such a hook, and a post without the declared header is refused. A declaration the spec answers `unusable` arms a hook that refuses every post before any secret is read and never falls back to `x-objectstack-signature`; a declared scheme bound without the engine's `config.secretRef` reader is refused at `start()`. Every failure is the same `401 INVALID_SIGNATURE` with its own cause in the server log (`declared-header-absent`, `declared-header-malformed`, `declared-prefix-missing`, `declared-no-digest`, `declared-mismatch`, `declared-outside-window`, `declared-unusable`), never the key, the header value or the body. A hook that declares no scheme is verified exactly as before. + - **`@objectstack/lint`.** `validateFlowApiTriggerSecret` grades an `api` hook by that same reader: a usable declaration is ready on its `config.secretRef` alone, and an unusable one is the new error `flow-api-trigger-signature-unusable` (with its `os explain` entry), located at the key the spec refuses. It fires only on a declaration the flow parse already refuses, so no stack that passed before is refused now. + - **`@objectstack/skills`.** The automation and api skills teach `config.signature` where they said a vendor-signed callback cannot be admitted. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 91a9384dfaa..0f2db3aaaa4 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "type": "commonjs", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index 1c489cdb2ff..f5f9dd21c24 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/plugin-trigger-record-change +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 6abe554bd01..9db506b7ab6 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": "18.3.0", + "version": "18.4.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)", "type": "commonjs", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index f3ea2f678bf..a07f1946bd4 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/plugin-trigger-schedule +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/metadata-core@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index c00a8acedf7..99a5a59af7a 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "18.3.0", + "version": "18.4.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)", "type": "commonjs", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 033495aad04..0e5984e966c 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,35 @@ # @objectstack/types +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index 5f02e0dc21e..59a4c23ccc4 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "18.3.0", + "version": "18.4.0", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "type": "commonjs", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index d114ce9f172..e499feb2338 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,150 @@ # @objectstack/verify +## 18.4.0 + +### Minor Changes + +- 9ebca94: feat(push): `requires: ['push']` and the stack's `push` key turn mobile push on under `objectstack serve`, and a relay configuration is refused at boot + + Clause-②: yes (widening) + Metadata-protocol: added + Skills-docs: follow-up owed — #29969 (skills/objectstack-platform/evals/config-plugins-ops.json: no eval asks for `requires: ['push']` or the `push` key); this diff teaches both in `skills/objectstack-platform/SKILL.md` (the top-level key enumeration and the `requires` table) and in the new `content/docs/kernel/runtime-services/push-service.mdx` + + A deployment that runs `objectstack serve` from a stack file can now turn on mobile push without host code. Before this, the only way to register a push service was a host that composed `PushServicePlugin` in code, so a `notify` node naming `push` had nothing to deliver through on a served stack. + + - **`@objectstack/spec`: the `push` capability token and the `push` stack key.** `push` joins `PLATFORM_CAPABILITY_TOKENS`, provided by `@objectstack/service-push` in the open edition. It is requested, never on the always-on slate. The stack definition gains a top-level `push` key beside `email` and `sms`: it is `PushServiceConfigSchema`, composed as a single value (`COMPOSE_KEY_DISPOSITIONS`). Authoring accepts a direct block, a relay block and absence; it refuses an inline secret, by key name (`key`, `privateKey`, `serviceAccount`, …) with `credentialRef` as the remedy, or pasted into a `credentialRef` (ADR-0143 D3). `PushServiceConfigSchema`'s docblock now names the stack key and `objectstack serve` as its reader; `PushRelayTransportConfigSchema` keeps its `[PLANNED — not yet served]` marker. + - **`@objectstack/core`: the `push` provider row and its argument.** `CAPABILITY_PROVIDERS.push` names `PushServicePlugin`, and `resolveCapabilityArgument('push', …)` builds its argument from the stack's `push` key through `resolvePushCapabilityArg`, read off the provider module as the `email` and `sms` readers are. Both `objectstack serve` and `@objectstack/verify`'s `bootStack` read this table, so an app's tests get the push service its server gets. + - **`@objectstack/service-push`: `resolvePushCapabilityArg`.** It parses the `push` key with `PushServiceConfigSchema` and refuses, before the kernel boots, two configurations: the relay transport (an absent key and `push: {}` read as it), whose sender has not shipped, with a message that says so and prescribes `transport: 'direct'` with an `apns` and/or `fcm` block; and anything the schema refuses, an inline secret first among them, naming each path and the schema's guidance and never the value. On a stack that declares `requires: ['push']` either refusal fails the boot as `Capability "push" (@objectstack/service-push) failed to start: …`. + - **`@objectstack/cli` and `@objectstack/verify`** depend on `@objectstack/service-push`, so the provider the token names resolves out of the box, as the open edition promises. + + To turn push on, add `requires: ['push']` and a direct block naming each vendor's secret by reference: + + ```ts + push: { + transport: 'direct', + apns: { teamId: 'ABCDE12345', keyId: 'KEY1234567', topic: 'com.example.app', environment: 'production', credentialRef: 'APNS_SIGNING_KEY' }, + fcm: { projectId: 'example-app', credentialRef: 'FCM_SERVICE_ACCOUNT' }, + }, + ``` + + In the open tier each `credentialRef` is the name of an environment variable holding the secret. A `push` key on a stack whose `requires` does not name `push` is not read, and `objectstack serve` logs one boot warning saying so. The runtime-services chapter has a new `services.push` page. + +### Patch Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. +- Updated dependencies [31fe289] +- Updated dependencies [a35fc66] +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [3b8c724] +- Updated dependencies [5cdaddb] +- Updated dependencies [509c40a] +- Updated dependencies [c23501b] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [5f47410] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [314ad95] +- Updated dependencies [771b2db] +- Updated dependencies [9037b13] +- Updated dependencies [8d5aab3] +- Updated dependencies [e3b56b2] +- Updated dependencies [18112bf] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [cd5f554] +- Updated dependencies [f19fbb8] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f9e5bef] +- Updated dependencies [63b86b3] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [171b6dd] +- Updated dependencies [f6f663a] +- Updated dependencies [343ddb2] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [159930a] +- Updated dependencies [04f36b6] +- Updated dependencies [4be9734] +- Updated dependencies [4be9734] +- Updated dependencies [1a3ea0c] +- Updated dependencies [bb86ff9] +- Updated dependencies [8f05e61] +- Updated dependencies [977be38] +- Updated dependencies [a26abbc] +- Updated dependencies [8eca18f] +- Updated dependencies [8b117c0] +- Updated dependencies [474c030] +- Updated dependencies [85c7374] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [42e1211] +- Updated dependencies [7193160] +- Updated dependencies [72d7db0] +- Updated dependencies [d0da880] +- Updated dependencies [7b2bb66] +- Updated dependencies [30dcd8a] +- Updated dependencies [c92b887] +- Updated dependencies [21510bb] +- Updated dependencies [d7c0389] +- Updated dependencies [917f024] +- Updated dependencies [0fd363d] +- Updated dependencies [a7293f7] +- Updated dependencies [59e6463] +- Updated dependencies [63a470a] +- Updated dependencies [4851668] + - @objectstack/objectql@18.4.0 + - @objectstack/service-analytics@18.4.0 + - @objectstack/service-push@18.4.0 + - @objectstack/service-messaging@18.4.0 + - @objectstack/spec@18.4.0 + - @objectstack/core@18.4.0 + - @objectstack/runtime@18.4.0 + - @objectstack/service-datasource@18.4.0 + - @objectstack/plugin-email@18.4.0 + - @objectstack/plugin-auth@18.4.0 + - @objectstack/mcp@18.4.0 + - @objectstack/service-queue@18.4.0 + - @objectstack/service-settings@18.4.0 + - @objectstack/rest@18.4.0 + - @objectstack/platform-objects@18.4.0 + - @objectstack/plugin-approvals@18.4.0 + - @objectstack/trigger-api@18.4.0 + - @objectstack/plugin-audit@18.4.0 + - @objectstack/service-automation@18.4.0 + - @objectstack/plugin-security@18.4.0 + - @objectstack/service-storage@18.4.0 + - @objectstack/plugin-pinyin-search@18.4.0 + - @objectstack/plugin-sharing@18.4.0 + - @objectstack/plugin-webhooks@18.4.0 + - @objectstack/service-mailbox@18.4.0 + - @objectstack/service-package@18.4.0 + - @objectstack/trigger-record-change@18.4.0 + - @objectstack/metadata@18.4.0 + - @objectstack/plugin-hono-server@18.4.0 + - @objectstack/service-cache@18.4.0 + - @objectstack/service-job@18.4.0 + - @objectstack/service-realtime@18.4.0 + - @objectstack/service-sms@18.4.0 + - @objectstack/trigger-schedule@18.4.0 + - @objectstack/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index d13ef0b63f8..513e5706aba 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "18.3.0", + "version": "18.4.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", diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index bf691e87887..9c9ca6a9216 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -12,7 +12,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: ai tags: agent, tool, skill, knowledge, llm, embedding, mcp --- diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index ee551eb0d46..85d21b81fb9 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -14,7 +14,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: api tags: rest, endpoint, auth, realtime, server --- diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index b804decc7b6..5f9bfe1b674 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: automation tags: flow, workflow, trigger, approval, state-machine, scheduled, webhook --- diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 682acf600aa..14540f17a4e 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -15,7 +15,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: data tags: object, field, validation, index, relationship, hook, schema, permission, rls, security, seed, fixture --- diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 56bfc9df238..aeff810eafb 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x and @objectstack/formula 18.x (CEL) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: expression tags: cel, formula, predicate, condition, validation, visibility, seed-dynamic --- diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index d1a68b281bc..3b5ce283b14 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -13,7 +13,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: i18n tags: i18n, translation, locale, l10n, bundle, coverage --- diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 9ef3dd3a4e1..9097ae66160 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -17,7 +17,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x and @objectstack/core 18.x (Zod v4 schemas), Node 22+ metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: platform tags: project, defineStack, driver, hono, plugin, kernel, service, requires, capability, DI, lifecycle, cli, deploy, ops --- diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index c5e825ddf6b..32f40d80ca5 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -11,7 +11,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: query tags: query, filter, sort, paginate, aggregate, ObjectQL, full-text --- diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index eee8e3ad2bf..c5c18e3fc4c 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -18,7 +18,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x (Zod v4 schemas) metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: ui tags: view, app, page, dashboard, report, chart, action, widget, doc --- diff --git a/skills/objectstack-upgrade/SKILL.md b/skills/objectstack-upgrade/SKILL.md index d8e80d2619b..de8c95c9064 100644 --- a/skills/objectstack-upgrade/SKILL.md +++ b/skills/objectstack-upgrade/SKILL.md @@ -16,7 +16,7 @@ license: Apache-2.0 compatibility: Requires `@objectstack/spec` and `@objectstack/cli` at the TARGET major; the chain replays from the spec's `MIGRATION_SUPPORT_FLOOR`. No network access required. metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: process tags: upgrade, migration, protocol, major, retired-keys, tombstone, conversions, validate, report --- diff --git a/skills/objectui/SKILL.md b/skills/objectui/SKILL.md index ac1d5b3979a..a693a40ebb8 100644 --- a/skills/objectui/SKILL.md +++ b/skills/objectui/SKILL.md @@ -5,7 +5,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x; each @object-ui/* package declares its exact @objectstack/spec range in its own manifest. metadata: author: objectstack-ai - version: "18.3.0" + version: "18.4.0" domain: ui tags: sdui, schema-renderer, json-ui, react, shadcn, tailwind, plugin, expression user-invocable: false diff --git a/ui/apps/console/CHANGELOG.md b/ui/apps/console/CHANGELOG.md index aa211031e6e..1c2619f9ede 100644 --- a/ui/apps/console/CHANGELOG.md +++ b/ui/apps/console/CHANGELOG.md @@ -1,5 +1,35 @@ # @object-ui/console +## 18.4.0 + +### Patch Changes + +- f35b361: fix(console): the sign-in page finishes the mobile sign-in hand-off instead of landing on a missing Console page + + Clause-②: no + Metadata-protocol: none + + The mobile app opens the Console's sign-in page in the system browser with `?redirect=` naming the hand-off route (`/api/v1/auth/one-time-token/handoff?...`), so that a user whose identity provider refuses embedded WebViews can still sign in. After sign-in the page sent every safe redirect through the Console mount, so the browser landed on `/_console/apps/.../api/v1/auth/one-time-token/handoff?...` and the page said it does not exist. Now: + + - A `redirect` whose path is exactly the hand-off route under the auth base the Console talks to is navigated to unprefixed, by script from the sign-in page. Every other redirect, `/api/v1/data/x` included, stays a Console route as before; a lookalike path, another base path or an absolute URL is never followed out of the mount. + - The hand-off comes before the organization steps: a user with several organizations is no longer sent to the picker first, and one with no active organization does not wait for it to be chosen. + - A session created more than five minutes ago, which the hand-off refuses, keeps the sign-in form on screen instead of forwarding to a JSON refusal. Signing in on that form makes a new session and forwards. + - The sign-up page does the same after a sign-up. +- cde492d: feat(plugin-auth): a page navigation to the mobile sign-in hand-off that has to sign in first goes to the sign-in page, so the app opens only the hand-off + + Clause-②: no + Metadata-protocol: none + + `GET /api/v1/auth/one-time-token/handoff` answered a missing or old session with `401` as raw JSON, so the mobile app had to open the Console's `/login` route itself and know how that page treats `redirect`. An installed app stays in use for months, so a Console upgrade could strand it. Now the app opens only the hand-off: + + - A page navigation with no session, or with one older than five minutes, answers `302` to this deployment's Console sign-in page, with `redirect` naming the same request. `Location` is root-relative, so the browser stays on the origin the request arrived on, and `uiBasePath` is honoured. + - Any other request still answers `401`, so the app's anonymous probe keeps reading "this server registered me". A navigation reporting `Sec-Fetch-Site: same-origin` answers `401` too: it is the sign-in page forwarding a session the route refuses. A deployment that does not serve the Console keeps the `401` too, and says so once at `warn`. + - A token is still minted only for a session under five minutes old, and every `400` (a cross-site navigation, an unknown parameter, an unregistered scheme, a bad challenge) is still decided first, with no redirect. + - A device whose clock is behind does not bounce between the route and the sign-in page. The redirect carries `handoff_refused_at`, the server's time of the refusal, and the page holds its form for any session not created after it, whatever the device's clock says. A session created after it forwards, so a provider's sign-in that returns to the same URL still ends at the app. The links between the sign-in and sign-up pages carry the mark beside a hand-off `redirect`, and every other link is unchanged. + + An app can change its entry only once its server runs this release: an older server answers the new entry with JSON. +- @objectstack/sdui-parser@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/apps/console/package.json b/ui/apps/console/package.json index a6215b2aa89..c26868224b7 100644 --- a/ui/apps/console/package.json +++ b/ui/apps/console/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/console", - "version": "18.3.0", + "version": "18.4.0", "description": "ObjectStack Console — opinionated, fork-ready runtime console built on @object-ui/app-shell with the full plugin set wired up. Ships as a Hono UI plugin serving a pre-built SPA.", "license": "MIT", "type": "module", diff --git a/ui/packages/app-shell/CHANGELOG.md b/ui/packages/app-shell/CHANGELOG.md index 26d89123206..b61dcbff1e8 100644 --- a/ui/packages/app-shell/CHANGELOG.md +++ b/ui/packages/app-shell/CHANGELOG.md @@ -1,5 +1,372 @@ # @object-ui/app-shell — Changelog +## 18.4.0 + +### Minor Changes + +- 15f808a: fix(metadata-protocol): a mapping override is served, a pair's removal of its own override of any overridable type brings the base's item back, and an excluded type's `overrides` refusal names a remedy that works for it (#30062) + + Clause-②: no — ADR-0146 D2 says an item in the customization package replaces the base's item and removing it brings the base's back, and ADR-0146's revert ("Restore package original") is deleting the overriding item from `.custom` so the base's document is served again. The 403 on removing a pair's own doc or mapping override, and the unserved mapping override, refused what that text already admits. A removal that names the base still keeps today's answer. + Metadata-protocol: semantic + + - **A mapping override in `.custom` is served.** A mapping item that declares `overrides: { package: '', … }` in the base's customization package now replaces the base's mapping for the by-name read (no `?package`, `?package=`), both lists and the layered read, as page, app, dataset, doc and book already did. The import door (`@objectstack/rest`'s `import-mapping.ts`) resolves a named mapping through that package-less by-name read, so it now gets the pair's mapping. + - **Removing a pair's own override works for every overridable type.** `DELETE` of a declared override the customization package holds (named with `?package=.custom`) was admitted only for a flow; a doc or mapping override answered `403 NOT_OVERRIDABLE` on the environment kernel. The removal now admits the pair's row for every type `isOverridableMetadataType` names. The row is deleted and the base's item is served again under every address. A removal that names the base (`?package=`) is unchanged: the pair's row stays and its override stays served. + - **An excluded type's refusal names a remedy that works for it.** A save of `overrides` on a type that takes no override now cites that type's own record (ADR-0146 D6 for the security catalog and code-only types, D1 for a datasource, D9 for a behavioural switch, D10 for a field, ADR-0126 §3 for one-shot types, D2 for the rest) and prescribes a door that exists for that type: a new item under a new name where the type may be created at runtime, the documented switch route for a sharing rule, Setup › Queues for a queue, Setup › Datasources for a datasource, an object extension for a field, and a new version of the base for a type defined only in package source. The error code and status are unchanged. + - **`@object-ui/app-shell`: the All metadata tile agrees with the Customize door.** On a read-only package, a type's tile reads 可定制 (Customizable) exactly where Studio offers a Customize door for it (flow and app included, whose overlay flag is off) and 只读 (Read-only) elsewhere (doc and mapping, which have no door yet). It no longer reads the overlay flag. + + The label precedence of a translated override (card item 3) is not in this change. +- 0fd363d: fix(spec)!: an object permission that edits or deletes a record also reads it + + Clause-②: no (narrowing) — the authoring object-permission schema refuses an entry granting `allowEdit: true` or `allowDelete: true` without read (`allowRead`, `viewAllRecords` or `modifyAllRecords`), a permission set it accepted before; the permission evaluator grants edit and delete only together with read; and a by-id update or delete from a caller the read door refuses answers that refusal. Nothing is widened. + Metadata-protocol: narrowed + Skills-docs: docs moved — `content/docs/permissions/permission-sets.mdx` states the rule under the object permission bits; `skills/**` teaches no entry granting edit or delete without read (its examples grant read beside edit), so no skill moves. + + + + **BREAKING** accept-set narrowing on `ObjectPermissionSchema` (and so `PermissionSetSchema`, `definePermissionSet`, `defineStack`, `os validate` / `os build`, and the metadata save and publish doors for `permission`). + + - **Before.** `objects: { crm_lead: { allowEdit: true } }` parsed, and the evaluator granted edit on `crm_lead` to a holder who could not read it. + - **Now.** The schema refuses it: one `custom` issue at `objects.crm_lead.allowEdit` (or `.allowDelete`), naming both fixes. `objectPermissionGrants` answers edit and delete as `read ∧ grant` (the shape `allowExport` already had), and `PermissionEvaluator.checkObjectPermission` asks it of the caller's merged entry, so the read half may come from another set — the same answer `/me/permissions` gives the client. + - **Stored rows.** A permission set stored before this release keeps loading; its unread edit or delete grant grants nothing. The response-side `EffectiveObjectPermissionSchema` stays tolerant. A built artifact carrying the shape is refused when it is parsed and is rebuilt from a corrected source. + - **By-id writes.** A by-id update or delete from a caller whose read of the object is refused — with these grants that is a capability the object requires for a read and not for a write — answers that read refusal for every id it addresses: a caller who cannot read an object writes none of its records by id. + - **`@object-ui/app-shell`.** The permission set editor ticks Read when Edit or Delete is ticked, and unticking any bit that reads (Read, View All or Modify All) unticks Edit and Delete once no bit that reads remains on the entry. + - Unchanged: `allowCreate` and `allowTransfer` are not part of this rule. + + **FROM → TO.** `objects: { crm_lead: { allowEdit: true } }` → `objects: { crm_lead: { allowRead: true, allowEdit: true } }` if the set should edit the object, or `objects: { crm_lead: {} }` if it should not. The upgrade-guide entry is `permission-write-without-read-refused` (a semantic migration: no automatic rewrite, because only the author knows which of the two the set means). + +### Patch Changes + +- 04cbdf7: fix(app-shell): on a read-only item, the Properties panel shown with nothing selected says why its controls are disabled + + Clause-②: no + Metadata-protocol: none + + On the metadata editor page (Studio's All metadata item page, Setup's metadata pages), a locked item, a read-only package, a caller without `manage_metadata`, a code-shipped artifact or a walled catalog type disables every control in the Properties panel. The scoped inspector (a selected flow node or edge) already said why; the panel shown with nothing selected (an object's or view's default inspector, or the generic form a flow shows) greyed its controls silently, and Tab never reached them. The page now wraps that panel, and the generic form on a page without a canvas, in the same reason wrapper the scoped inspectors use: hovering a disabled control or tabbing to the panel opens a tooltip with the reason, and the panel's accessible description is that reason. A writable item renders the panel exactly as before. No registered default inspector changes: the page draws the reason around it. +- 40fa433: fix(app-shell): Studio's publish review sheet no longer lists an `Options` change on a picklist-bound field nobody edited (#29899) + + Clause-②: no + Metadata-protocol: none + + On an object with a field bound to a package picklist (`{ type: 'select', picklist: 'NAME' }`), the publish review sheet listed `~ FIELD · Options` on every publish, beside the change the author actually made. The draft is stored without the field's `options`, while the live read serves the options the runtime resolved from the picklist, and the sheet compared the two as read. + + The sheet now compares both versions of an object in the form the publish door stores: a field that names a picklist is read without `options` on both sides. A field with its own inline `options` is untouched, so a real options change there is still listed, and a field moved between a picklist and inline options still lists both keys. +- b379262: fix(app-shell): the metadata editor's save-status chip says "Not saved" while a save is held, instead of promising an auto-save that never comes (#29901) + + Clause-②: no + Metadata-protocol: none + + On the metadata editor page (Studio's All metadata item page, Setup's metadata pages) with auto-save on, a draft the server refused with `422 INVALID_METADATA` stays dirty, and the chip beside Save kept reading "Auto-saving…" although the auto-save is held until the refused part changes and no further save is sent. The same happened while the inspector holds a fault it reports (a CEL predicate that does not parse), which holds the auto-save the same way. + + The chip now has a held state: while either hold stands on a dirty draft it reads "Not saved — fix the errors to save" in the destructive tone, with the Save button's own explanation as its tooltip. It returns to the usual states as soon as the hold lifts: editing the part the server named, or fixing the inspector fault, lets the auto-save run and the chip reads "Auto-saving…", then "Saved". Nothing about when a save is sent or held changes. New string: `engine.edit.notSavedHeld` (en, zh). +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- 6db9198: fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) + + Clause-②: no + Metadata-protocol: none + + The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. +- 9d1a465: internal: move-only — the Studio Interfaces pillar's navigation rail moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 3) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. The rail Studio's Interfaces pillar draws for an app's navigation (the nav tree and its groups, the rows a customization package contributes, the off-nav sections for pages, dashboards, reports and views, the two rail contexts, the kind names and kind icons) now lives in `views/studio-design/InterfacesNavRail.tsx`. The moved code is byte-identical apart from its imports and eleven `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. +- f7aab89: internal: move-only — Studio's nav item inspector moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 1) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. The menu-entry inspector Studio shows while you edit an app's navigation, with the nav target pickers and the select it draws with, now lives in `views/studio-design/StudioNavItemInspector.tsx`. The moved code is byte-identical apart from its imports and one `export`; `StudioDesignSurface.tsx` imports it from there and re-exports the inspector, so every import of it keeps working. The package's public exports are unchanged. +- 73a3ef9: internal: move-only — Studio's top-bar package switcher moves out of `StudioDesignSurface.tsx` into its own module (#29988, slice 2) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. The package switcher in Studio's top bar (the package list, New package, the package detail sheet and the deleted-package eviction), with the package-list toast id and the Studio landing path it shares with the surface, now lives in `views/studio-design/PackageSwitcher.tsx`. The moved code is byte-identical apart from its imports and three `export`s; `StudioDesignSurface.tsx` imports it from there. The package's public exports are unchanged. +- c618e89: internal: move-only — Studio's flow rail helpers, pillar kit and Data pillar helpers move out of `StudioDesignSurface.tsx` into three modules, taking it under 10,000 lines (#29988, slice 4) + + Clause-②: no + Metadata-protocol: none + + No behaviour change. Three groups of helpers now live in their own modules under `views/studio-design/`. `AutomationsFlowRail.tsx` holds the Automations rail's flow status dot, its glyph tables, the flow reads and the new-flow trigger helpers. `StudioPillarKit.tsx` holds what every pillar shares: the held-edit notice, the unsaved-work and taken-identifier checks, the "Show me" focus helpers, the draft-saved announcement and the read-only link. `DataPillarHelpers.tsx` holds the Data pillar's field-name, CEL-stamp and form-preview helpers. The moved code is byte-identical apart from its imports and `export`s. `StudioDesignSurface.tsx` imports it from there and re-exports the five names it exported before, so every import of them keeps working. The package's public exports are unchanged. +- 61b487b: fix(app-shell): a personal view opened by its address no longer carries the default view's filter and sort in the address bar + + Clause-②: no + Metadata-protocol: none + + - While the list waits for the reader's own views to resolve a requested or pinned view it does not hold yet, the list's URL state is neither seeded nor written. The default view the page falls back to meanwhile is not the view the address names, so its stored filter, sort and search no longer reach the address bar. Before, when the list preferences landed first, that default seed was written to the URL, and a personal views read landing before the write committed left it there: a reload or a shared link then opened the personal view with conditions nobody chose for it and marked it personalized. + - When the view resolves, the seed is taken for it alone: from its own stored state, or from the `uf_*` params a link carried, as before. +- 8fa370c: fix(app-shell): a record's 动态 stream is read again after a save, so its count and rows hold the change without a reload + + Clause-②: no + Metadata-protocol: none + + - **The record feed follows the record's invalidation.** A save through the edit dialog (or an action, an undo, a ⟳) put the record on the data-invalidation bus, and the page read the record again, but the 动态 feed kept the rows and the count the page payload answered when it opened. The feed now reads again on the same change, on the record page and in the drawer. + - **The re-read asks what the open asked.** Once the record changed after the open, the feed asks the record page route for the streams the open asked, and only those (the discussion, the activity rows, the stream's count), so the new activity row and the server's count land in place. The activity row a save writes exists before the save answers, so one ask after the save sees it; nothing polls. + - **The field changes follow the same change**, so on an object with `enable.trackHistory` the stream's field-change entries are read again too. +- 03be565: fix(app-shell): New automation no longer saves a typed prefix as the trigger object (#30038) + + Clause-②: no + Metadata-protocol: none + + In Studio's New automation dialog, typing part of an object name and pressing Enter committed the typed text itself, so the flow was created bound to an object that does not exist. The trigger-object picker there now takes objects only: Enter or leaving the field commits the object the text names exactly, else the only match left in the list, else no object, and the picker says no object is chosen. A record preset then keeps the dialog open until an object is picked. + + Every other object picker keeps its free text, because a flow reference may hold an expression. +- 7e9fa4b: fix(app-shell): Studio on a zh-CN console reads in Chinese in the view, page and report panels, the publish sheet, the flow Problems tab and the save-notice / notify nodes, and calls a dashboard 仪表盘 everywhere + + Clause-②: no + Metadata-protocol: none + + The view, page and report property panels render the spec's authoring forms from the console's own copy of the spec, and only the dashboard form had a zh-CN overlay, so section headings such as Basics, Columns & filters and Navigation & sharing, their descriptions and schema-titled rows such as Searchable Fields read in English. The overlay now carries every `METADATA_FORM_REGISTRY` type, worded as the platform's own zh-CN metadata-forms bundle words them: view, page and report in full, the other types by field label, which is what the Pending changes sheet names a changed property by (a flow's `~ Status` now reads `~ 状态`). The sheet's summary line counts datasets as 数据集 instead of the raw type name. + + A flow whose trigger is not armed on this deployment showed the engine's developer sentence under 未在此运行, above 没有问题 —— 该流程结构有效. The note now says, from the engine's `unboundCause` code, what this deployment does not do and who can change it, keeps the engine's sentence as the hover detail, and the line below it says the structure checks found nothing to fix instead of "no problems". A cause the console has no row for still shows the engine's sentence. + + The palette names the save-notice node 保存提示, and the notify node's Template and Template data fields and the save-notice Message field read in Chinese. English consoles read the same English as before. zh-CN Console and Studio text uses 仪表盘 for dashboard throughout. +- 5db77f7: fix(app-shell): on a zh-CN console the view properties panel's empty filter button reads 「+ 添加筛选…」 and the description row's hint reads in Chinese + + Clause-②: no + Metadata-protocol: none + + - The `filter-builder` widget's empty trigger was an English literal (`+ Add filter…`); it now reads the Studio string the dataset scope filter already uses, so it follows the designer locale. + - The zh-CN view form overlay gains the `description` row's help text, which showed the schema's English description. +- f4db5f3: fix(app-shell): the console AI dock tells the agent which app the user has selected and which page or record is in view, instead of the constant `activeApp: 'AI'` and no page at all (#30043) + + Clause-②: no + Metadata-protocol: none + + Every chat request the console sends to `POST /api/v1/ai/agents/:agent/chat` used to carry `context.activeApp: 'AI'`, whatever app the user was in, and the floating dock sent no `context.surface`. So the assistant asked "which app?" and "which record?" while the screen showed both. + + Now: + + - `context.activeApp` is the package of the app the user has selected: the console app the dock sits in (whether the dock runs `ask` or `build`), the app picked for editing on the full-page `/ai/build?package=…`, the package the Studio copilot designs. With no app selected the key is left out; nothing sends a placeholder. + - The dock, on the desktop rail and the phone sheet alike, sends `context.surface` in cloud#1610's existing shape for the page in view: an object list (`artifact: { type: 'object', name }`, with `selection: { kind: 'view', id }` on a saved list view), a record page (the object plus `selection: { kind: 'record', id }`), a dashboard, report or page (`artifact: { type, name }`, `mode: 'design'` in the dashboard designer). A page that shows no one artifact (the app landing, search, a component page) sends no `surface`. + - Both are read on every send, so navigating with the dock open moves the next turn's context. + + The dock's "discussing" chip now names the object by its label on object pages, as the Studio copilot's chip already did for its artifacts. +- 42afac7: feat(app-shell): Studio Interfaces picks an object's list view from the header title, object / view ▾, the way the running list does, and the open view stays in the address + + Clause-②: no + Metadata-protocol: none + + - **The view switcher is in the header's title.** An `object` entry's header reads object / view ▾. The switcher is the running list's own dropdown (`ObjectViewTitleSwitcher`): the object's list views by label, a view only drafted in this package included, a search box, and New view, which opens the Create View dialog. Studio does not offer the reader's own features there: no ☆, no 📌, no recent views, no Save as my view. It shows when the object has one list view too, so New view is always one click away. A read-only package switches views and offers no New view. + - **The Properties panel no longer holds a view picker, Add view or "Back to …".** It keeps the open view's settings, and its title is the view's name as the switcher gives it (the label the object's served `listViews` carry). A view with no label is named "Default view" or by its key; the header names the object before it. + - **The open view is in the address.** Switching writes `?view=.` beside `?surface=` and `?nav=`; the entry's own view writes none. A reload or a shared link opens that view again, a view only drafted in this package included. A link naming a view of another object is not taken. A pending edit is still sent before a switch, and an edit that cannot be sent still asks first. + - **`ObjectViewTitleSwitcher`: the pin is optional.** `onPinnedViewChange`, `pinnedViewId` and `recentViewIds` may be left out; without `onPinnedViewChange` no row draws 📌 and no Pinned section is listed. The running list passes all three, so it is unchanged. + - **`@object-ui/i18n` (studio namespace):** adds `engine.studio.inspector.listView.defaultView`; removes `defaultTitle`, `namedTitle`, `addView`, `backToEntry`, `switchView` and `switchViewDraft`, which only the removed panel controls read. +- d3edfb5: fix(app-shell): the console picks a signed-in user's UI language in the server's `resolveLocale` order + + Clause-②: no + Metadata-protocol: none + + - **Before.** `pickRenderableLocale` matched `sys_user.locale` against the offered locales exactly, then by base language only. A row stored mis-cased (`zh-cn`, written before the server door folded case and served as stored), or a tag the deployment offers only as a variant (`zh` or `zh-Hans-CN` against `zh-CN`), matched nothing, so the console rendered English. + - **Now.** It uses the order the server's i18n routes resolve with: exact, then case-insensitive, then base language, then the first offered variant of that base language. `zh-cn`, `zh` and `zh-Hans-CN` render `zh-CN`; `en-GB` renders `en`. A tag no offered locale shares a language with still matches nothing, and the device language keeps deciding, as before. + - No schema, key or server behaviour changes. +- a9fb14b: fix(app-shell): the Studio form designer lays out the form the app opens, so an object with a default form view no longer edits a layout users never see + + Clause-②: no + Metadata-protocol: none + + - **Which layout.** Studio → Data → an object → Form asks the app's own resolver which form view New and Edit open (`selectRecordFormView`, over the shell's merged objects). With none, the designer edits the object's `fieldGroups`, as before. With one (an `isDefault` form view, or an `OBJECT.create` view for New), the designer shows that view's sections and every layout edit (moving fields, adding, renaming, moving and deleting sections) is saved to the view's draft, which the package publish publishes. A chip in the toolbar names the layout being edited ("Form view: OBJECT.form" or "Field groups"). + - **New and Edit.** Where New and Edit open different form views, a New / Edit switch in the toolbar picks which one the designer and the Preview show. A pending edit of the shown view is sent before the other one loads. + - **Fields not on the form.** On a form view, a field the view lists in no section is not on the form; the designer keeps such fields in a "Not on this form" bucket, and dragging one into a section adds it. A field entry's own keys (`required`, `readonly`, …), a section's other keys, and entries the canvas does not show are written back as they were. A section that references a field group (`group`) keeps the reference until its members are changed. + - **Read-only base.** On a read-only base the view is customized, not edited: the edit is saved into the base's paired customization package as a declared override of the base's view (ADR-0146 D2), the same door list views use. + - **Preview.** The Form tab's Preview now lays the form out by the same form view (its sections, and the tabbed layout), as the dialog it previews does; before, it drew the object's field groups whatever form view the app opened. + - **`@object-ui/i18n`.** English and Chinese strings for the chip, the New / Edit switch and the "Not on this form" bucket. +- 6cc0b81: feat(app-shell): Studio customizes an installed app's navigation into the base's customization package (#30059) + + Clause-②: no + Metadata-protocol: none + + On a read-only (installed or code-shipped) package that has a paired customization package (`.custom`), the Interfaces rail now edits the app's navigation. Before, the rail's Edit button showed only on a writable package and its "+" offered only new pages, dashboards and reports, so an object made in the pair could be reached only by a typed URL. + + - Edit and the "+" open the same navigation editor a writable package uses (add, rename, reorder, group and remove entries, including the ones the base shipped). Nothing is written until an edit is saved. The first saved edit copies the whole app into the pair, declaring `overrides: { package: , basedOn: }`, the same route a view's Customize takes. Later edits update that copy. The base package is never written. + - The entry pickers list the pair's objects, pages, dashboards, reports and views after the base's, so a new object in the pair can be put on the menu. An entry bound to an object only the pair holds carries `requiresObject: `, so the runtime hides it if the object is later removed instead of linking to a missing object. + - The navigation change is a draft in the pair's pending changes and publishes in the same batch (one commit) as the object it opens. + - The rail marks a customized navigation with the same compact "customized" mark its rows carry, "only customizations" counts it and keeps the navigation listed, and the mark's popover offers "Restore package original", which deletes the pair's draft and copy of the app after a confirmation. + + A customized app is a whole copy: a navigation entry the vendor adds in a later version does not appear until the customization is restored and redone. A writable package's navigation editing is unchanged. No spec, server or metadata-type change. +- 797f925: feat(app-shell): Studio customizes an installed package's page into the base's customization package (#30063) + + Clause-②: no + Metadata-protocol: none + + On a read-only (installed or code-shipped) package, a page opened in the Interfaces pillar now offers Customize, the same button a dashboard, report or view shows. Before, Studio showed the page as read-only with no way to change it, although the server already serves a page override from the paired customization package (`.custom`). + + - Customize unlocks the page designer. Nothing is written until an edit is saved. The first saved edit copies the whole page into the pair, declaring `overrides: { package: , basedOn: }`. Later edits update that copy. The base package is never written. + - The edit is a draft in the pair's pending changes; once published, the running app serves the customized page at its usual address. + - The page's header carries the override mark, whose popover offers "Restore package original": it deletes the pair's draft and copy of the page after a confirmation, and the shipped page reads back. + - The Interfaces rail marks a customized page, "only customizations" counts and lists it, and the All metadata directory's page tile counts it. + + A customized page is a whole copy: a change the vendor ships to that page in a later version does not appear until the customization is restored and redone. A writable package's page editing is unchanged. No spec, server or metadata-type change. +- 6f095b9: feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. + - **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". + - **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. + - **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. + - **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. +- eb6199b: feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, and 保存 writes the panel back into that view + + Clause-②: no + Metadata-protocol: none + + - **Opening one of the reader's own views (My views) shows its conditions as ordinary rows in the Filter panel**, with the active count on the Filter control. They are the list's filter while they are there: not applied under the list a second time, and not a personalization. A stored filter the panel cannot hold (an any-of group beside other conditions), or one naming a field the object no longer declares or the reader cannot read, stays applied under the list, as for any other view. + - **Editing those rows marks the list 已个性化, and the marker offers 保存 to that view** (for a reader allowed to update their views). 保存 replaces the row's `config` through the data API with the body Save as my view builds, so the stored `ListView` shape is unchanged and no key is added; the reader's temporary list state for the view is then cleared and the list opens on the saved view. Edits not saved stay the reader's temporary list state and are never written to the view. A panel the fold cannot write losslessly keeps the refusal Save as my view gives, and nothing is written. + - **Save as my view now clears the view it came from**: that view's stored list state (sort, filters, column layout, density, hidden fields) and cached search term go, and the address being left loses its `uf_*` params, so the source no longer reads filtered or 已个性化 after the save. + - Both saves read the Filter panel as the list reports it, not the address's copy, so an emptied panel saves as no filter. + - `@object-ui/plugin-list`: `ListView`'s host prop `viewDefaults` accepts `filters`, the view's own conditions for a host that shows them as panel rows; the `filters` personalization part is then judged against them. +- dfafae8: feat(plugin-list): a reader can add any field they may read to their own list from the Hide Fields popover (#30077) + + Clause-②: no + Metadata-protocol: none + + The Hide Fields popover on a Console object list (the toolbar's, and the Hide Fields section of the Display panel) listed only the columns the view already has, so a reader who wanted another field on their list had to ask an administrator to change the view for everyone. After the view's columns it now lists every other field of the object the reader may read, unticked: business fields first, then system fields such as Owner. A field the reader may not read is not offered, and neither is a field the object definition marks `hidden`. Ticking one adds it as a column of the reader's own list; unticking it takes it off again. + + The addition is the reader's personalization, stored beside their hidden fields in their `ui.list.` preference row (`perView..addedFields`), never written into the view: another user sees the view's columns, and "Reset to view" drops it with the rest of the reader's list. While it is stored the list's Personalized marker names "Column layout". An added column is fetched and drawn like a declared one, and an added lookup field is expanded in the list read as a declared lookup column is. + + A column the view declares with no label of its own (a bare `{ field }` or field name) is now listed in the Hide Fields popover (and in the Group and Row color pickers, which share its list) under the object definition's field label, the label the grid's header shows, instead of under its machine name (`expected_revenue` now reads Expected Revenue); a column's own label and a translation still come first. + + It is offered wherever a Hide Fields choice is stored: a view that declares `columns`, and a saved view. On a system view or fallback tab that declares no columns the popover lists the view's columns alone, as before. + + For a `ListView` host: two new host props, `addedFields` (the names among `schema.columns` the reader added) and `onAddedFieldsChange` (the door that stores them). Without `onAddedFieldsChange` the popover is exactly what it was. No metadata key and no spec change. +- 4be9734: fix(automation)!: a `currency` or `percent` screen field refuses a non-number on resume, as a `number` field does, and the console renders `percent` as a numeric input (#30178) + + Clause-②: yes (narrowing) — `POST …/runs/RUN/resume` refuses a string under a `type: 'currency'` or `type: 'percent'` screen field, a bag it accepted before. Nothing is widened. + Metadata-protocol: none + + + + **BREAKING** accept-set narrowing at the screen-flow resume door. + + - **Before.** A `type: 'percent'` or `type: 'currency'` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too. The bound compares numbers, so it never judged a string, and the run consumed its suspension on the value. The console rendered `percent` as a text input and posted a string, so a `percent` field's `min` / `max` held for no console user. + - **Now.** `number`, `currency` and `percent` are one value domain: a present value that is not a finite JSON number is refused with `invalid_type` inside `400 INVALID_SCREEN_INPUT`, the suspension is kept, and a corrected resume still lands. A numeric string is not read as the number it spells. The console renders `percent` as a numeric input carrying `min` / `max`, so it posts a number and the browser stops an over-limit value before any request. + - **Unchanged.** An absent value and a hidden field are not judged; `rating`, `slider`, `progress`, `text` and untyped fields still take a string; a JSON number on `currency` / `percent` is judged against the bound as before. + - **For a caller that skips the console.** The resume bag carries the JSON number (`60`), not a string; the refusal names the field and says it must be a number. No stored metadata changes, so there is nothing for `objectstack migrate meta` to rewrite. +- ef2ad31: fix(app-shell): a dismissed or revoked default permission set in the suggested-bindings panel can be reconsidered (#30199) + + Clause-②: no + Metadata-protocol: none + + The suggested-bindings panel (the install dialog and Marketplace package page, and Studio's Access pillar) drew a dismissed suggestion, or a revoked acceptance, as a state line with no action. An administrator who chose Dismiss could only take it back by calling `POST /api/v1/security/suggested-bindings/:id/reopen` by hand, although the server has offered that door all along. + + Such a row now offers "Reconsider". It calls the re-open door, and the row comes back as a pending suggestion with Accept and Dismiss (a Guest row with Dismiss only, as before). Re-opening grants nothing: the set reaches the Everyone position only when it is accepted again, under the same checks. The button is shown to the same callers who see Accept and Dismiss: a caller without `manage_metadata` is refused the list and sees no panel. A refusal (the row is gone, the set is no longer suggested, or the deployment's security service has no re-open door) is shown with the server's own message, and the panel re-reads the list. + + New client call: `reopenSuggestedBinding(id)`, beside `dismissSuggestedBinding`. New strings: `marketplace.suggestedBindings.reopen`, `.reopening` and `.reopenedToast` in all ten locale packs, and `engine.studio.access.suggestReopen`, `.suggestReopening` and `.suggestReopenedToast` (en, zh). +- fccde75: fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message + + Clause-②: no + Metadata-protocol: none + + After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL ]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before. +- 614e831: feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card + + Clause-②: no + Metadata-protocol: none + + The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: + + - **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. + - **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. + - **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. + + Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. + + The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [46d2b65] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [83fc096] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [f9e5bef] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [29cbfcc] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [eb6199b] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [dfafae8] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [fccde75] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/lint@18.4.0 + - @object-ui/i18n@18.4.0 + - @objectstack/formula@18.4.0 + - @object-ui/data-objectstack@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/plugin-list@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/plugin-dashboard@18.4.0 + - @object-ui/plugin-charts@18.4.0 + - @object-ui/plugin-chatbot@18.4.0 + - @object-ui/collaboration@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/plugin-calendar@18.4.0 + - @object-ui/plugin-designer@18.4.0 + - @object-ui/plugin-form@18.4.0 + - @object-ui/plugin-kanban@18.4.0 + - @object-ui/plugin-report@18.4.0 + - @object-ui/plugin-view@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/layout@18.4.0 + - @object-ui/plugin-editor@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/permissions@18.4.0 + - @object-ui/providers@18.4.0 + - @object-ui/auth@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/app-shell/package.json b/ui/packages/app-shell/package.json index c97cdd7f2bb..7fe2abe9b56 100644 --- a/ui/packages/app-shell/package.json +++ b/ui/packages/app-shell/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/app-shell", - "version": "18.3.0", + "version": "18.4.0", "private": true, "type": "module", "sideEffects": [ diff --git a/ui/packages/auth/CHANGELOG.md b/ui/packages/auth/CHANGELOG.md index 65e41fdd74c..e1ac0b33699 100644 --- a/ui/packages/auth/CHANGELOG.md +++ b/ui/packages/auth/CHANGELOG.md @@ -1,5 +1,35 @@ # @object-ui/auth +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/auth/package.json b/ui/packages/auth/package.json index 7402c36354e..e2bcfd81fe6 100644 --- a/ui/packages/auth/package.json +++ b/ui/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/auth", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Authentication system for ObjectUI with AuthProvider, useAuth hook, AuthGuard, and form components.", diff --git a/ui/packages/cli/CHANGELOG.md b/ui/packages/cli/CHANGELOG.md index 4342b81c576..198befa5e5d 100644 --- a/ui/packages/cli/CHANGELOG.md +++ b/ui/packages/cli/CHANGELOG.md @@ -1,5 +1,41 @@ # @object-ui/cli +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/cli/package.json b/ui/packages/cli/package.json index d30498b56e4..32bd537453e 100644 --- a/ui/packages/cli/package.json +++ b/ui/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/cli", - "version": "18.3.0", + "version": "18.4.0", "description": "Standalone CLI for ObjectUI — scaffold, develop, build and validate JSON/YAML schema-driven applications.", "type": "module", "homepage": "https://objectstack.ai/docs/frontend/cli", diff --git a/ui/packages/collaboration/CHANGELOG.md b/ui/packages/collaboration/CHANGELOG.md index 6b285dfcba2..0b062807fb7 100644 --- a/ui/packages/collaboration/CHANGELOG.md +++ b/ui/packages/collaboration/CHANGELOG.md @@ -1,5 +1,26 @@ # @object-ui/collaboration +## 18.4.0 + +### Patch Changes + +- Updated dependencies [b379262] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/i18n@18.4.0 + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/collaboration/package.json b/ui/packages/collaboration/package.json index 3a2bf943ca6..75607260f48 100644 --- a/ui/packages/collaboration/package.json +++ b/ui/packages/collaboration/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/collaboration", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Real-time collaboration for ObjectUI with presence tracking, live cursors, conflict resolution, and comment threads.", diff --git a/ui/packages/components/CHANGELOG.md b/ui/packages/components/CHANGELOG.md index ddb5292776d..0f4fa787421 100644 --- a/ui/packages/components/CHANGELOG.md +++ b/ui/packages/components/CHANGELOG.md @@ -1,5 +1,109 @@ # @object-ui/components +## 18.4.0 + +### Minor Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. + +### Patch Changes + +- 9049be2: fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 + + Clause-②: no + Metadata-protocol: none + + A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: + + - **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). + - **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. + - **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. + + A body table that fits shows none of these: no pinned column and the icon-only ⤢. + + `@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- 6db9198: fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) + + Clause-②: no + Metadata-protocol: none + + The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. +- bff5ba2: fix(ui): the record Edit form judges a field's `requiredWhen` as the engine does, a transition gate against the stored record, so a record already inside the gate without the value saves an unrelated edit + + Clause-②: yes (widening) + Metadata-protocol: none + + This widens the public surface of `@object-ui/core`: the new export `requiredWhenRestsOnStoredRecord` and the new `requiredWhenAtRest` field in the return type of `resolveFieldRuleState`. + + - **`@object-ui/components` form renderer:** on an Edit form (`previousValues` set), a field's `requiredWhen` is required only when the stored record complied with it. A stored record that already violates it (the predicate holds over the stored record and its value is missing, as with a contract activated before its signed-copy rule existed) is not blocked any more: it saves an edit that leaves the field empty, as the engine and REST `PATCH` already accept. This follows `Field.requiredWhen`: the write is refused only when the merged record violates the requirement AND the pre-write record complied. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A Create form enforces as before. The "Required to run" note under an empty field (the update-action heads-up) is no longer shown for such a stored record, because the engine judges the action's write against the same stored record. + - **`@object-ui/core`:** `resolveFieldRuleState` takes `statics.name` (the field's name) and returns `requiredWhenAtRest`. With `previous` and `statics.name` given, a stored record already violating `requiredWhen` resolves `required: false` and `requiredWhenAtRest: true`. A caller that passes no `name` keeps the plain verdict. New export `requiredWhenRestsOnStoredRecord(pred, name, stored, scope?)`, the stored-record check alone. A predicate that cannot be evaluated over the stored record counts it as compliant, as on the server. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + - @objectstack/sdui-parser@18.4.0 + - @object-ui/react-runtime@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/components/package.json b/ui/packages/components/package.json index 9ac01f84329..574e26eac59 100644 --- a/ui/packages/components/package.json +++ b/ui/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/components", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Standard UI component library for ObjectUI, built with Shadcn UI + Tailwind CSS", diff --git a/ui/packages/core/CHANGELOG.md b/ui/packages/core/CHANGELOG.md index b23fb93834f..6410be30441 100644 --- a/ui/packages/core/CHANGELOG.md +++ b/ui/packages/core/CHANGELOG.md @@ -1,5 +1,89 @@ # @object-ui/core +## 18.4.0 + +### Minor Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. + +### Patch Changes + +- c2be734: fix(core): a successful `operation: 'update'` action now refreshes the record it wrote, with no `refreshAfter: true` (#29935) + + Clause-②: no + Metadata-protocol: none + + The spec fills in `refreshAfter: false` on an action authored without it, and that parsed shape is what the Console is served. The server-action dispatch (`createServerActionHandler`) refreshed only when `refreshAfter !== false`, so a declared update never refreshed: the route wrote the record and answered 200, the toast reported success, and the record page kept the old values and the action button whose `visible` predicate had turned false. The list the record came from kept it under a filter it no longer matched. + + A declared `operation: 'update'` writes the record it addresses by definition, so its success is now treated as a record change whatever `refreshAfter` says, the same way a client-side `dataSource.update` already reaches every reader. The record-scoped invalidation re-reads the record page (drawer and full page) and refetches every list of that object. `refreshAfter` keeps governing every other server action: a non-update action served with the default `refreshAfter: false` still sends no re-read. The spec default is unchanged. +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- bff5ba2: fix(ui): the record Edit form judges a field's `requiredWhen` as the engine does, a transition gate against the stored record, so a record already inside the gate without the value saves an unrelated edit + + Clause-②: yes (widening) + Metadata-protocol: none + + This widens the public surface of `@object-ui/core`: the new export `requiredWhenRestsOnStoredRecord` and the new `requiredWhenAtRest` field in the return type of `resolveFieldRuleState`. + + - **`@object-ui/components` form renderer:** on an Edit form (`previousValues` set), a field's `requiredWhen` is required only when the stored record complied with it. A stored record that already violates it (the predicate holds over the stored record and its value is missing, as with a contract activated before its signed-copy rule existed) is not blocked any more: it saves an edit that leaves the field empty, as the engine and REST `PATCH` already accept. This follows `Field.requiredWhen`: the write is refused only when the merged record violates the requirement AND the pre-write record complied. Still refused: the edit that moves a record into the gate without the value, and clearing the value of a record that held it. A Create form enforces as before. The "Required to run" note under an empty field (the update-action heads-up) is no longer shown for such a stored record, because the engine judges the action's write against the same stored record. + - **`@object-ui/core`:** `resolveFieldRuleState` takes `statics.name` (the field's name) and returns `requiredWhenAtRest`. With `previous` and `statics.name` given, a stored record already violating `requiredWhen` resolves `required: false` and `requiredWhenAtRest: true`. A caller that passes no `name` keeps the plain verdict. New export `requiredWhenRestsOnStoredRecord(pred, name, stored, scope?)`, the stored-record check alone. A predicate that cannot be evaluated over the stored record counts it as compliant, as on the server. +- 31260ca: fix(core): a compareTo widget on an already-past date range (yesterday, last week / month / quarter / year, last N days) compares with the period before it, and previousYear from 29 February stays in February + + Clause-②: no + Metadata-protocol: none + + - **`shiftFilterByCompareTo`, `kind: 'previousPeriod'`.** `{yesterday}` and the `last_*` tokens were never moved, so a dashboard filtered to `yesterday`, `last_week`, `last_month`, `last_quarter` or `last_year` compared the window with itself and the delta always read 0. They now compare with the period before them: `{yesterday}` with `{2_days_ago}`, and `last_week_*` … `last_year_*` with the week, month, quarter or year before last. The macro vocabulary has no token for "two periods back", so those windows are computed: the `last_*` start is resolved in the display zone, moved back one period, and the end is the day before it. + - **Rolling windows.** `{N_days_ago}` … `{today}` (`last_7_days`, `last_30_days`, `last_90_days`; N + 1 days) moved only its end and compared with the N days ending yesterday, which overlap it. It now compares with the N + 1 days before it, `{(2N+1)_days_ago}` … `{(N+1)_days_ago}`, the same window the dataset executor computes. + - **`kind: 'previousYear'` from 29 February** re-resolved the filter at 1 March of the year before (`new Date(2027, 1, 29)`), so `this_month` compared with March. The day is now clamped to the month's last day, 28 February. + - **`compareToTrendLabelKey`** reads all of these as `vsPreviousPeriod` (they read `vsLastWeek`, `vsLastMonth`, `vsLastQuarter`, `vsLastYear` and `vsYesterday`); no `dashboard.trend.*` key names "the month before last". `today`, the `this_*` and the forward presets compare and label as before. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [0f3ee58] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [29cbfcc] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @objectstack/formula@18.4.0 + - @object-ui/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/core/package.json b/ui/packages/core/package.json index 5e18cb8cf92..f433ed780a3 100644 --- a/ui/packages/core/package.json +++ b/ui/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/core", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/ui/packages/create-plugin/CHANGELOG.md b/ui/packages/create-plugin/CHANGELOG.md index 5aad5df7eaf..a851ba1d4c7 100644 --- a/ui/packages/create-plugin/CHANGELOG.md +++ b/ui/packages/create-plugin/CHANGELOG.md @@ -1,5 +1,9 @@ # @object-ui/create-plugin +## 18.4.0 + +No changes in this release. + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/create-plugin/package.json b/ui/packages/create-plugin/package.json index da14617d7ee..185d281d65e 100644 --- a/ui/packages/create-plugin/package.json +++ b/ui/packages/create-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/create-plugin", - "version": "18.3.0", + "version": "18.4.0", "description": "CLI tool to scaffold ObjectUI plugins", "type": "module", "license": "MIT", diff --git a/ui/packages/data-objectstack/CHANGELOG.md b/ui/packages/data-objectstack/CHANGELOG.md index 216ea98c567..b3e14b88fb8 100644 --- a/ui/packages/data-objectstack/CHANGELOG.md +++ b/ui/packages/data-objectstack/CHANGELOG.md @@ -1,5 +1,60 @@ # @object-ui/data-objectstack +## 18.4.0 + +### Patch Changes + +- 83fc096: Clause-②: no + Metadata-protocol: none + + A `ConnectionError` for a request nothing answered (an unreachable host, a rejected `fetch`) no longer reads as a server that answered with a 503 (#29916). + + - The `ConnectionError` constructor leaves `statusCode` undefined when the caller passes none. Before, it stamped `503`. `connect()` builds it that way when discovery's `fetch` rejects, every data method opens with `await this.connect()`, and the post-connect `error` connection state builds it the same way, so a read against a down backend was taken for the server's own answer. `answeredLoadFailure` now returns `null` for it and `classifyLoadError` reads `network`, so a failed-read state keeps its "check your connection" sentence. + - A discovery read the server answered with an error status (`!res.ok`) now carries that status (a 401 reads as `unauthorized`), instead of the invented 503. + - An answered 503 or 504 through `createErrorFromResponse` keeps its status, as before. +- 6f095b9: feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. + - **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". + - **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. + - **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. + - **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + - @objectstack/client@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/data-objectstack/package.json b/ui/packages/data-objectstack/package.json index 6da7a12cc75..38faa54cbc2 100644 --- a/ui/packages/data-objectstack/package.json +++ b/ui/packages/data-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/data-objectstack", - "version": "18.3.0", + "version": "18.4.0", "description": "ObjectStack Data Adapter for ObjectUI", "license": "MIT", "type": "module", diff --git a/ui/packages/fields/CHANGELOG.md b/ui/packages/fields/CHANGELOG.md index 5a434cd690e..2493b96b23a 100644 --- a/ui/packages/fields/CHANGELOG.md +++ b/ui/packages/fields/CHANGELOG.md @@ -1,5 +1,59 @@ # @object-ui/fields +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/permissions@18.4.0 + - @object-ui/providers@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/fields/package.json b/ui/packages/fields/package.json index 6065cda8360..3df68e8725f 100644 --- a/ui/packages/fields/package.json +++ b/ui/packages/fields/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/fields", - "version": "18.3.0", + "version": "18.4.0", "description": "Field renderers and registry for ObjectUI", "license": "MIT", "type": "module", diff --git a/ui/packages/i18n/CHANGELOG.md b/ui/packages/i18n/CHANGELOG.md index 57674ebe0bf..b138cc14c53 100644 --- a/ui/packages/i18n/CHANGELOG.md +++ b/ui/packages/i18n/CHANGELOG.md @@ -1,5 +1,149 @@ # @object-ui/i18n +## 18.4.0 + +### Minor Changes + +- aa57c76: feat(spec): three forward date-range presets: `next_month`, `next_quarter` and `next_30_days` + + Clause-②: yes (widening) + Metadata-protocol: added + + The relative dates a dashboard date filter and the filter builder offer looked only backward. A forecast asks forward: what closes next month, next quarter, in the next 30 days. Those questions had to use a custom range, whose fixed dates go stale. + + `DATE_RANGE_PRESETS` now also declares `next_month` (the whole of next month), `next_quarter` (the whole of next quarter) and `next_30_days` (today and the 30 days after it, the mirror of `last_30_days`). Every place that accepts a preset accepts them: `dashboard.dateRange.defaultRange`, a date global filter's `defaultValue`, the filter builder's `between` row and `AnalyticsQuery.timeDimensions[].dateRange`. They sit beside the family they extend in the filter bar: `next_month` after `last_month`, `next_quarter` after `last_quarter`, `next_30_days` after `last_90_days`. Their `DATE_RANGE_PRESET_MACRO_WINDOWS` rows follow the inclusive last-day convention: `['{next_month_start}', '{next_month_end}']`, `['{next_quarter_start}', '{next_quarter_end}']` and `['{today}', '{30_days_from_now}']`. A preset name written as a bare filter comparand is refused with that window as the prescription, as for the other presets. + + `@objectstack/core`'s `resolveAnalyticsDateRangePreset` resolves the three in its half-open form. `next_30_days` stops before `{31_days_from_now}`. `next_month` and `next_quarter` stop before the month and the quarter after next, which no macro token names: the resolver reads `next_month_start` (or `next_quarter_start`) again from the start of the day it named, so the end is still a day the one token resolver computed. No macro token is added. + + In the console, the three presets have labels in all ten locale packs, and the client date-macro resolver now answers `{next_month_end}` and `{next_quarter_end}`, two spec tokens it did not resolve before. `@objectstack/verify`'s dashboard-window proof also writes fixture rows on a forward preset's future edges, so it judges the three new windows; the days it writes for the other presets are unchanged. `@objectstack/lint` changes documentation only. + +### Patch Changes + +- b379262: fix(app-shell): the metadata editor's save-status chip says "Not saved" while a save is held, instead of promising an auto-save that never comes (#29901) + + Clause-②: no + Metadata-protocol: none + + On the metadata editor page (Studio's All metadata item page, Setup's metadata pages) with auto-save on, a draft the server refused with `422 INVALID_METADATA` stays dirty, and the chip beside Save kept reading "Auto-saving…" although the auto-save is held until the refused part changes and no further save is sent. The same happened while the inspector holds a fault it reports (a CEL predicate that does not parse), which holds the auto-save the same way. + + The chip now has a held state: while either hold stands on a dirty draft it reads "Not saved — fix the errors to save" in the destructive tone, with the Save button's own explanation as its tooltip. It returns to the usual states as soon as the hold lifts: editing the part the server named, or fixing the inspector fault, lets the auto-save run and the chip reads "Auto-saving…", then "Saved". Nothing about when a save is sent or held changes. New string: `engine.edit.notSavedHeld` (en, zh). +- 9049be2: fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 + + Clause-②: no + Metadata-protocol: none + + A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: + + - **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). + - **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. + - **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. + + A body table that fits shows none of these: no pinned column and the icon-only ⤢. + + `@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. +- 03be565: fix(app-shell): New automation no longer saves a typed prefix as the trigger object (#30038) + + Clause-②: no + Metadata-protocol: none + + In Studio's New automation dialog, typing part of an object name and pressing Enter committed the typed text itself, so the flow was created bound to an object that does not exist. The trigger-object picker there now takes objects only: Enter or leaving the field commits the object the text names exactly, else the only match left in the list, else no object, and the picker says no object is chosen. A record preset then keeps the dialog open until an object is picked. + + Every other object picker keeps its free text, because a flow reference may hold an expression. +- 7e9fa4b: fix(app-shell): Studio on a zh-CN console reads in Chinese in the view, page and report panels, the publish sheet, the flow Problems tab and the save-notice / notify nodes, and calls a dashboard 仪表盘 everywhere + + Clause-②: no + Metadata-protocol: none + + The view, page and report property panels render the spec's authoring forms from the console's own copy of the spec, and only the dashboard form had a zh-CN overlay, so section headings such as Basics, Columns & filters and Navigation & sharing, their descriptions and schema-titled rows such as Searchable Fields read in English. The overlay now carries every `METADATA_FORM_REGISTRY` type, worded as the platform's own zh-CN metadata-forms bundle words them: view, page and report in full, the other types by field label, which is what the Pending changes sheet names a changed property by (a flow's `~ Status` now reads `~ 状态`). The sheet's summary line counts datasets as 数据集 instead of the raw type name. + + A flow whose trigger is not armed on this deployment showed the engine's developer sentence under 未在此运行, above 没有问题 —— 该流程结构有效. The note now says, from the engine's `unboundCause` code, what this deployment does not do and who can change it, keeps the engine's sentence as the hover detail, and the line below it says the structure checks found nothing to fix instead of "no problems". A cause the console has no row for still shows the engine's sentence. + + The palette names the save-notice node 保存提示, and the notify node's Template and Template data fields and the save-notice Message field read in Chinese. English consoles read the same English as before. zh-CN Console and Studio text uses 仪表盘 for dashboard throughout. +- 42afac7: feat(app-shell): Studio Interfaces picks an object's list view from the header title, object / view ▾, the way the running list does, and the open view stays in the address + + Clause-②: no + Metadata-protocol: none + + - **The view switcher is in the header's title.** An `object` entry's header reads object / view ▾. The switcher is the running list's own dropdown (`ObjectViewTitleSwitcher`): the object's list views by label, a view only drafted in this package included, a search box, and New view, which opens the Create View dialog. Studio does not offer the reader's own features there: no ☆, no 📌, no recent views, no Save as my view. It shows when the object has one list view too, so New view is always one click away. A read-only package switches views and offers no New view. + - **The Properties panel no longer holds a view picker, Add view or "Back to …".** It keeps the open view's settings, and its title is the view's name as the switcher gives it (the label the object's served `listViews` carry). A view with no label is named "Default view" or by its key; the header names the object before it. + - **The open view is in the address.** Switching writes `?view=.` beside `?surface=` and `?nav=`; the entry's own view writes none. A reload or a shared link opens that view again, a view only drafted in this package included. A link naming a view of another object is not taken. A pending edit is still sent before a switch, and an edit that cannot be sent still asks first. + - **`ObjectViewTitleSwitcher`: the pin is optional.** `onPinnedViewChange`, `pinnedViewId` and `recentViewIds` may be left out; without `onPinnedViewChange` no row draws 📌 and no Pinned section is listed. The running list passes all three, so it is unchanged. + - **`@object-ui/i18n` (studio namespace):** adds `engine.studio.inspector.listView.defaultView`; removes `defaultTitle`, `namedTitle`, `addView`, `backToEntry`, `switchView` and `switchViewDraft`, which only the removed panel controls read. +- a9fb14b: fix(app-shell): the Studio form designer lays out the form the app opens, so an object with a default form view no longer edits a layout users never see + + Clause-②: no + Metadata-protocol: none + + - **Which layout.** Studio → Data → an object → Form asks the app's own resolver which form view New and Edit open (`selectRecordFormView`, over the shell's merged objects). With none, the designer edits the object's `fieldGroups`, as before. With one (an `isDefault` form view, or an `OBJECT.create` view for New), the designer shows that view's sections and every layout edit (moving fields, adding, renaming, moving and deleting sections) is saved to the view's draft, which the package publish publishes. A chip in the toolbar names the layout being edited ("Form view: OBJECT.form" or "Field groups"). + - **New and Edit.** Where New and Edit open different form views, a New / Edit switch in the toolbar picks which one the designer and the Preview show. A pending edit of the shown view is sent before the other one loads. + - **Fields not on the form.** On a form view, a field the view lists in no section is not on the form; the designer keeps such fields in a "Not on this form" bucket, and dragging one into a section adds it. A field entry's own keys (`required`, `readonly`, …), a section's other keys, and entries the canvas does not show are written back as they were. A section that references a field group (`group`) keeps the reference until its members are changed. + - **Read-only base.** On a read-only base the view is customized, not edited: the edit is saved into the base's paired customization package as a declared override of the base's view (ADR-0146 D2), the same door list views use. + - **Preview.** The Form tab's Preview now lays the form out by the same form view (its sections, and the tabbed layout), as the dialog it previews does; before, it drew the object's field groups whatever form view the app opened. + - **`@object-ui/i18n`.** English and Chinese strings for the chip, the New / Edit switch and the "Not on this form" bucket. +- 6f095b9: feat(studio): customize a read-only package's dataset in the All metadata area, see what uses it before publishing, and read a refused publish at the dataset + + Clause-②: yes (widening) + Metadata-protocol: none + + - **Customize a dataset.** On a read-only base package, Studio's All metadata area opens a dataset in its form editor with the dataset designer and live preview, and offers Customize. The first saved edit copies the whole dataset into the base's paired `.custom` package, declaring `overrides: { package, basedOn }`; the header's Publish applies it, and "Restore package original" on the override mark deletes the copy. On a writable package a dataset still opens the generic item page. + - **What uses it.** While a dataset is customized, the editor lists the dashboards, reports and other items that name it, read from the References read, so the person sees what a published change reaches. A failed read says it failed and offers to read again; it never reads as "nothing uses it". + - **A refused publish at the item.** A publish the server refuses for a dataset is placed on its measure or dimension and the designer's input (`Not published. Check Aggregate of the measure "Total Amount"`), in the Console's language, and shown at the dataset's editor as well as on its Pending changes entry. The server's own text stays under Details. + - **References read fixed.** `MetadataClient.references()` now reads the route's declared answer (`{ references: [{ type, name, label, path, kind }] }`) and maps each element to `MetadataReference` (`fromType`, `fromName`, new optional `fromLabel` and `kind`). It used to read a key the route never serves, so Setup's References tab and Studio's item tools showed "Nothing in the metadata graph points at this item. Safe to delete." for every item. An answer the declaration refuses is now a failed read. + - **Published surface.** `MetadataReference`, exported from `@object-ui/data-objectstack`, gains two optional fields, `fromLabel` (the referring item's label, when the server serves one) and `kind` (the served reference kind); existing fields are unchanged. `MetadataClient.references()` now accepts less: a `200` answer that `FindReferencesToMetaResponseSchema` refuses throws instead of resolving `[]`, so a caller must handle the rejection as it already does for a non-2xx answer; in this repository every caller reads it through `useReferencesScan`, which catches it and shows the failed-check state. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- ef2ad31: fix(app-shell): a dismissed or revoked default permission set in the suggested-bindings panel can be reconsidered (#30199) + + Clause-②: no + Metadata-protocol: none + + The suggested-bindings panel (the install dialog and Marketplace package page, and Studio's Access pillar) drew a dismissed suggestion, or a revoked acceptance, as a state line with no action. An administrator who chose Dismiss could only take it back by calling `POST /api/v1/security/suggested-bindings/:id/reopen` by hand, although the server has offered that door all along. + + Such a row now offers "Reconsider". It calls the re-open door, and the row comes back as a pending suggestion with Accept and Dismiss (a Guest row with Dismiss only, as before). Re-opening grants nothing: the set reaches the Everyone position only when it is accepted again, under the same checks. The button is shown to the same callers who see Accept and Dismiss: a caller without `manage_metadata` is refused the list and sees no panel. A refusal (the row is gone, the set is no longer suggested, or the deployment's security service has no re-open door) is shown with the server's own message, and the panel re-reads the list. + + New client call: `reopenSuggestedBinding(id)`, beside `dismissSuggestedBinding`. New strings: `marketplace.suggestedBindings.reopen`, `.reopening` and `.reopenedToast` in all ten locale packs, and `engine.studio.access.suggestReopen`, `.suggestReopening` and `.suggestReopenedToast` (en, zh). +- eca7cd2: feat(i18n): `chatbot.tool.create_record` / `update_record` / `remove_sample_data` labels and the record-write card's `console.ai.recordWrite*` labels in all ten packs; `chatbot.tool.draft_record` retired + + Clause-②: no + Metadata-protocol: none + + The spec registry now lists the cloud chat agents' confirmed record write and the sample-data cleanup (cloud#3565) and drops `draft_record`, so the chatbot's tool timeline gets a sentence for each new step in every pack ("Create the record" / "新建记录", "Change the record" / "修改记录", "Remove the sample data" / "清理示例数据") and loses the retired one, keeping `toolLabels-locale-parity-7481` and `toolLabels-sentences-12724` green. The console's record-write card gets its eleven labels under `console.ai.recordWrite*`: the create and update titles (with the object's label), the three buttons 确认 / 打开完整表单 / 取消, the two stored reasons, the "not included" lead-in and the three states (writing, written, cancelled) — plus the 「清理示例数据」 card's four: `console.ai.sampleDataCleanupTitle` (its title over the seeded tables and counts), `console.ai.sampleDataCleanupIrreversible` (「删除后无法恢复」 — the removal cannot be undone, said on every card), and the two count families `console.ai.sampleDataCleanupAbsent` (the recorded rows the card does not list) and `console.ai.sampleDataCleanupEdited` (the rows kept because someone edited them), each with every plural slot its language selects (objectui#11445). +- 614e831: feat(app-shell): Connect an Agent adds connect cards for WorkBuddy, Doubao Work and QwenWork, and drops the VS Code card + + Clause-②: no + Metadata-protocol: none + + The Connect an Agent page (Account → Developer, and Setup) now carries three more client cards, each filled in with the deployment's MCP URL, listed after the Claude, Codex and Cursor cards: + + - **WorkBuddy** — the `mcpServers` entry for Connectors → Custom connector → Configure MCP, with the `streamableHttp` transport spelling WorkBuddy reads, and the reminder that the connector stays inactive until it is trusted. + - **Doubao Work** — the name, transport and server URL to type into Skills · Connectors → New → New custom connector. + - **QwenWork** — the `mcpServers` JSON for Extensions → Connectors → Paste JSON config, with the `streamable-http` spelling QwenWork reads. + + Each of the three names the fallback for a client version without the browser sign-in: the request header `Authorization: Bearer `. A minted key is now revealed in that form as well as the existing `x-api-key` line. + + The claude.ai / Claude Desktop and Claude Code cards merge into one full-width Claude card with a half for each. The Codex CLI card becomes a Codex card for the Codex app: the name, transport and URL to enter under Settings → MCP servers → Add server, and where to sign in (new string `connectAgent.codex.hint`); it no longer shows a `config.toml` snippet. The VS Code / Copilot card is gone, with its string `connectAgent.vscode.body`; the generic `mcpServers` snippet on the Cursor card and the MCP URL at the top still serve any MCP client. The client cards and the API-key card form one list, the API-key card last. Long one-line snippets wrap instead of being clipped. The Doubao Work and QwenWork card titles are strings too (`connectAgent.doubao.title`, `connectAgent.qwenwork.title`), so the zh pack shows the products' own names, 豆包工作 and 千问办公. New strings live under `connectAgent.fields`, `connectAgent.workbuddy`, `connectAgent.doubao` and `connectAgent.qwenwork`, translated in all ten packs. Nothing on the server changes; `@objectstack/mcp` updates only its description of the page. +- Updated dependencies [c2be734] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] + - @object-ui/core@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/i18n/package.json b/ui/packages/i18n/package.json index 641ccc97f2f..29444d0270b 100644 --- a/ui/packages/i18n/package.json +++ b/ui/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/i18n", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/ui/packages/layout/CHANGELOG.md b/ui/packages/layout/CHANGELOG.md index 0d053f35a5f..2e4c2216130 100644 --- a/ui/packages/layout/CHANGELOG.md +++ b/ui/packages/layout/CHANGELOG.md @@ -1,5 +1,45 @@ # @object-ui/layout +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/layout/package.json b/ui/packages/layout/package.json index 0a6473711f1..78b895c5bbc 100644 --- a/ui/packages/layout/package.json +++ b/ui/packages/layout/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/layout", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": [ "./dist/index.js", diff --git a/ui/packages/mobile/CHANGELOG.md b/ui/packages/mobile/CHANGELOG.md index e7faae4a991..76823bc21a3 100644 --- a/ui/packages/mobile/CHANGELOG.md +++ b/ui/packages/mobile/CHANGELOG.md @@ -1,5 +1,12 @@ # @object-ui/mobile +## 18.4.0 + +### Patch Changes + +- Updated dependencies [a52761f] + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/mobile/package.json b/ui/packages/mobile/package.json index 1b281add61e..6bb93afa013 100644 --- a/ui/packages/mobile/package.json +++ b/ui/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/mobile", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Mobile optimization for ObjectUI with responsive components, PWA support, and touch gesture handling.", diff --git a/ui/packages/permissions/CHANGELOG.md b/ui/packages/permissions/CHANGELOG.md index 01a19c8735b..671cd19abd2 100644 --- a/ui/packages/permissions/CHANGELOG.md +++ b/ui/packages/permissions/CHANGELOG.md @@ -1,5 +1,36 @@ # @object-ui/permissions +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/permissions/package.json b/ui/packages/permissions/package.json index 1d091051172..79c364e1e7d 100644 --- a/ui/packages/permissions/package.json +++ b/ui/packages/permissions/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/permissions", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "RBAC permission system for ObjectUI with object/field/row-level access control, permission guards, and hooks.", diff --git a/ui/packages/plugin-calendar/CHANGELOG.md b/ui/packages/plugin-calendar/CHANGELOG.md index 8a93b2109fe..3fcf9b9ccde 100644 --- a/ui/packages/plugin-calendar/CHANGELOG.md +++ b/ui/packages/plugin-calendar/CHANGELOG.md @@ -1,5 +1,40 @@ # @object-ui/plugin-calendar +## 18.4.0 + +### Patch Changes + +- Updated dependencies [b379262] +- Updated dependencies [c2be734] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-calendar/package.json b/ui/packages/plugin-calendar/package.json index 684d5e812e5..b568217a38d 100644 --- a/ui/packages/plugin-calendar/package.json +++ b/ui/packages/plugin-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-calendar", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Calendar view plugins for ObjectUI - includes both ObjectQL-integrated and standalone calendar components", diff --git a/ui/packages/plugin-charts/CHANGELOG.md b/ui/packages/plugin-charts/CHANGELOG.md index df3c0b47b7a..8fbc932360d 100644 --- a/ui/packages/plugin-charts/CHANGELOG.md +++ b/ui/packages/plugin-charts/CHANGELOG.md @@ -1,5 +1,42 @@ # @object-ui/plugin-charts +## 18.4.0 + +### Patch Changes + +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- Updated dependencies [b379262] +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-charts/package.json b/ui/packages/plugin-charts/package.json index 177da6a2d51..dc69e33ec14 100644 --- a/ui/packages/plugin-charts/package.json +++ b/ui/packages/plugin-charts/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-charts", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Chart components plugin for ObjectUI, powered by Recharts", diff --git a/ui/packages/plugin-chatbot/CHANGELOG.md b/ui/packages/plugin-chatbot/CHANGELOG.md index aecb1ed1178..985fafe1fe9 100644 --- a/ui/packages/plugin-chatbot/CHANGELOG.md +++ b/ui/packages/plugin-chatbot/CHANGELOG.md @@ -1,5 +1,74 @@ # @object-ui/plugin-chatbot +## 18.4.0 + +### Patch Changes + +- fccde75: fix(plugin-chatbot,app-shell): after 确认 or 取消 on the chat's confirm card, the follow-up sent to the model no longer shows as the person's own message + + Clause-②: no + Metadata-protocol: none + + After the operator confirmed or cancelled an action on the chat's confirm card, the chat sent the model a short follow-up («[HITL pa_…] The operator approved the `update_record` call … Acknowledge the outcome …») through the ordinary user-message channel, so the person saw it as their own bubble, after a reload too, and the conversation list showed it as the conversation's preview. The model still receives that follow-up unchanged. What changed is who reads it as the person's words: the chat transcript draws no bubble for it (live, after a reload, and on a shared transcript), the conversation list previews the person's last ask instead, and the conversation-language probe no longer reads it as the person's latest turn, which turned a Chinese conversation's outbound text English when the action's result carried no Chinese. The follow-up is recognised by its leading `[HITL ]` tag, the one part of a stored message that survives a reload; `@object-ui/plugin-chatbot` exports the reader as `isHitlContinuation` (also from `./core`). An ordinary message is sent, drawn and previewed as before. +- eca7cd2: feat(plugin-chatbot): the record-write card — ONE record the chat proposes and the user confirms, with 确认 / 打开完整表单 / 取消 (cloud#3565) + + Clause-②: no + Metadata-protocol: none + + The cloud chat's `create_record` / `update_record` tools queue a HITL pending action in the user's name and answer the approval envelope plus the record: `{ status:'pending_approval', pendingActionId, recordWrite:{ operation, objectName, objectLabel, recordId?, label?, reason?, fields:[{ name, label, type, value, current? }], dropped } }`. The chat now draws that as a card instead of the generic Approve / Deny strip and the raw payload: + + - **New export** `detectRecordWrite(result)` (with the `RecordWrite` and `RecordWriteField` types), lifted onto `ChatbotEnhancedToolInvocation.recordWrite` by the live mapper beside `pendingActionId`. It reads the bare object, the persisted `{ type:'text', value }` wrapper and a JSON string; an envelope without the pending id or without a field to confirm is not a card. + - **The card** (`data-testid="record-write"`): the create or update title with the object's label, the record's label and reason, every field as a labelled row — an update shows the stored value struck through, then →, then the proposed one — the fields the server left out and why, and three buttons. 确认 calls `onToolApprove(toolCallId, true)` (the pending action's dispatcher writes as the user); 取消 calls it with `false` and `recordWriteCancelReason`; 打开完整表单 calls it with `false` and `recordWriteOpenFormReason` FIRST, then the new `onOpenRecordForm(write)` prop, so a record saved in the form is never written a second time from the card (disabled where the host passes no handler). After a decision the card says written (with the record's link through `onOpenRecord`, read from the dispatcher's `{ status:'written', objectName, recordId, label }` result), cancelled, or the server's refusal. + - **New props**: `onOpenRecordForm`, `recordWriteCreateTitle(objectLabel)`, `recordWriteUpdateTitle(objectLabel)`, `recordWriteConfirmLabel`, `recordWriteOpenFormLabel`, `recordWriteCancelLabel`, `recordWriteCancelReason`, `recordWriteOpenFormReason`, `recordWriteDroppedLabel`, `recordWriteSubmittingLabel`, `recordWriteWrittenLabel`, `recordWriteCancelledLabel`, all with English defaults. + - **「清理示例数据」** rides the same seam: `detectSampleDataCleanup(result)` (with the `SampleDataCleanup` type) lifts `{ status:'pending_approval', pendingActionId, sampleDataCleanup:{ tables:[{ objectName, label, count }], total, absent, edited } }` onto `sampleDataCleanup`, and the chat draws the tables and counts (`data-testid="sample-data-cleanup"`, title from the new `sampleDataCleanupTitleLabel` prop) above the generic Approve / Deny strip, with the raw payload hidden. The card always says the removal cannot be undone (`sampleDataCleanupIrreversibleLabel`, default "Removed rows cannot be recovered." — there is no recycle bin until objectstack#3146), says how many recorded rows it does not list when `absent` is above 0 (`sampleDataCleanupAbsentLabel(count)`) and how many stay because someone edited them when `edited` is above 0 (`sampleDataCleanupEditedLabel(count)`; an envelope without `edited` reads as 0). + - **`ToolDecisionState`** (widened, additive): every state may carry `decision: 'approved' | 'rejected'`, and an approved `success` may carry `result`, the dispatcher's answer. `useHitlInChat` sets both; the generic strip keeps reading `message` alone. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-chatbot/package.json b/ui/packages/plugin-chatbot/package.json index 571deb0945c..1e0108b830b 100644 --- a/ui/packages/plugin-chatbot/package.json +++ b/ui/packages/plugin-chatbot/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-chatbot", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Chatbot interface plugin for ObjectUI", diff --git a/ui/packages/plugin-dashboard/CHANGELOG.md b/ui/packages/plugin-dashboard/CHANGELOG.md index 64e4e952f20..8affccc21ce 100644 --- a/ui/packages/plugin-dashboard/CHANGELOG.md +++ b/ui/packages/plugin-dashboard/CHANGELOG.md @@ -1,5 +1,67 @@ # @object-ui/plugin-dashboard +## 18.4.0 + +### Patch Changes + +- ff12aed: fix(core): a compareTo widget on a forward date range (next month, next quarter, next 30 days) compares with the period just before it, not with itself + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/core`: `shiftFilterByCompareTo`** with `kind: 'previousPeriod'` moved only `current_*` / `today` tokens, so a dashboard filtered to `next_month` or `next_quarter` compared the window with itself and the delta always read 0, and `next_30_days` (`{today}` … `{30_days_from_now}`) moved only its start and compared with a window that overlapped it on all but one day. A forward window now steps back one period: `{next_month_*}` becomes this month, `{next_quarter_*}` this quarter, and a `{today}` … `{N_days_from_now}` pair on one field becomes the N + 1 days that end yesterday (`{(N+1)_days_ago}` … `{yesterday}`), the same window the dataset executor computes for that range. Every other token, and the 13 backward presets, compare as before. + - **`@object-ui/core`: `compareToTrendLabelKey`** names that window: `vsThisMonth` for a forward month, `vsThisQuarter` for a forward quarter, and `vsPreviousPeriod` for a forward rolling window (it read `vsLastMonth`, `vsLastQuarter` and `vsYesterday`). + - **`@object-ui/i18n`:** `dashboard.trend.vsThisMonth` and `dashboard.trend.vsThisQuarter` in all ten locale packs (zh: 较本月, 较本季度). + - **`@object-ui/plugin-dashboard`, `@object-ui/plugin-charts`:** English fallbacks for the two keys in the metric tile and in the chart's comparison series label. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-dashboard/package.json b/ui/packages/plugin-dashboard/package.json index 987d5e309f4..d4dac9b2a2e 100644 --- a/ui/packages/plugin-dashboard/package.json +++ b/ui/packages/plugin-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-dashboard", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Dashboard plugin for ObjectUI", diff --git a/ui/packages/plugin-designer/CHANGELOG.md b/ui/packages/plugin-designer/CHANGELOG.md index da0c596c465..5ef483e627b 100644 --- a/ui/packages/plugin-designer/CHANGELOG.md +++ b/ui/packages/plugin-designer/CHANGELOG.md @@ -1,5 +1,62 @@ # @object-ui/plugin-designer +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [83fc096] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/data-objectstack@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/plugin-form@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/layout@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-designer/package.json b/ui/packages/plugin-designer/package.json index 66406e4be04..f20be20e68f 100644 --- a/ui/packages/plugin-designer/package.json +++ b/ui/packages/plugin-designer/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-designer", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Visual designer plugin for ObjectUI with page, data model, process, and report designers.", diff --git a/ui/packages/plugin-detail/CHANGELOG.md b/ui/packages/plugin-detail/CHANGELOG.md index 7cd97ad8ea7..03b1e205e65 100644 --- a/ui/packages/plugin-detail/CHANGELOG.md +++ b/ui/packages/plugin-detail/CHANGELOG.md @@ -1,5 +1,124 @@ # @object-ui/plugin-detail +## 18.4.0 + +### Patch Changes + +- 6e68d8f: fix(plugin-detail): a record page's body tables total only the columns declared additive — a version number or an instalment number is no longer summed + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** The 「Totals (this page)」 line under a body table of the default record page (a related list drawn full width in the details column) used to add up every `currency`, `number`, `integer` and `decimal` column. Ordinals were summed with the quantities: a contract's versions v1–v4 read 「版本号 10」, five instalments 「期次 15」. The line now totals: + + - a `currency` column, when every counted row is in one known currency, formatted in it. Each row's currency is the one its cell displays (`resolveFieldCurrency`): the field's code under `currencyMode: 'fixed'`, the tenant default under `dynamic`, the row's own code field under `record`. Rows in different currencies, or a row whose currency is unknown (a `record`-mode row with a blank code, a `dynamic` amount with no tenant default), give that column no total rather than a bare or wrong-currency sum. A `currencyConfig` without `currencyMode: 'fixed'` no longer formats in its `defaultCurrency`, which the spec does not read outside `fixed`; it shows the tenant default, as the column's cells do; + - a `number`, `integer` or `decimal` column only when the record's own object declares a `summary` field whose `summaryOperations` sums that child field (`function: 'sum'`, `object` naming the child). The roll-up is the author's statement that the column adds up. Without one, as for an amount stored as `number` with its currency on the parent, the column gets no total; + - nothing else. When no column qualifies the line is not drawn. +- 9049be2: fix(plugin-detail): a record page's body table that is wider than the details column says so — its first column stays pinned and ⤢ is offered as 「All columns」 + + Clause-②: no + Metadata-protocol: none + + A body table of the default record page (a related list drawn in the details column) keeps its place in that column, as ADR-0085's role-based layout puts it. When its columns are wider than the column, it now makes the overflow obvious: + + - **The first column stays pinned** while the table scrolls sideways, so a scrolled row is still identifiable (the table's existing `frozenColumns: 1`, set only while the table overflows). + - **⤢ is offered in words.** The enlarge control in the card header, icon-only before, shows a visible 「All columns」 label (`detail.relatedCardAllColumns`, all ten locales) while the table overflows. It opens the same enlarged tab as before, where every column fits. + - **The right-edge shadow** while columns remain to the right is the table's own and unchanged; it clears when the table is scrolled to its end. + + A body table that fits shows none of these: no pinned column and the icon-only ⤢. + + `@object-ui/components`: a `data-table` header label carries its full text as a `title`, so a header the ellipsis cuts can be read on hover, and the table's scroll box carries a `data-table-scroll-box` marker a host can read the overflow from. +- 6db9198: fix(ui): a greyed action's `disabledReason` tooltip wraps inside a phone-width viewport instead of running off-screen (#29954) + + Clause-②: no + Metadata-protocol: none + + The record action bar, the `record:quick_actions` button and the schema-driven `action:*` controls (button, icon, group, menu, through the shared disabled-reason trigger) rendered the authored `disabledReason` in a tooltip with no width bound, so a long reason sat on one line and was cut at the right edge of a 390px viewport. All three tooltips now carry the same `max-w-xs` bound the home page's disabled-reason tooltip uses, so the reason wraps. +- f68b47c: fix(plugin-detail): the related table leads with the record's name when the relation declares no columns + + Clause-②: no + Metadata-protocol: none + + A related list with no columns declared took its columns from the child object's `highlightFields`, which by design never list the record's title, so the related tab on a record page showed amounts and dates with no deal or contact name, and the only link in a row was its first value. The record's name (`nameField`, else the field the platform derives, ADR-0079) now leads that set, listed once even when a highlight names it, and the grid links that first column to the record. The field walk used when the child declares no `highlightFields` leads with the declared name the same way. The enlarged tab and the table in the record body read the same set. Columns an author declares on the relation are drawn exactly as declared. A `titleFormat`-only object leads with the field the platform derives, not the formatted title, which has no column behind it. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- c1240a3: feat(record page): the timeline's Changes chip holds the audit's field changes only, and is offered only while the record shows its field history + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: what Changes holds.** The timeline stream's Changes chip, and the activity panel's `changes_only` filter, hold the field changes read from the audit log (each with its before and after values) and nothing else. The activity rows that share their look (created, updated, deleted, assigned, shared) are events, so they sit under Activity whether or not the record shows its field history; before, they sat under Changes and, on the panel, under `changes_only`. + - **`@object-ui/plugin-detail`: `RecordFieldHistoryContext`.** A host tells the record page's timeline stream whether it shows the record's field history; while it says `false`, the stream draws the All / Activity / Discussion chips and no Changes chip, and a stream left on Changes goes back to All. With no provider every chip draws, as before. `RecordActivityTimeline` takes the chips to offer as `streamFilters` (all four when absent). + - **The Console's record page** hands over its `enable.trackHistory` gate, so an object that does not opt in no longer offers a Changes chip on its timeline tab. + - **The CRM example** opens `enable.trackHistory` on `crm_lead`, `crm_account`, `crm_contact` and `crm_opportunity`, so their record pages show each field change with its before and after values. The flag stays opt-in for every other object. +- 5fa9375: feat(plugin-detail): the default record page lets an editor move a record to another stage by pressing it on the stage path + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** The record page the console synthesizes for an object with a stage field (no authored page) used to draw its stage path as a read-only progress rail, so moving a deal to its next stage took 编辑 → change the stage → 更新. That page's `record:path` now carries `allowStageChange: true`, the existing opt-in (no new metadata key): + + - **Who can press.** Unchanged from the opt-in: a reader the record page lets edit this record, on a stage field that is not read-only or computed, whose field edit grant holds for the stored row. Everyone else keeps the read-only path, and so does an editor while the record's inline edit holds unsaved changes. + - **Every press asks for confirmation**, then saves the stage through the record's ordinary update with the record's version, so the object's validation rules and `state_machine` decide. A refused move is shown under the path, naming any other field the move needs (for example a field made required by the new stage); nothing is written. + - **Only reachable stages are offered.** On every page with the opt-in, a stage the stage field's blocking `state_machine` cannot reach from the stored value is drawn as a readout, not a button, the same moves the record form, the grid's inline editor and the board offer. A field with no such rule offers every stage, and the server still judges each move. + - **Authored pages keep their own setting.** A page, or an authored `highlights` slot, that declares its own `record:path` keeps whatever it declares: `true`, `false`, or the key left out (a read-only rail). + + To keep a default record page's path read-only, assign the object an authored page (or a `highlights` slot) whose `record:path` leaves `allowStageChange` out or sets it `false`. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-detail/package.json b/ui/packages/plugin-detail/package.json index 4f3abb16a13..82d4cadbdea 100644 --- a/ui/packages/plugin-detail/package.json +++ b/ui/packages/plugin-detail/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-detail", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "DetailView plugin for ObjectUI - comprehensive detail page with sections, tabs, and related lists", diff --git a/ui/packages/plugin-editor/CHANGELOG.md b/ui/packages/plugin-editor/CHANGELOG.md index 3676a47c9ff..81f0d2b907e 100644 --- a/ui/packages/plugin-editor/CHANGELOG.md +++ b/ui/packages/plugin-editor/CHANGELOG.md @@ -1,5 +1,21 @@ # @object-ui/plugin-editor +## 18.4.0 + +### Patch Changes + +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-editor/package.json b/ui/packages/plugin-editor/package.json index c722cf80a47..7bfdd759d0e 100644 --- a/ui/packages/plugin-editor/package.json +++ b/ui/packages/plugin-editor/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-editor", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Rich text editor plugin for ObjectUI, powered by Monaco Editor", diff --git a/ui/packages/plugin-form/CHANGELOG.md b/ui/packages/plugin-form/CHANGELOG.md index 1efaaa3f686..a8998458a8c 100644 --- a/ui/packages/plugin-form/CHANGELOG.md +++ b/ui/packages/plugin-form/CHANGELOG.md @@ -1,5 +1,58 @@ # @object-ui/plugin-form +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-form/package.json b/ui/packages/plugin-form/package.json index 84c462058cd..6681d3b29ec 100644 --- a/ui/packages/plugin-form/package.json +++ b/ui/packages/plugin-form/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-form", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Form plugin for ObjectUI", diff --git a/ui/packages/plugin-gantt/CHANGELOG.md b/ui/packages/plugin-gantt/CHANGELOG.md index 58bff763b13..e4eaa954a53 100644 --- a/ui/packages/plugin-gantt/CHANGELOG.md +++ b/ui/packages/plugin-gantt/CHANGELOG.md @@ -1,5 +1,63 @@ # @object-ui/plugin-gantt +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-gantt/package.json b/ui/packages/plugin-gantt/package.json index 05ed88afdfa..e4e78b2171f 100644 --- a/ui/packages/plugin-gantt/package.json +++ b/ui/packages/plugin-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-gantt", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Gantt chart plugin for ObjectUI", diff --git a/ui/packages/plugin-grid/CHANGELOG.md b/ui/packages/plugin-grid/CHANGELOG.md index 779c445196e..5cbfffb6b88 100644 --- a/ui/packages/plugin-grid/CHANGELOG.md +++ b/ui/packages/plugin-grid/CHANGELOG.md @@ -1,5 +1,81 @@ # @object-ui/plugin-grid +## 18.4.0 + +### Patch Changes + +- 8c1bf5b: fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag. + + - **`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before. + - **`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-grid/package.json b/ui/packages/plugin-grid/package.json index 9daa9379e40..9befa2d576f 100644 --- a/ui/packages/plugin-grid/package.json +++ b/ui/packages/plugin-grid/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-grid", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Grid plugin for ObjectUI", diff --git a/ui/packages/plugin-kanban/CHANGELOG.md b/ui/packages/plugin-kanban/CHANGELOG.md index f2f92f119f2..4098a1fed47 100644 --- a/ui/packages/plugin-kanban/CHANGELOG.md +++ b/ui/packages/plugin-kanban/CHANGELOG.md @@ -1,5 +1,40 @@ # @object-ui/plugin-kanban +## 18.4.0 + +### Patch Changes + +- Updated dependencies [b379262] +- Updated dependencies [c2be734] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-kanban/package.json b/ui/packages/plugin-kanban/package.json index 8eed708a77b..8977523110e 100644 --- a/ui/packages/plugin-kanban/package.json +++ b/ui/packages/plugin-kanban/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-kanban", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Kanban board plugin for ObjectUI, powered by dnd-kit", diff --git a/ui/packages/plugin-list/CHANGELOG.md b/ui/packages/plugin-list/CHANGELOG.md index 1e2fe510dc4..04dc385aa12 100644 --- a/ui/packages/plugin-list/CHANGELOG.md +++ b/ui/packages/plugin-list/CHANGELOG.md @@ -1,5 +1,105 @@ # @object-ui/plugin-list +## 18.4.0 + +### Patch Changes + +- 8c1bf5b: fix(plugin-list): a stored view sort the platform refuses is no longer sent, so the list loads instead of failing; a column on a `sortable: false` field offers no sort when the grid sorts in the browser + + Clause-②: no + Metadata-protocol: none + + **Visible behaviour change on a released page.** A list view whose stored `sort` (or a restored user sort) names a field the platform will not order by used to send that sort with its data request. The Data API answers such a sort `400 INVALID_SORT`, so the list showed its load error instead of its rows. Fields the platform refuses are the ones its sortability projection, served with the object's metadata, marks unsortable: a `formula` field today, and a field marked `sortable: false` once the platform honours that flag. + + - **`@object-ui/plugin-list`:** the list's data request, its server export and the kanban, calendar, timeline and gantt views (which fetch their own rows) now send the sort without the refused entries. The entries left keep their original order, and a sort with nothing refused in it is sent unchanged. The refused entry is still listed in the sort picker as removable only, as before, and the next sort the list saves leaves it out. With no projection served (an older platform, inline rows), the sort is sent as before. + - **`@object-ui/plugin-grid`:** a column listed in a view's `columns` now reads its field's `sortable: false`, as columns derived from the object's fields already did. Where the grid sorts the rows it holds (inline rows), such a column no longer offers a sort click. A `formula` column keeps its sort click there, since the browser orders by the value the cell shows; where the sort is sent to the server, the platform's projection still decides, as before. +- 5f80129: fix(plugin-detail): a record page's related table offers Export only to a reader the list page offers it to, and an export refusal reads in the Console's language + + Clause-②: no + Metadata-protocol: none + + - **`@object-ui/plugin-detail`: the related table asks the list page's question.** The enlarged related list (the record page's active tab) drew Export whenever its data source could export. It now also reads the child object's effective API operation set (`/me/permissions` `apiOperations`) through `usePermissions()`, the source the list page's `exportPermitted` reads, and draws no Export when that set leaves `export` out. A missing set keeps the button, as on the list page. A reader who may export still exports this relation's rows alone. + - **Export refusals read in the Console's language.** When the export route still refuses (a 403, such as `EXPORT_NOT_PERMITTED`), the related table, the list page and the grid show the new `view.exportNotPermitted` sentence in the reader's language instead of the route's English text. Other export failures keep the server's sentence. + - **`@object-ui/i18n`: `view.exportNotPermitted`** in all ten locale packs. +- eb6199b: feat(app-shell,plugin-list): a personal view's own conditions are Filter panel rows, and 保存 writes the panel back into that view + + Clause-②: no + Metadata-protocol: none + + - **Opening one of the reader's own views (My views) shows its conditions as ordinary rows in the Filter panel**, with the active count on the Filter control. They are the list's filter while they are there: not applied under the list a second time, and not a personalization. A stored filter the panel cannot hold (an any-of group beside other conditions), or one naming a field the object no longer declares or the reader cannot read, stays applied under the list, as for any other view. + - **Editing those rows marks the list 已个性化, and the marker offers 保存 to that view** (for a reader allowed to update their views). 保存 replaces the row's `config` through the data API with the body Save as my view builds, so the stored `ListView` shape is unchanged and no key is added; the reader's temporary list state for the view is then cleared and the list opens on the saved view. Edits not saved stay the reader's temporary list state and are never written to the view. A panel the fold cannot write losslessly keeps the refusal Save as my view gives, and nothing is written. + - **Save as my view now clears the view it came from**: that view's stored list state (sort, filters, column layout, density, hidden fields) and cached search term go, and the address being left loses its `uf_*` params, so the source no longer reads filtered or 已个性化 after the save. + - Both saves read the Filter panel as the list reports it, not the address's copy, so an emptied panel saves as no filter. + - `@object-ui/plugin-list`: `ListView`'s host prop `viewDefaults` accepts `filters`, the view's own conditions for a host that shows them as panel rows; the `filters` personalization part is then judged against them. +- dfafae8: feat(plugin-list): a reader can add any field they may read to their own list from the Hide Fields popover (#30077) + + Clause-②: no + Metadata-protocol: none + + The Hide Fields popover on a Console object list (the toolbar's, and the Hide Fields section of the Display panel) listed only the columns the view already has, so a reader who wanted another field on their list had to ask an administrator to change the view for everyone. After the view's columns it now lists every other field of the object the reader may read, unticked: business fields first, then system fields such as Owner. A field the reader may not read is not offered, and neither is a field the object definition marks `hidden`. Ticking one adds it as a column of the reader's own list; unticking it takes it off again. + + The addition is the reader's personalization, stored beside their hidden fields in their `ui.list.` preference row (`perView..addedFields`), never written into the view: another user sees the view's columns, and "Reset to view" drops it with the rest of the reader's list. While it is stored the list's Personalized marker names "Column layout". An added column is fetched and drawn like a declared one, and an added lookup field is expanded in the list read as a declared lookup column is. + + A column the view declares with no label of its own (a bare `{ field }` or field name) is now listed in the Hide Fields popover (and in the Group and Row color pickers, which share its list) under the object definition's field label, the label the grid's header shows, instead of under its machine name (`expected_revenue` now reads Expected Revenue); a column's own label and a translation still come first. + + It is offered wherever a Hide Fields choice is stored: a view that declares `columns`, and a saved view. On a system view or fallback tab that declares no columns the popover lists the view's columns alone, as before. + + For a `ListView` host: two new host props, `addedFields` (the names among `schema.columns` the reader added) and `onAddedFieldsChange` (the door that stores them). Without `onAddedFieldsChange` the popover is exactly what it was. No metadata key and no spec change. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-list/package.json b/ui/packages/plugin-list/package.json index 10ef8524209..163c310d39a 100644 --- a/ui/packages/plugin-list/package.json +++ b/ui/packages/plugin-list/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-list", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "ListView plugin for ObjectUI - unified view component with view type switching", diff --git a/ui/packages/plugin-map/CHANGELOG.md b/ui/packages/plugin-map/CHANGELOG.md index 06d0a7be4be..7728a79803e 100644 --- a/ui/packages/plugin-map/CHANGELOG.md +++ b/ui/packages/plugin-map/CHANGELOG.md @@ -1,5 +1,29 @@ # @object-ui/plugin-map +## 18.4.0 + +### Patch Changes + +- Updated dependencies [c2be734] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [f68b47c] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-map/package.json b/ui/packages/plugin-map/package.json index 386e9798f8d..2f9137efaf6 100644 --- a/ui/packages/plugin-map/package.json +++ b/ui/packages/plugin-map/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-map", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Map visualization plugin for ObjectUI", diff --git a/ui/packages/plugin-markdown/CHANGELOG.md b/ui/packages/plugin-markdown/CHANGELOG.md index c0a167c7e69..1bef6195dd4 100644 --- a/ui/packages/plugin-markdown/CHANGELOG.md +++ b/ui/packages/plugin-markdown/CHANGELOG.md @@ -1,5 +1,21 @@ # @object-ui/plugin-markdown +## 18.4.0 + +### Patch Changes + +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-markdown/package.json b/ui/packages/plugin-markdown/package.json index 2ce1104486c..d49e0a4157e 100644 --- a/ui/packages/plugin-markdown/package.json +++ b/ui/packages/plugin-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-markdown", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Markdown rendering plugin for ObjectUI, powered by react-markdown", diff --git a/ui/packages/plugin-report/CHANGELOG.md b/ui/packages/plugin-report/CHANGELOG.md index 33151467155..02eb5e74866 100644 --- a/ui/packages/plugin-report/CHANGELOG.md +++ b/ui/packages/plugin-report/CHANGELOG.md @@ -1,5 +1,35 @@ # @object-ui/plugin-report +## 18.4.0 + +### Patch Changes + +- Updated dependencies [b379262] +- Updated dependencies [c2be734] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [ff12aed] +- Updated dependencies [bff5ba2] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/plugin-report/package.json b/ui/packages/plugin-report/package.json index 540a14f1eb6..455393501fb 100644 --- a/ui/packages/plugin-report/package.json +++ b/ui/packages/plugin-report/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-report", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "main": "dist/index.umd.cjs", "module": "dist/index.js", diff --git a/ui/packages/plugin-timeline/CHANGELOG.md b/ui/packages/plugin-timeline/CHANGELOG.md index 31bf822037f..d66e3a5a51a 100644 --- a/ui/packages/plugin-timeline/CHANGELOG.md +++ b/ui/packages/plugin-timeline/CHANGELOG.md @@ -1,5 +1,63 @@ # @object-ui/plugin-timeline +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/mobile@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-timeline/package.json b/ui/packages/plugin-timeline/package.json index 0122a2aa2bd..a501238be19 100644 --- a/ui/packages/plugin-timeline/package.json +++ b/ui/packages/plugin-timeline/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-timeline", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Timeline component plugin for ObjectUI", diff --git a/ui/packages/plugin-tree/CHANGELOG.md b/ui/packages/plugin-tree/CHANGELOG.md index 3833621e780..3b7cdc173b7 100644 --- a/ui/packages/plugin-tree/CHANGELOG.md +++ b/ui/packages/plugin-tree/CHANGELOG.md @@ -1,5 +1,63 @@ # @object-ui/plugin-tree +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [6e68d8f] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [f68b47c] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [c1240a3] +- Updated dependencies [5fa9375] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/plugin-detail@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/fields@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-tree/package.json b/ui/packages/plugin-tree/package.json index f37cfa20f80..58cbb0dd685 100644 --- a/ui/packages/plugin-tree/package.json +++ b/ui/packages/plugin-tree/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-tree", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Tree / tree-grid visualization plugin for ObjectUI", diff --git a/ui/packages/plugin-view/CHANGELOG.md b/ui/packages/plugin-view/CHANGELOG.md index ae8b254f55c..951bbb32335 100644 --- a/ui/packages/plugin-view/CHANGELOG.md +++ b/ui/packages/plugin-view/CHANGELOG.md @@ -1,5 +1,60 @@ # @object-ui/plugin-view +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [6db9198] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [8c1bf5b] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/components@18.4.0 + - @object-ui/types@18.4.0 + - @object-ui/plugin-grid@18.4.0 + - @object-ui/plugin-form@18.4.0 + - @object-ui/react@18.4.0 + - @object-ui/permissions@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/plugin-view/package.json b/ui/packages/plugin-view/package.json index ccbac4797c8..92406b62977 100644 --- a/ui/packages/plugin-view/package.json +++ b/ui/packages/plugin-view/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/plugin-view", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Object View plugin for ObjectUI", diff --git a/ui/packages/providers/CHANGELOG.md b/ui/packages/providers/CHANGELOG.md index 584241a06c1..dc7abada962 100644 --- a/ui/packages/providers/CHANGELOG.md +++ b/ui/packages/providers/CHANGELOG.md @@ -1,5 +1,12 @@ # @object-ui/providers — Changelog +## 18.4.0 + +### Patch Changes + +- Updated dependencies [a52761f] + - @object-ui/types@18.4.0 + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/providers/package.json b/ui/packages/providers/package.json index 88b3a1cb7bc..35f35874979 100644 --- a/ui/packages/providers/package.json +++ b/ui/packages/providers/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/providers", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "Reusable context providers for ObjectUI applications", diff --git a/ui/packages/react-runtime/CHANGELOG.md b/ui/packages/react-runtime/CHANGELOG.md index c7d5fc9280e..928ad31f09c 100644 --- a/ui/packages/react-runtime/CHANGELOG.md +++ b/ui/packages/react-runtime/CHANGELOG.md @@ -1,5 +1,9 @@ # @object-ui/react-runtime +## 18.4.0 + +No changes in this release. + ## 18.3.0 ### Patch Changes diff --git a/ui/packages/react-runtime/package.json b/ui/packages/react-runtime/package.json index bdc4428e119..06e3988cc88 100644 --- a/ui/packages/react-runtime/package.json +++ b/ui/packages/react-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react-runtime", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "sideEffects": false, "license": "MIT", diff --git a/ui/packages/react/CHANGELOG.md b/ui/packages/react/CHANGELOG.md index 94e0a67b812..9be48c5323f 100644 --- a/ui/packages/react/CHANGELOG.md +++ b/ui/packages/react/CHANGELOG.md @@ -1,5 +1,53 @@ # @object-ui/react +## 18.4.0 + +### Patch Changes + +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [b379262] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [c2be734] +- Updated dependencies [e3b56b2] +- Updated dependencies [9049be2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [03be565] +- Updated dependencies [7e9fa4b] +- Updated dependencies [42afac7] +- Updated dependencies [a9fb14b] +- Updated dependencies [6f095b9] +- Updated dependencies [5f80129] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [ff12aed] +- Updated dependencies [4be9734] +- Updated dependencies [bff5ba2] +- Updated dependencies [bb86ff9] +- Updated dependencies [31260ca] +- Updated dependencies [ef2ad31] +- Updated dependencies [eca7cd2] +- Updated dependencies [eca7cd2] +- Updated dependencies [614e831] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + - @object-ui/i18n@18.4.0 + - @object-ui/core@18.4.0 + - @object-ui/types@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/react/package.json b/ui/packages/react/package.json index 89e90a6ead9..49805483fdc 100644 --- a/ui/packages/react/package.json +++ b/ui/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/react", - "version": "18.3.0", + "version": "18.4.0", "type": "module", "license": "MIT", "description": "React bindings and SchemaRenderer component for ObjectUI", diff --git a/ui/packages/types/CHANGELOG.md b/ui/packages/types/CHANGELOG.md index 7c28540feb5..3f0d45ea8cd 100644 --- a/ui/packages/types/CHANGELOG.md +++ b/ui/packages/types/CHANGELOG.md @@ -1,5 +1,44 @@ # @object-ui/types +## 18.4.0 + +### Patch Changes + +- a52761f: fix(console): an action another package places on `sys_user` at `record_header` / `record_more` draws again on the user's record page, in the drawer and on the full page (#29951) + + Clause-②: no + Metadata-protocol: none + + - **The header reads `placedActions: true`.** `page:header`'s `placedActions` (#30018) is now drawn by the console's header renderer (`PageHeaderActions` in `@object-ui/components`): `true` draws every action of the page's object whose `locations` include `record_header` or `record_more`, in declaration order, read from the object's metadata at render time, so an action an installed package adds to the object is drawn without editing the page. Omitted or `false`, an authored `actions` list is still the whole set (#11592). A `true` beside an authored list, which the spec refuses, draws none of the object's actions and is reported once in the console. + - **One placed-header reader.** `placedHeaderActions` (`@object-ui/types`) is the rule both headers read: the synthesized default record page's header in `RecordDetailView` and a `placedActions: true` header. The `page:header` registration publishes the `placedActions` input. + - **The platform record pages opt in.** `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`) and `sys_approval_request_detail` (`@objectstack/plugin-approvals`) set `placedActions: true` in place of the id lists #27219 froze, which dropped every header action another package places on those objects. The tab strips are unchanged, and every action the lists named is placed at `record_header` / `record_more`, so each still draws. + - `@objectstack/spec`: the `placedActions` describe no longer opens with `[PLANNED — not yet rendered]`, and the `page` liveness record names the reader. +- Updated dependencies [8aa2e91] +- Updated dependencies [9ebca94] +- Updated dependencies [140d8ce] +- Updated dependencies [699ebd8] +- Updated dependencies [4c2cd29] +- Updated dependencies [4be8a2e] +- Updated dependencies [471add3] +- Updated dependencies [9037b13] +- Updated dependencies [e3b56b2] +- Updated dependencies [a52761f] +- Updated dependencies [e8afd1e] +- Updated dependencies [217ba78] +- Updated dependencies [cde492d] +- Updated dependencies [eee4149] +- Updated dependencies [d59a1eb] +- Updated dependencies [3a27208] +- Updated dependencies [aa57c76] +- Updated dependencies [fc869c2] +- Updated dependencies [4be9734] +- Updated dependencies [bb86ff9] +- Updated dependencies [eca7cd2] +- Updated dependencies [21510bb] +- Updated dependencies [0fd363d] +- Updated dependencies [63a470a] + - @objectstack/spec@18.4.0 + ## 18.3.0 ### Minor Changes diff --git a/ui/packages/types/package.json b/ui/packages/types/package.json index c6e9ef1abec..367107e95aa 100644 --- a/ui/packages/types/package.json +++ b/ui/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@object-ui/types", - "version": "18.3.0", + "version": "18.4.0", "description": "Pure TypeScript type definitions for ObjectUI - The Protocol Layer", "type": "module", "sideEffects": false,