diff --git a/.changeset/17306-flow-screen-field-help-text-translation.md b/.changeset/17306-flow-screen-field-help-text-translation.md deleted file mode 100644 index 74339a9f4b8..00000000000 --- a/.changeset/17306-flow-screen-field-help-text-translation.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A flow screen field's help text is translatable: the `flows` translation face carries `inlineHelpText` beside `label` and `placeholder` (#17306). - -Clause-②: yes (widening) - -- **`TranslationDataSchema`.** `flows..screens..fields.` accepts `inlineHelpText`, the key the screen field itself declares (`ScreenFieldConfig.inlineHelpText`, the object field's spelling). The console's screen dialog draws that text under the control, so a translated help line now renders in the active locale. -- **`FLOW_SCREEN_FIELD_COPY_KEYS`** (`@objectstack/spec/system`) is `['label', 'placeholder', 'inlineHelpText']`. Its readers follow it without an edit: `translateFlow` overlays the key, `os i18n extract` scaffolds it, and objectui's `FlowRunner` overlays it on the field it draws. `FlowScreenFieldLike` gains the optional `inlineHelpText` member. -- **Refusals.** `help`, `helpText`, `hint`, `tooltip` and `description` on a screen field translation are still refused, and the message now names the rename to `inlineHelpText`. They used to be told that the face had no help key. `options` is still refused with its guidance. - -Nothing that parsed before is refused now. A bundle that never wrote a help line is unchanged. diff --git a/.changeset/20274-agent-guardrails-live.md b/.changeset/20274-agent-guardrails-live.md deleted file mode 100644 index e99aa4542ed..00000000000 --- a/.changeset/20274-agent-guardrails-live.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Liveness ledger: `agent.guardrails` (`maxTokensPerInvocation`, `maxExecutionTimeSec`, `blockedTopics`) is now `live`, not `experimental`. The cloud AI runtime enforces it on every user turn. The token and time limits are checked before each model round, with each limit refusal audited, and a blocked tool name or category is removed from the offer and refused at call time. - -Clause-②: no - -- The `guardrails` describe drops its `[EXPERIMENTAL — not enforced]` marker. It now says the cloud AI runtime enforces the block and the open framework edition does not run agents. The generated agent reference page follows. -- Author-facing effect: `os lint` / `os validate` no longer warn `liveness-experimental-property` on an agent that sets `guardrails`. A warning is not a refusal, so the accept set is unchanged. -- The ledger row cites the cloud readers and producer, dated to the reading they come from. -- The liveness README no longer says its gate refuses `live` on evidence attributed only to the closed cloud runtime. The gate never did. -- `tool.outputSchema` stays `experimental`, because nothing reads it on a tool record. Its describe and the tools guide now say where output validation actually lives: `ai.outputSchema` on the action, against which the cloud AI runtime checks the action's result. The old claim that the keys are folded into the tool description is gone. -- ⛔ No schema, parse, export or accept-set change. `agent.memory`, `agent.structuredOutput` and `agent.lifecycle` stay `experimental`. diff --git a/.changeset/20274-agent-memory-contract.md b/.changeset/20274-agent-memory-contract.md deleted file mode 100644 index 0806f914138..00000000000 --- a/.changeset/20274-agent-memory-contract.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/platform-objects': patch ---- - -feat(spec)!: an agent's `memory` contract states exactly what the runtime honours — `maxEntries` and `reflectionInterval` are required once long-term memory is enabled, `longTerm.store` is retired, and the block is `live`, enforced by the cloud AI runtime (#20274) - -**BREAKING** — `agent.memory` narrows to what the cloud AI runtime, the one runtime -that executes agents, actually does with it. That runtime recalls the newest -`maxEntries` distilled notes for the user before the first round, writes one note -every `reflectionInterval` delivered interactions, evicts notes beyond `maxEntries`, -and keeps them in its own database store. Before an agent's first turn it refused -exactly the declarations this spec still accepted, so authoring now refuses them, -by name, with a prescription (ADR-0049 enforce-or-remove): - -- **`longTerm.maxEntries` and `reflectionInterval` are required when - `longTerm.enabled` is true.** No default is declared for either: none has a - measured basis, and the runtime adds none. -- **`reflectionInterval` is refused without an enabled `longTerm`** — a reflection - writes a long-term note, so with none enabled it would do nothing. -- **`longTerm.store` is retired as a whole key.** The memory store is platform - infrastructure, not agent metadata: the runtime keeps the notes in its own - database store, and refused `vector` (the key's default, so what an omitted - `store` parsed to) and `redis`. Its old spellings `backend`, `storage` and - `provider` under `longTerm` are answered with the same prescription instead of - being steered onto `store`. - -`longTerm.enabled` is unchanged. - -### FROM → TO - -| before | what to write instead | -| --- | --- | -| `memory.longTerm.store` — any value, `database` included | delete the key; where the notes are kept is the platform's choice. | -| `longTerm: { enabled: true, … }` without `maxEntries` | add `maxEntries`: how many distilled notes are kept for each user (an integer of at least 1). | -| `longTerm: { enabled: true, … }` without `memory.reflectionInterval` | add `reflectionInterval`: how many delivered interactions pass between the reflections that write a note (an integer of at least 1). | -| `memory.reflectionInterval` without `longTerm.enabled: true` | enable long-term memory with both numbers, or delete `reflectionInterval`. | - -**The one-line fix: declare `maxEntries` and `reflectionInterval` when `longTerm.enabled`; delete `store`.** -`os migrate meta --from 17` lists the mechanical edits for existing sources (the -`store` deletion); the two numbers are the author's to choose. - -Each refusal is a parse error at the key's own path, naming the key and the fix, and -`store` also fails `tsc` (its input type is `never`). - -### The retirement kit - -- **Tombstone.** `longTerm.store` is a `retiredKey()` carrying the prescription; the - three old alias spellings moved from `aliases` to `guidance`, because an alias may - not steer an author onto a tombstone. -- **The contract check** is a refinement on `memory` (`reflectionInterval` is - `longTerm`'s sibling), one `custom` issue per missing or misplaced key. A JSON - Schema cannot state a value-conditioned requirement in the closed projection list, - so the published `ai/Agent` schema (and the four installed-package schemas that - embed agents) names the site in `x-dropped-refinements`, recorded in - `dropped-refinements.baseline.json`. -- **D2 conversion `agent-memory-long-term-store-removed`** (step 18, retired from the - load path): it deletes `store` from `memory.longTerm`, whatever it holds — the - delete is lossless, because no value of it ever chose a backend. Stored - `sys_metadata` agent rows and built artifacts replay it; one notice per agent. It - supplies neither number. -- **D3 entry `agent-memory-store-retired-and-limits-required`** carries the judgement - the conversion cannot make: the two numbers an enabled `longTerm` now requires. -- **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:memory.longTerm.store`. -- **No deprecation window**, per the project's startup-stage posture. - -### Describes and the liveness ledger - -- `agent.memory` drops `[EXPERIMENTAL — not enforced]`: it states that the cloud AI - runtime enforces it and that the open framework edition does not run agents. - `longTerm`, `enabled`, `maxEntries` and `reflectionInterval` each state what the - runtime does with them. -- The ledger row moves `experimental` → `live`, citing the cloud reader - `agent-runtime.ts#compileAgentMemory` (via `AgentRuntime.resolveTurnGuardrails`), - the enforcement in `ai-service.ts` and the store `agent-memory.ts#AgentMemoryStore`, - as attested by the cloud seat's reading at cloud `ef5a4344`, `verifiedAt` - 2026-10-02. `os lint` / `os validate` no longer warn - `liveness-experimental-property` on an agent that sets `memory`. -- ⚠️ **The window, stated.** At `ef5a4344` the cloud reader still reads `store`: it - honours `database` only and refuses `vector` and `redis`. Cloud drops `store` in - that one reader once this release reaches its pin, and no earlier. - -### The agent form's help texts - -- The `memory` row's help text on the agent metadata form named short-term memory, - a key the schema refuses. It now states what memory does and that `maxEntries` - and `reflectionInterval` are required once long-term memory is enabled. -- The neighbouring `planning` row named a strategy and a replan switch the schema - does not declare; it now states the one key it has, the iteration cap. -- The `platform-objects` metadata-form catalogs follow: the English leaves are - regenerated, and the `zh-CN`, `ja-JP` and `es-ES` leaves are authored, not copied. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is -published, and tenant-authored agents were not measured. This repo authors no -`longTerm` outside `packages/spec`, and no cloud built-in agent declares one. - -Clause-②: yes (narrowing) - - diff --git a/.changeset/20281-job-pull-organization.md b/.changeset/20281-job-pull-organization.md deleted file mode 100644 index c1d2969a83e..00000000000 --- a/.changeset/20281-job-pull-organization.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor -'@objectstack/service-automation': minor ---- - -A job pulls a mapping's connector source by declaration — `pull: { mapping }` — and every job runs as the `organization` it declares (#20281). - -Clause-②: yes (widening) - -- **`JobSchema.pull`** (`@objectstack/spec/system`). A third run form beside `body` and `handler`: `{ mapping: '' }`. On each run the platform pulls that mapping's `connectorSource` and writes the rows through the import runner. It carries no code. The key is refused beside `body` or `handler`, because one of the two run forms would never run. `body` with `handler` stays legal, and the body still wins. A job must now declare one of `body`, `handler` or `pull`. `pull` is closed: an unknown key inside it is refused. -- **`JobSchema.organization`**. The organization a job runs as. It applies to the body's `ctx.api`, to the handler's new `executionContext`, and to the pull's reads and writes. The value shape is the scheduled flow's: a non-empty `sys_organization.id`. A near-miss spelling (`organizationId`, `orgId`, `tenantId`, …) is refused at parse and pointed at the key. -- **`defineStack`, and so `os validate`**, refuses a job whose `pull` names a mapping the stack does not declare, or a mapping with no `connectorSource`. The refusal is the existing `STACK_CROSS_REFERENCE_INVALID` envelope. -- **`IAutomationService.pullConnectorSource`** (`@objectstack/spec/contracts`, with `ConnectorSourcePullRequest`, `ConnectorSourcePullResult` and `ConnectorSourcePullSummary`). The connector sync executor is now on the `automation` service. `@objectstack/service-automation`'s engine serves it from the executor `AutomationServicePlugin` attaches at init (`AutomationEngine.setConnectorPullSource`). A bare engine refuses with `SERVICE_UNAVAILABLE` (503). -- **The job binder** (`@objectstack/runtime`, `scheduleAppArtifactJobs`) schedules a `pull` job on every door: the boot, and `os package install` on install and rehydrate. Each run calls `pullConnectorSource` through the service registry. A refused pull fails the run, and `retryPolicy` applies. A pull whose rows the import runner refused records the run `degraded`, with the counts. A pull naming a mapping the artifact does not carry is not scheduled, and neither is one whose mapping has no `connectorSource`, nor one on a kernel whose `automation` service cannot pull. Each case is logged at `warn` with the reason. `collectJobsWithoutBody` does not name a `pull` job that binds, so `os package install` installs one. The result gains `pulls` and `missingOrganization`. -- **The organization, judged at bind** by the posture rule scheduled flows use (`resolveScheduledWorkPolicy`). Every run carries `{ isSystem: true, tenantId: }`, or `{ isSystem: true }` for a job that declares none. Under `single` the key is not required. Under `group` it is optional; an undeclared job is scheduled and named once at `warn`, because a tenant-scoped row it writes is refused. Under `isolated`, with package-authored scheduled work switched on, it is **required**. **Action on such a deployment:** declare `organization` on each packaged job, or the job is not scheduled; the error log names the job. Until now such a job was scheduled, and every tenant-scoped write it made was refused at the write. An unrecognized `OS_TENANCY_POSTURE` withholds every job (`scheduled-work-policy-unreadable`) instead of guessing whether a declaration is required. -- **Texts this makes true.** The `mapping.connectorSource` description, the `connector.syncConfig` tombstone prescription and the `connector-sync-keys-retired` upgrade entry said "nothing schedules a pull yet". They now name the `job` `pull` that drives it. - -Nothing that parsed before is refused now. Every new refusal falls on a key that did not exist before this change. diff --git a/.changeset/20299-rls-policy-rows-live.md b/.changeset/20299-rls-policy-rows-live.md deleted file mode 100644 index 5ee928b9a45..00000000000 --- a/.changeset/20299-rls-policy-rows-live.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Liveness ledger: a permission set's row-level security policy `label` and `description` (`rowLevelSecurity[].label` / `.description`) are now `live`, not `dead`. Studio's permission editor shows both on every policy. Ledger data and its generated count shard only. - -Clause-②: no - -- **What shows them.** These are display keys, so under the ledger's "Designer previews count as consumers" ruling, being shown to a human is the whole of their claimed effect. The Row-Level Security section of the permission editor (`PermissionAdvancedFacets` in objectui) now heads each policy card with the policy's `label` and, beneath it, its `description`, exactly as written. Both rows cite that reader at the `.objectui-sha` pin `89cad75d557`. The registered permission preview also draws both, but no route mounts it for `permission`, so it is not cited. -- **Where the values come from.** Each row names its producer: the `permission` edit page registration and the Studio edit route that mounts it, the editor's `GET /api/v1/meta/permission/:name/layers` read, and this repo's shared layered answer (`createMetaLayeredAnswer`), which serves a permission set whole. The showcase's contributor permission set authors both keys on all three of its policies. -- **Author-facing effect.** `os lint` / `os validate` no longer warn `liveness-dead-property` on a policy that sets `label` or `description`. A warning is not a refusal, so the accept set is unchanged. -- **Still kept.** The re-grade reverses no ADR-0033 decision. Both rows stay docs-shaped annotation, deliberately kept and not `authorWarn`'d. -- The regenerated liveness count is the `liveness/state-counts/permission.md` shard: `permission` has 38 live and 4 dead (was 36 and 6). The `view` container's own `label` stays `dead`. -- ⛔ No schema, parse, `.describe()`, export or accept-set change. diff --git a/.changeset/20301-lint-list-view-tabs-walk-deleted.md b/.changeset/20301-lint-list-view-tabs-walk-deleted.md deleted file mode 100644 index bff9e102ee3..00000000000 --- a/.changeset/20301-lint-list-view-tabs-walk-deleted.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/lint": patch ---- - -The list-view field-reference rule no longer walks a list view's own `tabs[].filter` - -Clause-②: no - -The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape, and this rule judges the parsed stack, so the key could never reach the walk: the parse refuses it first, with its prescription. The dead branch is deleted. The rule still judges `filter` and `userFilters.tabs[].filter` exactly as before. - -No finding changes for any stack that `os validate`, `os lint` or `os build` accepts. diff --git a/.changeset/20301-metadata-protocol-list-view-tabs-walk-deleted.md b/.changeset/20301-metadata-protocol-list-view-tabs-walk-deleted.md deleted file mode 100644 index 925a0d39212..00000000000 --- a/.changeset/20301-metadata-protocol-list-view-tabs-walk-deleted.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -`computeViewReferenceDiagnostics` no longer walks a list view's own `tabs[].filter` - -Clause-②: no - -The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape. The write door refuses it, and a stored or artifact-shipped body has it stripped by the conversion replay before it is served, so the read could never see it. A served body that still carries it is already badged by the spec diagnostics (`computeMetadataDiagnostics`), with the tombstone's prescription. The `userFilters.tabs[].filter`, `filterableFields` and `kanban` checks are unchanged. diff --git a/.changeset/20301-spec-view-container-name-ledger-note.md b/.changeset/20301-spec-view-container-name-ledger-note.md deleted file mode 100644 index 4a26724d0f0..00000000000 --- a/.changeset/20301-spec-view-container-name-ledger-note.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Liveness ledger: the view container's body `name` row stays `dead`, and its note now states what the platform actually does with the key - -Clause-②: no - -- The old note said the body copy was "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body that has none, containers included (`normalizeViewMetadata` in `@objectstack/metadata-protocol`). Its overlay paths key on that stamped copy: `hydrateOverlayIntoRegistry` registers no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by it. -- The verdict is unchanged, because the ledger's `live` means that authoring the key changes runtime behaviour. An authored container `name` only restates the key the container already registers under, or contradicts it. `os validate` and `os lint` keep warning `liveness-dead-property` ("drop it"). -- The note records why the key is kept rather than tombstoned: the door's own saves stamp it, so a tombstone would refuse the platform's own writes. A maintainer ruling also refused a spec-level forbid of a container's `name`. -- It corrects the old attribution too. Artifact-shipped containers and the metadata-validation sweep author no `name`; what was read as theirs is the door's stamp. -- The ledger README's `view` cell says the same. The `view.list.tabs` row's note now records that the two author-time walks that still read a list view's own `tabs` are deleted. -- A comment in `system/i18n-resolver.ts` that still called the list view's own `tabs` a live carrier now says the key is a tombstone and `UserFiltersSchema.tabs` is the one carrier. -- ⛔ No schema, parse, export, status or accept-set change. diff --git a/.changeset/20318-cli-flow-label-demand.md b/.changeset/20318-cli-flow-label-demand.md deleted file mode 100644 index c6eaa5a3845..00000000000 --- a/.changeset/20318-cli-flow-label-demand.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os lint` and `os i18n extract` ask for a flow's `flows..label` translation only when the flow has a screen node at any depth, the only kind of flow the console's screen-flow runner opens and names, so a scheduled, record-triggered or API flow with no screen no longer draws an `i18n/missing-flow` demand for a label no surface shows. - -Clause-②: no diff --git a/.changeset/20318-flows-translation-live.md b/.changeset/20318-flows-translation-live.md deleted file mode 100644 index 8a46684d425..00000000000 --- a/.changeset/20318-flows-translation-live.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Liveness ledger: the `flows` translation group is `live`, and so are both its children. The console's screen-flow runner now names the flow by `flows..label` in the active language, in the runner's header and in its completion toast. A locale the bundle does not cover shows the label authored on the flow, and then the flow's API name. `screens` was already `live`. - -Clause-②: no - -- The `flows` row drops `authorWarn` and its `authorHint`. `os lint` and `os validate` no longer warn `liveness-planned-property` on a bundle that authors `flows`. A warning is not a refusal, so the accept set is unchanged. -- Dropping that bit switches on the CLI's i18n coverage demand for `flows.*`. `os lint` now reports a `flows..*` key that a supported locale is missing as `i18n/missing-flow`, and `os i18n extract` scaffolds the group into the bundle. The flow's own `label` is demanded only for a flow with a screen node (see the `@objectstack/cli` entry). Under `--i18n-strict` a missing key is an error: translate it, or run `os i18n extract` to scaffold it. -- The `flows` TSDoc in `translation.zod.ts` and the translations guide's boundary note now say that both halves are applied. -- ⛔ No schema, parse, export or accept-set change. diff --git a/.changeset/20595-core-provenance-anchors.md b/.changeset/20595-core-provenance-anchors.md deleted file mode 100644 index 47fa503319e..00000000000 --- a/.changeset/20595-core-provenance-anchors.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/core': patch ---- - -Provenance comments in `@objectstack/core` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each now cites the commit in this repository's history that made the decision it describes, except -three source comments: one in `resolve-authz-context.ts` that quotes a maintainer ruling now cites -ADR-0131's 2026-09-17 amendment, which records that ruling verbatim, and two on the unpack-time -integrity re-verification leg, which pointed at a tracker for work that was never built, now say in -words that the leg is unbuilt. One test comment named a maintainer-ruling comment that also answers -404; it now cites ADR-0025 §3.7, which records that ruling's effect. Some of these docblocks sit on -exported members, so the reworded text appears in the published declaration files (`index.d.ts` / -`index.d.cts`), and the comments esbuild keeps appear in the JavaScript output (`index.js` / -`index.cjs`). - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-driver-memory-provenance-anchors.md b/.changeset/20595-driver-memory-provenance-anchors.md deleted file mode 100644 index 5ad48659bba..00000000000 --- a/.changeset/20595-driver-memory-provenance-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/driver-memory': patch ---- - -Provenance comments in `@objectstack/driver-memory` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes. Some of -these docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` / -`index.d.mts`, and comments that esbuild keeps appear in the JavaScript output. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-driver-mongodb-provenance-anchors.md b/.changeset/20595-driver-mongodb-provenance-anchors.md deleted file mode 100644 index 0612e81a5cd..00000000000 --- a/.changeset/20595-driver-mongodb-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-mongodb': patch ---- - -Provenance comments in `@objectstack/driver-mongodb` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes. One of -these docblocks sits on an exported member (`MongoDBDriver.update()`), so the reworded text appears in -the published `index.d.ts` / `index.d.mts`; that docblock and one more comment esbuild keeps appear in -the JavaScript output (`index.js` / `index.mjs`); the sourcemaps do not change. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-driver-sql-provenance-anchors.md b/.changeset/20595-driver-sql-provenance-anchors.md deleted file mode 100644 index 4dc2efb4013..00000000000 --- a/.changeset/20595-driver-sql-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -Provenance comments in `@objectstack/driver-sql` cite the commits and ADR that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes, or the -ADR that records it (ADR-0104's 2026-09-05 addendum). Some of these docblocks sit on exported members, -so the reworded text appears in the published `index.d.ts` / `index.d.mts`, and comments that esbuild -keeps appear in the JavaScript output. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-driver-turso-provenance-anchors.md b/.changeset/20595-driver-turso-provenance-anchors.md deleted file mode 100644 index 881358f3d0c..00000000000 --- a/.changeset/20595-driver-turso-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-turso': patch ---- - -Provenance comments in `@objectstack/driver-turso` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes. Some -of these docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` -/ `index.d.mts`, and the comments esbuild keeps appear in the JavaScript output (`index.js` / -`index.mjs`); the sourcemaps do not change. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-formula-provenance-anchors.md b/.changeset/20595-formula-provenance-anchors.md deleted file mode 100644 index 9cb49b8b5b0..00000000000 --- a/.changeset/20595-formula-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/formula': patch ---- - -Provenance comments in `@objectstack/formula` cite the commit that decided them, not a tracker number that no longer resolves - -Clause-②: no - -Comments and docblocks in the package cited an issue-tracker number that now answers 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes. One of -these docblocks sits on an exported member (`SCOPE_ROOTS`), so the reworded text appears in the -published `index.d.ts` / `index.d.mts`; one comment esbuild keeps inside that list appears in the -JavaScript output (`index.js` / `index.mjs`); the sourcemaps do not change. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-metadata-core-provenance-anchors.md b/.changeset/20595-metadata-core-provenance-anchors.md deleted file mode 100644 index f26bf487af1..00000000000 --- a/.changeset/20595-metadata-core-provenance-anchors.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/metadata-core': patch ---- - -Provenance comments in `@objectstack/metadata-core` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each now cites the commit in this repository's history that made the decision it describes, with two -exceptions: two comments on `retiredFromLoadPath`'s jurisdiction (in `artifact-forward-conversion.ts` -and its test) cite ADR-0087, which records that determination, and five comments that meant an -objectui issue now spell it `objectui#6111`, as they already spelled `objectui#6110` beside it. One -commit citation sits inside a maintainer ruling quoted in `record-organization.ts`: the number there -became the bracketed editorial substitution `[commit 7901b2dd2]`, the commit that landed the ruling it -names, and the rest of the quotation is unchanged. Some of these docblocks sit on exported members, so -the reworded text appears in the published declaration files (`index.d.ts` / `index.d.cts`, -`testing.d.ts` and a shared declaration chunk); the JavaScript output and its sourcemaps do not change. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-metadata-fs-provenance-anchors.md b/.changeset/20595-metadata-fs-provenance-anchors.md deleted file mode 100644 index 780e3a035a5..00000000000 --- a/.changeset/20595-metadata-fs-provenance-anchors.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/metadata-fs': patch ---- - -Provenance comments in `@objectstack/metadata-fs` cite the commit that decided them, not a tracker number that no longer resolves - -Clause-②: no - -Comments and docblocks in the package cited an issue-tracker number that now answers 404 on GitHub. -Each one now cites the commit in this repository's history that made the decision it describes. One of -these docblocks sits on a public method (`FileSystemRepository.close()`), so the reworded text appears in -the published `index.d.ts` / `index.d.cts` and, because esbuild keeps that docblock, in the JavaScript -output (`index.js` / `index.cjs`); the sourcemaps do not change. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-metadata-provenance-anchors.md b/.changeset/20595-metadata-provenance-anchors.md deleted file mode 100644 index 8e628498753..00000000000 --- a/.changeset/20595-metadata-provenance-anchors.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/metadata': patch ---- - -Provenance comments in `@objectstack/metadata` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each now cites the commit in this repository's history that made the decision it describes, except two -that meant an objectui issue and now spell `objectui#6111`. Some of these -docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` / -`index.d.cts`, `node.d.ts` / `node.d.cts` and `view-container.d.ts` / `view-container.d.cts`, and -comments that esbuild keeps appear in the JavaScript output (`index.js` / `index.cjs`, `node.js` / -`node.cjs`). - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20595-platform-objects-provenance-anchors.md b/.changeset/20595-platform-objects-provenance-anchors.md deleted file mode 100644 index 35ec805ac92..00000000000 --- a/.changeset/20595-platform-objects-provenance-anchors.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -Provenance comments in `@objectstack/platform-objects` cite the commits that decided them, not tracker numbers that no longer resolve - -Clause-②: no - -Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. -Each now cites the commit in this repository's history that made the decision it describes, except one -that cites ADR-0104's 2026-09-05 addendum, the record of that ruling. Some of these docblocks sit on -exported members, so the reworded text appears in the published declaration files (`apps`, `identity`, -`metadata-translations` and `system` `index.d.ts` / `index.d.mts`), and the field comments esbuild keeps -appear in the JavaScript output (`index`, `apps`, `audit`, `identity` and `plugin`, `.js` / `.mjs`). - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20749-lint-strings-stage1-state-the-decision.md b/.changeset/20749-lint-strings-stage1-state-the-decision.md deleted file mode 100644 index 2cfa47b9348..00000000000 --- a/.changeset/20749-lint-strings-stage1-state-the-decision.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -Flow, hook, action, approval and expression rule findings no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some findings these rules show to authors through `os validate`, `os lint` and `os build`, and the startup-registry findings a plugin author reads, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- Flow patterns: the record-change date-equality hint and the date-equality filter hint name the declarative alternative, a `schedule` flow whose start node carries a `config.timeRelative` descriptor; the unscoped `runAs` hint says `runAs` is enforced, so a run with no trigger user has its data operations refused rather than run unscoped; the unbounded bulk-write hint says `multi: true` is how a flow declares bulk intent and that the engine admits a whole-object write declared that way; the revise-target hint says the run-resume route continues a pause on a service-owned node type only through the service that owns it; the two interpolation hints say a flow node value is a string template in which only single-brace tokens resolve. -- Startup-registry findings: the open-vocabulary notes say the engine judges node types only once the vocabulary is sealed at `kernel:bootstrapped`; the prescription describes the lazy cache resolution and the ADR-0104 attestation by what each does; the assertive-wording finding describes its two incidents, and how each was fixed, in words. -- Expression findings: the field-level `visibleWhen` consequence names the `current_user` binding ADR-0089 D1 gives every runtime record surface; the retired `script` keys finding says spec 17 made `script` a call to a registered function and nothing else. -- Trigger readiness: the array `triggerType` hint says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. -- Body writes, readonly writes and approvals: the discarded `ctx.record` write says the snapshot stays read-only by design and an action writes through `ctx.api`; the `readonlyWhen` write finding says a bulk update strips the field from every matched row once any one of them is locked; the `queue` approver finding says the type was deprecated rather than built; the empty-slate hint says the admin override may act on any pending request, so that one nobody in its slate can decide never stays stuck. -- The other findings drop a citation the sentence already explained. - -Text only: no rule id, severity, condition or finding moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20749-lint-strings-stage2-state-the-decision.md b/.changeset/20749-lint-strings-stage2-state-the-decision.md deleted file mode 100644 index 0b1a4637f65..00000000000 --- a/.changeset/20749-lint-strings-stage2-state-the-decision.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -Data-model, filter, predicate, search, sort, security, seed, view, widget and registry findings no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -The remaining `@objectstack/lint` findings that `os validate`, `os lint` and `os build` show to authors, plus the `surfaceReason` texts of the exported `AUTHORING_RULES` registry and one integrity error, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- Data model: the bare declared `unique: true` warning says that protocol 18 rejects the spelling and that stored metadata still carrying it converts to `unique: 'global'`, which builds the same physical index. -- Empty filter combinators: the `$and: []`, `$or: []` and empty-node messages say every backend reduces an empty combinator to its boolean identity; the `$or: []` message says an empty disjunction never opens a read scope to the whole table. -- Null guards: the fail-closed outcome says a predicate that cannot evaluate refuses the write rather than being skipped. -- Visibility and metadata-form predicates: the fall-open consequence says failing open is the console's settled behaviour; the dotted right-hand-side message says the form evaluator keeps its right-hand side a literal by design and says why only in a development build. -- Component props: the advisory hint says props are judged at the authoring door as a warning before they become an error. -- Rule schema formats: the format hint says `rule-validator.ts` registers the default `ajv-formats` set so that a `format` is enforced on every write. -- Security posture: the unset-OWD message describes the leave_request incident (an object with no `sharingModel` let an ordinary read/write grant read and edit every other user's records); the `controlled_by_parent` message says the write is refused as a metadata defect rather than a permission denial. -- Seeds and views: the seed state-machine message says a seed records established facts rather than walking the lifecycle; the `views:` container message says the stack schema, the rule and the registration loop hold `views:` to one container-only contract. -- React pages: the absent-`groupBy` hint states the ruling directly. -- Liveness: the unrecognised-status integrity error says such a status fails loudly rather than being graded `dead`. -- `AUTHORING_RULES` `surfaceReason` texts: the full-snapshot, capability-reference and sharing-rule reasons name the runtime publish gate (the Studio, REST and MCP door that runs this registry) in place of a tracker number; the advisory-volume reason says the object door opened to the gating object rules alone; the component-types reason names the crossing discipline the gating object rules went through. -- The other findings (search fields, sort fields, nav servability, dashboard actions, widget bindings and the remaining predicate and combinator messages) drop a citation the sentence already explained. - -Text only: no rule id, severity, condition, finding or registry field moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20749-spec-strings-stage3-state-the-decision.md b/.changeset/20749-spec-strings-stage3-state-the-decision.md deleted file mode 100644 index e2824c5f693..00000000000 --- a/.changeset/20749-spec-strings-stage3-state-the-decision.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Field-key guidance, the retired `DriverCapabilities` tombstones, the datasource `readOnly` guidance, the retired filter operators and the legacy `apiMethods` strip warning no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -These are the `@objectstack/spec` texts an author meets at the moment something is refused or rewritten: the unknown-field-key guidance that `os validate` and the lint print, the parse errors for retired `DriverCapabilities` keys, the guidance for `readOnly` written inside a datasource driver's `config`, the `INVALID_FILTER` refusal every driver face prints for `$regex` / `$options`, and the warning `enable.apiMethods` prints when it strips a retired legacy value. They pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- Field-key guidance: `index` and `indexed` say the field-level index flag built no index and was removed under ADR-0049 enforce-or-remove; `dataQuality` and `cached` say their leftover `DataQualityRules` and `ComputedFieldCache` schemas were deleted from the public API too, and that computed-field caching returns only together with a runtime consumer. -- `DriverCapabilities` tombstones: the `bulkCreate` / `bulkUpdate` / `bulkDelete` prescriptions name discovery's `transactionalBatch` bit, derived from the live composition so a client negotiates instead of probing; the `fullTextSearch` prescription says `$contains` itself stays case-sensitive while textual search is case-insensitive. -- Datasource `readOnly` guidance: says a managed datasource has no read-only gate by decision, because a flag only the application checks cannot stop direct connections, migrations or DDL. -- Retired filter operators: the `$regex` and `$options` refusals say they are retired under ADR-0049 enforce-or-remove, refused rather than reinterpreted. -- Legacy `apiMethods` strip warning: the `restore` and `purge` prescriptions say `enable.trash` was retired because no runtime ever read it, and that the recycle-bin (soft-delete) work `restore` would need is parked. - -Text only: no key, schema shape, condition, error code or status moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20749-spec-strings-stage4-conversion-summaries.md b/.changeset/20749-spec-strings-stage4-conversion-summaries.md deleted file mode 100644 index 7e94e6deca3..00000000000 --- a/.changeset/20749-spec-strings-stage4-conversion-summaries.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The protocol 16 → 17 conversion summaries, the `autonumberFormat` description and two metadata route descriptions no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -A conversion's `summary` is the line an author reads when upgrading metadata: it is the "Change" column of `docs/protocol-upgrade-guide.md`'s protocol 16 → 17 table, the `to` text of `spec-changes.json`'s `converted[]` records, and what `os migrate meta --json` reports under `specChanges`. Fifty-six of the protocol-17 summaries pointed at an issue-tracker number for the reason behind a rewrite. The number goes; where the sentence did not already say what was decided, it now does. For example: - -- `action-execute-to-target` says the spec and the renderer had resolved `execute` / `target` in opposite directions, so one key now names the handler. -- `stack-api-require-auth-removed` names the declarations that replaced the deployment-wide opt-out: a public form, a share link or `book.audience: 'public'`. -- `retry-policy-converged` says why the merged default is 0 / 1: retry is opt-in, because a retry replays whatever the attempt already did. -- The flow-node alias entries say each one was an undeclared executor fallback that graduates into the conversion layer. - -The same goes for `FieldSchema.autonumberFormat`'s description (the `{0000}` default is a contract default every driver and the engine fallback read) and the descriptions of `GET /meta/:type/:name/layers` and `POST /meta/:type/:name/publish`. - -Text only: no conversion's id, surface, protocol step, transform or order changes, and no schema key, shape or default moves. A tool or test that matches the old summary text (for example a tracker-number suffix) needs the new spelling. The protocol 17 → 18 summaries are a later change. diff --git a/.changeset/20749-spec-strings-stage5-conversion-summaries.md b/.changeset/20749-spec-strings-stage5-conversion-summaries.md deleted file mode 100644 index 8c73367ffae..00000000000 --- a/.changeset/20749-spec-strings-stage5-conversion-summaries.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The protocol 17 → 18 conversion summaries no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -A conversion's `summary` is the line an author reads when upgrading metadata: `os migrate meta --json` reports it under `specChanges` (its chain already runs to protocol 18), and it becomes the "Change" column of the upgrade guide's protocol 17 → 18 table and the `to` text of `spec-changes.json`'s `converted[]` records once protocol 18 ships. Thirty-five of the protocol-18 summaries pointed at an issue-tracker number for the reason behind a rewrite. The number goes; where the sentence did not already say what was decided, it now does. For example: - -- The six duration-key renames (`hook.timeout` → `timeoutMs`, `apis[].cacheTtl` → `cacheTtlSeconds` and the rest) say the rule they follow: a duration key carries its unit in its name. -- `translation-per-app-settings-removed` says why both application doors lose `settings`: settings copy belongs to the platform, and the bundle entry and the translation item are two doors of one type that accept one shape. -- `flow-decision-mode-inclusive-explicit` says the decision node now follows mainstream engines (first match wins) and that taking every true edge must be declared. -- `list-view-sort-string-clause-to-array` and `page-component-filter-record-to-rule-array` say what "one orthography platform-wide" means for each, and why combinator filters are named rather than flattened. - -Text only: no conversion's id, surface, protocol step, transform or order changes, and no schema key, shape or default moves. A tool or test that matches the old summary text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20749-spec-strings-stage6-conformance-notes.md b/.changeset/20749-spec-strings-stage6-conformance-notes.md deleted file mode 100644 index 2ee23826ce5..00000000000 --- a/.changeset/20749-spec-strings-stage6-conformance-notes.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The shared conformance tables' case notes and names no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -The conformance tables in `@objectstack/spec` (`FILTER_LOGIC_CASES`, `FILTER_TEXT_CASES`, `FILTER_COMPARAND_TYPE_CASES`, `AGGREGATION_CASES`, `TEMPORAL_ROWS` / `TEMPORAL_CASES` / `TEMPORAL_TIME_CASES`, `VALUE_ROUNDTRIP_CASES`, `TEXT_OPERATOR_DOOR_TYPE_CLASSES` and `METADATA_ROUNDTRIP_CASES`) are what every driver, and any third-party implementation, is measured against. A case's `note` or `why` is printed when that case fails, and some drivers print it in the test title. Fifty-eight of those texts pointed at an issue-tracker number for the reason a case exists. The number goes; where the sentence did not already say what was decided, it now does. For example: - -- The four empty-combinator cases say every face reduces an empty combinator to its boolean identity, and why `{}` and `$not: {}` follow from it. -- The no-value cases say `$ne`, `$nin`, `$notContains` and `$not` are NULL-safe on every face, and that `$exists` means "has a value" because SQL cannot tell a missing key from a stored null. -- The boolean-aggregand cases say a boolean is worth 1 or 0 on every face, including for `min` / `max`, and that this ruling superseded an earlier `false` / `true` answer. -- The `$empty` cases say every face answers `$empty` by the field's declared type. - -Six case names change with them: `icontains (the infix/view spelling, ruled never an alias of ilike) lowers to $icontains — …` in `FILTER_TEXT_CASES`, and five names in `FILTER_COMPARAND_TYPE_CASES` (the control cell, the bigint crash cell, and the three array-in-the-equality-slot refusals, which now say they are refused at the shared face). - -Text only: no case's filter, input, expected rows, verdict, error code, order or count changes, and no export, type or schema moves. A tool or test that selects or pins a case by its old note or name (for example by a tracker-number substring) needs the new spelling. diff --git a/.changeset/20749-spec-strings-stage7-registry-rationales.md b/.changeset/20749-spec-strings-stage7-registry-rationales.md deleted file mode 100644 index cc53251ea5f..00000000000 --- a/.changeset/20749-spec-strings-stage7-registry-rationales.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The error-code waiver reasons and the public auth-feature registry's notes no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Two registries in `@objectstack/spec` carry a written reason beside each entry. In `@objectstack/spec/api`, every `STANDARD_SYNONYM_WAIVERS` and `PROVENANCE_WAIVERS` entry records why a registered error code is kept or placed where it is. In `@objectstack/spec/kernel`, `PUBLIC_AUTH_FEATURES` records how each public auth flag is consumed. Seventeen of those reasons pointed at an issue-tracker number for the decision behind them. The number goes; where the sentence did not already say what was decided, it now does. For example: - -- The five grandfathered synonyms (`CONFLICT`, `FORBIDDEN`, `INTERNAL`, `NOT_FOUND`, `UNAUTHORIZED`) say their consolidation onto the standard member is deferred until a code has a measured victim. -- The `FLOW_DISABLED` waiver says every door that dispatches a flow answers from one status table. The `TENANT_SCOPE_REQUIRED` waiver says an uninstall across every organization must be declared, never inferred from a missing one. -- The `phoneNumber` note names the fix the registry generalizes: create-user's phone field follows the opt-in phoneNumber plugin. - -One reason also corrects a stale fact. The `deviceAuthorization` exemption described a known gap in objectui's `DeviceAuthPage`. That gap was closed in objectui on 2026-07-15: the page reads the flag and says device authorization is not enabled, rather than calling the device-auth endpoints. The text now says so. - -Text only: no waiver's code, package, shadowed member or registration, no flag's surface, semantics or gated inputs, and no export, type, schema, order or count moves. Each reason still parses under its schema's non-empty rule. A tool that matches one of these reasons by its old text (for example by a tracker-number substring) needs the new spelling. diff --git a/.changeset/20749-spec-strings-stage8-step17-rationale.md b/.changeset/20749-spec-strings-stage8-step17-rationale.md deleted file mode 100644 index 9910507f3c2..00000000000 --- a/.changeset/20749-spec-strings-stage8-step17-rationale.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The protocol 16 → 17 upgrade rationale no longer cites tracker numbers; each cited decision is stated in words - -Clause-②: no - -`MIGRATIONS_BY_MAJOR[17].rationale` in `@objectstack/spec` is the prose an author reads when upgrading metadata from protocol 16. `os migrate meta` prints it for each hop, and `docs/protocol-upgrade-guide.md` reproduces it word for word under "Protocol 16 → 17". It pointed at 116 issue-tracker numbers (91 distinct records, four of them in sibling repositories). Every number is gone. Where the sentence already said what was decided, the number was dropped. Where the number stood in for the decision, the decision is now stated. For example: - -- The app-area paragraph says the fail-open area gates' caveat "was CLOSED by a server-side fix inside this same 17.0.0 window". The fix is described in the same sentence: `filterAppForUser` now runs the same `filterNav` over every `areas[].navigation`. -- The datasource paragraphs name the change that validates `datasource.config` against the declared driver's own config contract, and the follow-up that retired the factory's legacy key aliases. -- The aggregation paragraph names the freeze it relied on as the maintainer's freeze on the in-memory and MongoDB drivers, lifted 2026-08-11. It names the divergence class as the one closed when every SQL face got one aggregate spelling and one refusal. -- The `view.exportOptions` paragraph says PDF export was declined platform-side as not planned. - -Text only. No step, conversion, semantic entry, retired key or retired def changes: no id, order, schema or behaviour. The generated upgrade guide was regenerated from the new text. A tool that matched this rationale by its old text, for example by a tracker-number substring, needs the new spelling. diff --git a/.changeset/20749-spec-strings-stage9-step18-rationale.md b/.changeset/20749-spec-strings-stage9-step18-rationale.md deleted file mode 100644 index 92a82e135fa..00000000000 --- a/.changeset/20749-spec-strings-stage9-step18-rationale.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The protocol 17 → 18 upgrade rationale no longer cites tracker numbers; each cited decision is stated in words - -Clause-②: no - -`MIGRATIONS_BY_MAJOR[18].rationale` in `@objectstack/spec` is the prose an author reads when upgrading metadata to protocol 18. `os migrate meta` prints it for that hop today, because its chain runs to the highest registered major, and `docs/protocol-upgrade-guide.md` will reproduce it once protocol 18 is cut. It is built from one fragment per retirement, and 49 of those fragments pointed at 88 tracker numbers: issue numbers in this repository and in objectui, and four decision-batch numbers. Every number is gone. Where the sentence already said what was decided, the number was dropped. Where the number stood in for the decision, the decision is now stated. For example: - -- The export-wildcard paragraph says the admin sets' wildcard was the export-axis twin of "the earlier removal of `member_default`'s CRUD wildcard". -- The `allowRestore` / `allowPurge` paragraph says the ruling "chose retiring the two bits over gating operations that do not exist", and that `allowTransfer` stays because the server guards who may rewrite a record's owner. -- The `reference_to` paragraph says the conversion is the server half of the ruling that the server normalizes the protocol and the renderer only executes it. -- The translation paragraphs give each ruling its date and its content: settings copy belongs to the platform, and one app metadata type has two authoring doors and one accepted shape. - -Text only. No fragment id or order, conversion, semantic entry, retired key or retired def changes: no schema or behaviour. No generated artefact prints step 18 yet, so none was regenerated. A tool that matched this rationale by its old text, for example by a tracker-number substring, needs the new spelling. diff --git a/.changeset/20751-services-strings-stage2-state-the-decision.md b/.changeset/20751-services-strings-stage2-state-the-decision.md deleted file mode 100644 index 6a008c57251..00000000000 --- a/.changeset/20751-services-strings-stage2-state-the-decision.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/connector-mcp': patch -'@objectstack/plugin-email': patch -'@objectstack/service-knowledge': patch -'@objectstack/service-queue': patch -'@objectstack/service-sms': patch -'@objectstack/service-storage': patch -'@objectstack/trigger-record-change': patch ---- - -MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. -- `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. -- `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. -- `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. -- `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. -- `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. -- `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20751-services-strings-stage3-state-the-decision.md b/.changeset/20751-services-strings-stage3-state-the-decision.md deleted file mode 100644 index 3eb37fac9c9..00000000000 --- a/.changeset/20751-services-strings-stage3-state-the-decision.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-datasource': patch -'@objectstack/plugin-approvals': patch ---- - -Datasource and approval refusals, warnings, field help and generated-draft comments no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings these two packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `@objectstack/service-datasource`: the credential-migration refusal says an unbindable key is either an alias spelling from before inline credentials were refused at publish, which no connection builder reads, or turso's `encryptionKey`, which has no secret slot of its own because the one slot carries the `authToken`; the remote-primary-key comment in a generated object draft says a driver's introspection can report only the first column of a composite key, so the list is a lower bound. -- `@objectstack/plugin-approvals`: the `queue` approver warning says the platform has no ownership queue to expand the type from, that the type is no longer offered for authoring, and to route the step to a team, department or position instead; the live-record warnings say approvers are being resolved against the trigger snapshot instead of the live record they are normally resolved from; the recall refusal's log line names the admin override; the `sys_approval_action` `via_override` help (in every shipped locale) says a platform or organization admin may act on any pending request, so that one nobody in its slate can decide never stays stuck; the cross-organization team, team-member and manager warnings, the expanded-to-nobody warning, the revise-window refusal, the `attachments` help and the `sys_approval_delegation` description drop their citations. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20751-services-strings-stage4-state-the-decision.md b/.changeset/20751-services-strings-stage4-state-the-decision.md deleted file mode 100644 index d5b7d11efb9..00000000000 --- a/.changeset/20751-services-strings-stage4-state-the-decision.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-automation': patch -'@objectstack/plugin-audit': patch ---- - -Automation refusals, prescriptions, log lines and run-object field help, and the activity type help, no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings these two packages show to flow authors, operators and administrators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `@objectstack/service-automation`: the refusal for a `fieldValues` write map says a runtime alias for it was rejected by design, so the node keeps one strict `fields` key; the refusal for a screen field's `visibleIf` says a predicate under any other key is never read, so the field always shows, and a `required` field meant to stay hidden then blocks the screen from ever being submitted; the undeclared-config-key refusal says the built-in node types were reconciled so that every key their executors read is declared; the unknown-function error in a flow value expression says such a name is refused rather than evaluated to null, which would write the field as undefined; the inert-connector warning says entries without a `provider` are catalog descriptors, while an entry that names a `provider` is a connector instance that provider's installed executor materializes; the `sys_automation_run` field help says the paused node's type decides who may continue a run (an approval pause only through its owning service), that rows written before run history recorded its trigger were not backfilled, and that a finished run's bounded step log keeps its per-node detail across a restart; three bridge debug lines say what each bridge provides. The bulk-intent guidance, the degraded-connector dispatch error and retry lines, the user-less `runAs` warning and refusal, the unclaimed-branch warning, the script-function and node-config refusals and the `sys_flow_dispatch` description drop their citations. -- `@objectstack/plugin-audit`: the `sys_activity` `type` help, whose English text all four shipped locale bundles carry, says the vocabulary is open by decision, not a gap awaiting enforcement. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20751-services-strings-stage5-state-the-decision.md b/.changeset/20751-services-strings-stage5-state-the-decision.md deleted file mode 100644 index 1f52bb33350..00000000000 --- a/.changeset/20751-services-strings-stage5-state-the-decision.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -Security refusals, explain details, field help and log lines no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings the security plugin shows to administrators, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- The curated capability-name refusal says a curated name is refused at authoring so that no admin-authored row can collide with the row the platform seeds for it. -- The two delegation anchor refusals say the business-unit anchor roots the delegate's business-unit visibility, so a delegation may only narrow it. -- The `managed_by` field help on `sys_permission_set` and `sys_position`, in every shipped locale, says capabilities, permission sets and positions all share one platform / package / admin vocabulary. -- The explain details for an unresolvable security posture and for the View/Modify All Data bypass drop their citations; those sentences already said that access fails closed and that the write path consults the same bypass. -- The derived-capability boot warning says the derivation refreshes a row's label and description only when it can prove the row is the platform's own, and that the seeder neither adopts a row it cannot prove is its own nor backfills provenance on the operator's behalf. -- The fail-closed log lines say what each denial protects: a `controlled_by_parent` child is readable and writable only where its master is, and a chain the derivation cannot resolve admits no child; only a resolved sharing allow (Modify All Data or an edit-level share) may replace the platform ownership floor; an authored-policy verdict that cannot be resolved never lifts the sharing refusal; a path that bypasses the engine middleware never runs without the owner and share scope a direct read applies; a delegated read is never scoped wider than its delegator's own; an unreadable posture never defaults to public or uncontracted. -- The public-form line says an anonymous submission cannot set ownership, tenancy or audit columns; the uninstall line says a package's permission rows are removed by `package_id`, so no grant outlives the package; the platform-owner wall-bypass line says only the declared platform owner's reads cross the wall and writes stay walled for everyone. The org-scoping entitlement, masking-rule, permission-set resolution, vocabulary-normalization and service-registration lines drop their citations, and the log lines that carried a tracker number in their `[security/…]` prefix now open with `[security]`. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix or prefix) needs the new spelling. diff --git a/.changeset/20751-services-strings-stage6-state-the-decision.md b/.changeset/20751-services-strings-stage6-state-the-decision.md deleted file mode 100644 index 0860b5d3af9..00000000000 --- a/.changeset/20751-services-strings-stage6-state-the-decision.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-sharing': patch -'@objectstack/plugin-audit': patch ---- - -Sharing refusals and log lines, and the audit write-failure line, no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings these two packages show to administrators and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- `@objectstack/plugin-sharing`: the orphan-sweep line for record shares says every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it; the same line for share links says a share link is a bearer token, so a reused record id must not inherit it; the write-gate failure line says a failed lookup is a refusal, never an abstention, because an abstention would hand the row to the other write authorities, which may admit it; the authored-row-write probe line says only an app-authored row-level policy that positively admits the row may lift the sharing refusal; the hierarchy-scope line says the resolver contract makes a resolver fail closed on a missing organization. The two sharing-rule refusals (no active organization; deleting a platform-global rule) drop their citations, since each sentence already says why. The `OrphanSweepSubject.issue` member's doc comment now says the member carries that reason in words. -- `@objectstack/plugin-audit`: the missing-table fix in the audit write-failure line says that on a fresh `os dev` boot the table exists in the sibling telemetry file and not in the primary one, so look there before concluding it was never created. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20751-services-strings-stage7-state-the-decision.md b/.changeset/20751-services-strings-stage7-state-the-decision.md deleted file mode 100644 index 8d6394c9681..00000000000 --- a/.changeset/20751-services-strings-stage7-state-the-decision.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -Analytics filter refusals, the no-strategy diagnostic and the cube-gate warning no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings the analytics service shows to callers, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- The two field-reference refusals (a `{ $field }` comparand the SQL lowering cannot render, and a `{ $field }` used as a `$between` bound) say the engine path's driver enforces the cross-field rules (declared same-table columns only, never the tenant-isolation column, one comparison class) with metadata it owns, so those rules are enforced in one place. The bound refusal also says `FieldReferenceSchema` was removed from the `$between` endpoint union rather than implemented there, since nothing asked for it. -- The no-strategy diagnostic for a cross-field filter on a deployment with no aggregate bridge says the same about the engine path. -- The `where` refusals: an undefined comparand is refused rather than read as null, on the SQL drivers and on this door alike; a field constraint with zero operators is refused on every backend, because neither "every row" nor "no row" is the author's intent; a field constraint mixing `$` operators with bare keys is refused by both doors in the package; and the two filter-array refusals say a filter array is lowered at every door or refused, never dropped, so it means the same rows whichever door it enters. Where the undefined-comparand refusal cited a tracker number for the silent widening, it now says that a dropped predicate widens the query; the mixed-wrapper refusal already said so and only drops its citation. -- The dotted-measure refusal drops its citation; the sentence already says measures do not traverse relationships and that the prefix used to be dropped silently. -- The warning logged when no object-registry hook is configured says the inactive gate is the one that answers 404 `CUBE_NOT_FOUND` for a name that is neither a registered cube nor a registered object. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20751-services-strings-stage8-state-the-decision.md b/.changeset/20751-services-strings-stage8-state-the-decision.md deleted file mode 100644 index a00711f9b01..00000000000 --- a/.changeset/20751-services-strings-stage8-state-the-decision.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -The read-scope comparand refusals, the native-SQL cross-field backstop and the two display-SQL echo refusals no longer cite tracker numbers; each one states the decision behind it in words - -Clause-②: no - -Some strings the analytics service shows to operators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. - -- The read-scope compiler's undefined-comparand refusal says an undefined comparand is refused rather than read as null, on the SQL drivers and on this door alike. Its refusal of a non-boolean `$null`, `$exists` or `$empty` comparand says a non-boolean comparand for any of the three is refused rather than coerced, on every driver and on this door alike. Both still say they fail closed, and that the producer to fix is whoever built the read scope, never the caller of the query. -- The native-SQL strategy's cross-field backstop and the `/analytics/sql` echo's refusal of a field-reference comparison say the engine path's driver enforces the cross-field rules (declared same-table columns only, never the tenant-isolation column, one comparison class) with metadata it owns, so those rules are enforced in one place, next to the metadata they read. -- That echo refusal and the echo's unmapped-operator refusal say the echo renders every predicate the query runs with, or refuses. - -Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. diff --git a/.changeset/20790-flow-credential-channel.md b/.changeset/20790-flow-credential-channel.md deleted file mode 100644 index 0f65579db7c..00000000000 --- a/.changeset/20790-flow-credential-channel.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-automation': minor -'@objectstack/metadata-protocol': minor -'@objectstack/trigger-api': minor -'@objectstack/runtime': minor ---- - -feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790) - -Clause-②: yes (widening) - -A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it. - -**⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '' (): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification. - -What else changes: - -- **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`. -- **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential. -- **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials. -- **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing. -- **Refused now, loudly**: - - With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again. - - The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret. - - diff --git a/.changeset/20822-retired-matcher-pointers.md b/.changeset/20822-retired-matcher-pointers.md deleted file mode 100644 index 18263e6811f..00000000000 --- a/.changeset/20822-retired-matcher-pointers.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/service-analytics': patch -'@objectstack/formula': patch -'@objectstack/objectql': patch ---- - -Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it - -Clause-②: no - -`driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships: - -- `@objectstack/spec`: - - The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it. - - `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`). - - `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did. - - A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it. -- `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too. -- `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`. -- `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it. - -Comment only: no export, type, error code, status, message text or runtime behaviour changes. diff --git a/.changeset/20827-choice-door-select-radio-needs-options.md b/.changeset/20827-choice-door-select-radio-needs-options.md deleted file mode 100644 index 7b3fefd0d23..00000000000 --- a/.changeset/20827-choice-door-select-radio-needs-options.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -fix(spec)!: `FieldSchema` refuses a `select` / `radio` field with neither `options` nor `picklist` (#20827) - -Clause-②: yes - - - -**BREAKING** accept-set narrowing on `FieldSchema`, shipped as `minor` under the -repo's launch-window convention for breaking changes — the grade the `reference` -precedent shipped with (a `lookup` / `master_detail` without `reference`, refused -at parse as a `minor` with the **BREAKING** header). - -**What was accepted before.** A `select` or `radio` field with no `options` key, -with `options: []`, and with no `picklist` parsed cleanly. It is a choice with -nothing to choose: the form control offers nothing, and server-side value -validation is off (the record validator checks membership only against a -non-empty allowed list), so any value writes through the API. The author-time -completeness gate (ADR-0078, `field/choice-without-options`, used by `os build`, -`os validate` and `os lint`) already graded it an error, and registration warns on -it; a runtime-API or Studio save was the one door that let it through. - -**What is refused now.** At parse, on the `options` path, with a `custom` issue -that names the field type and both remedies: a `select` / `radio` whose `options` -is absent or empty and whose `picklist` is absent. The predicate is the -completeness gate's own, so the two cannot disagree. - -**The fix.** Declare `options: [{ label, value }]` with at least one entry, or -`picklist: 'industry'` (the name of any shared list) to offer a shared list — -never both (that pair stays refused as before). If any value is meant to be allowed, use a `text` field instead. - -**Unchanged.** `multiselect` and `tags` keep parsing without options (free-form -by design), and `checkboxes` keeps parsing with a completeness warning. A -`select` / `radio` with at least one option, or with a `picklist`, parses as -before. The ADR-0078 author-time rule and the registration warning are -unchanged — this door is one more gate, not a replacement. `Field.select()` -called with an empty list emits `options: []`, which is now refused at parse. - -**Stored rows.** No conversion can supply the missing options, so a row saved -before this release is not rewritten. It is still served — with -`_diagnostics.valid: false` naming `fields.FIELD.options` — and still -registered at boot (counted invalid); a later save of its object is refused -until an option or a `picklist` is added. To find such rows, read -`GET /api/v1/meta/diagnostics`, or the boot log's `field/choice-without-options` -lines. The `os migrate meta --stored` preview does not validate bodies, so it -does not find them: it counts such a row canonical, or — when the row also -carries an older spelling to lower — pending, and the apply then reports that -row failed and leaves its bytes as they were. diff --git a/.changeset/20871-page-requires-live.md b/.changeset/20871-page-requires-live.md deleted file mode 100644 index b9cc60b784c..00000000000 --- a/.changeset/20871-page-requires-live.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/lint': patch ---- - -`page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5). - -Clause-②: no - -The key's description used to say the list is "validated at save and load" while the liveness ledger recorded it as not enforced yet. Both are now true and say so. On a server that has the deployment's SDUI component manifest, saving a `kind: 'html'` page compiles its source, refuses a written `requires` that disagrees with it (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`; a draft at its publish) and stores the derived list. At load, a stored page whose list names a plugin no manifest component carries is reported and still served. A server with no manifest checks neither and says so once at boot. Omit `requires`: it is derived from the source. The liveness row moves from `planned` to `live`, and the generated page reference carries the new description. - -`validateJsxPages`' reason for staying off the runtime publish gate no longer says it parses through `typescript`/`sucrase`. It parses with the dependency-free `@objectstack/sdui-parser`, and it stays CLI-only because the save door already runs that compiler on every html page. The `ui-html-page-div-refused` upgrade-guide entry now names that save door too: on a server with a manifest, a `div` page saved from Studio or through the metadata API is refused under the same rule ids. - -No schema accepts or refuses anything it did not before, and no runtime behaviour changes. diff --git a/.changeset/20936-action-name-refs-related-list.md b/.changeset/20936-action-name-refs-related-list.md deleted file mode 100644 index 9eaf990a503..00000000000 --- a/.changeset/20936-action-name-refs-related-list.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -fix(lint)!: `action-name-undefined` resolves `record:related_list` action ids against the related object, and refuses an id the list cannot draw (#20936) - -Clause-②: no (narrowing) - -`action-name-undefined` is the authoring gate for "a surface names an action that renders nothing". It already walked list-view row and bulk menus, the `record:quick_actions` bar, the `record:alert` call-to-action, the `page:header` action ids and app navigation. One page surface that binds actions by id was never read: `record:related_list` → `properties.actions`. - -The console now reads that key. It resolves each id against the RELATED (child) object's own actions, never the page's object, and places it by that action's own `locations`: `list_toolbar` draws a header button, `list_item` and `record_related` draw a row-menu item. An id that names no action of the child object, or an action placed at none of those three, draws no button; the list shows a refusal notice naming it instead. The spec types the key as plain strings, so a misspelled id passed spec validation and lint and surfaced only at runtime. - -The rule now walks the key, scoped to `record:related_list`, and answers the same two questions the renderer asks: - -- each string id must name an action of the related object: one written on that object, or a `stack.actions` entry bound to it by `objectName`. An id defined only on the page's object, or only as a global action, is refused like a typo, and the message names where it is defined. The did-you-mean and the hint's action list are the related object's own; -- the action it names must declare at least one location a related list draws. The location set is read from the spec's `ACTION_LOCATIONS` vocabulary, classified per member, so a location added to the vocabulary has to be classified before this package compiles. - -The related object is the component's bound `dataSource.object` when one is set, otherwise `properties.objectName`. A related object this stack does not define is skipped: its actions belong to another package, and the rule does not guess. Inline-object elements are skipped, as on `page:header`, and every id is reported at its authored index. Every other walk of the rule is unchanged: it still asks only whether a name is defined anywhere in the stack. - -**What moves for consumers.** A stack whose related list names an id the list cannot draw built clean before and now fails `os validate` / `os lint` / `os build` with `action-name-undefined` (severity `error`). That id never rendered a button, so nothing that worked stops working. The rule still does not run at the runtime publish door for `page` writes. No related list in the platform's own pages or in the example apps authors `actions`, so none of them changes. - - diff --git a/.changeset/21000-analytics-metric-type-verdict.md b/.changeset/21000-analytics-metric-type-verdict.md deleted file mode 100644 index a1bf3e8de23..00000000000 --- a/.changeset/21000-analytics-metric-type-verdict.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: both analytics strategies refuse a cube measure whose `type` names no aggregate, in the spec's words — the custom-SQL `EXPRESSION_METRIC_TYPES` partition is gone with the three types it named (#21000) - -**BREAKING** — `@objectstack/spec` retired the cube metric types `number`, `string` -and `boolean` from `AggregationMetricType` (a measure's `sql` is a column reference, -so they had nothing left to compute). Every door that parses a cube refuses them; -this release removes the runtime branches that still served them for a cube that -reached the analytics service WITHOUT meeting that parse — one a host registers -in-process from a literal, through `AnalyticsServicePlugin({ cubes })` or -`AnalyticsService({ cubes })` (the registry never parses). - -| | before | now | -| --- | --- | --- | -| `NativeSQLStrategy`, a measure typed `number` / `string` / `boolean` | served: the column emitted UNAGGREGATED in the statement (`amount AS "m"` beside `GROUP BY`) | refused, nothing executed | -| `ObjectQLStrategy`, the same measure | refused `INVALID_FIELD` / 400 | refused, nothing executed | -| either strategy, a type the spec never declared (`median`) | native: refused; ObjectQL: forwarded to `executeAggregate` as the method (the auto-bridge refused it; a host's own executor received it), and `/analytics/sql` echoed `MEDIAN(amount)` | refused, nothing executed | - -**The one refusal** is `aggregateOfMeasure`'s, shared by both strategies and both -doors (`POST /analytics/query` and `POST /analytics/sql`): it names the measure and -the cube, then quotes the spec's own verdict on the type — for a retired type the -retirement prescription (the six aggregates to choose from, and where a per-row or -derived value goes instead), for anything else zod's message listing the six. It is -a bare `Error`, the undeclared-500 tier this package assigns to a cube that never -met the parse, so the HTTP answer is `500` with the message readable in the body -(measured through the dispatcher's analytics route), never a caller-blaming `400`. -The ObjectQL envelope for the three retired types therefore moves from -`INVALID_FIELD` / 400 to that tier. - -**The fix:** give the measure one of the six aggregate types — `count`, `sum`, -`avg`, `min`, `max`, `count_distinct` — or parse the cube through `CubeSchema` -before registering it, which refuses the same types with the same prescription. - -**Removed export:** `EXPRESSION_METRIC_TYPES` from -`strategies/native-sql-strategy.ts` (internal to the package; not re-exported from -its entry point). **Unchanged:** every aggregate measure on both strategies, the -auto-bridge's own parse of an engine method (still pinned, driven directly), and -`GET /analytics/meta`, which keeps publishing each registered measure's `type` as -registered. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21000-cube-metric-expression-types-retired.md b/.changeset/21000-cube-metric-expression-types-retired.md deleted file mode 100644 index ab71e17b317..00000000000 --- a/.changeset/21000-cube-metric-expression-types-retired.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: retire the cube metric types `number`, `string` and `boolean` — a measure's `sql` is a column reference, so the custom-SQL-expression types had nothing left to compute (#21000) - -**BREAKING** — three members leave `AggregationMetricType`, so a cube measure's -`measures..type` no longer accepts `number`, `string` or `boolean`. ADR-0049 -enforce-or-remove. They declared "a custom SQL expression returning a number / -string / boolean": the measure's `sql` was the whole computation. A cube member's -`sql` is a column reference since `cube-member-sql-expression-retired` (#20943), so -the three were left naming nothing: measured before this change, the raw-SQL -analytics path returned the referenced column UNAGGREGATED (a bare column in a -grouped statement — by SQL's own rules an error on PostgreSQL and an arbitrary row's -value on SQLite), and the ObjectQL path refused the measure. The six aggregates — `count`, `sum`, -`avg`, `min`, `max`, `count_distinct` — are unchanged and are now the whole -vocabulary. - -### FROM → TO - -| removed | what to write instead | -| --- | --- | -| `measures..type: 'number'`, `'string'` or `'boolean'` | the aggregate the measure means: `sum`, `avg`, `min` or `max` over the column; `count` (over `'*'` for a row count, or over a column for its non-null values); or `count_distinct`. | -| a measure whose old expression computed a value per row | keep that value as a field of the object (a stored or formula field) and aggregate the field. | -| a measure whose old expression combined measures (a ratio, a difference) | `derived: { op, of: [...] }` on an ADR-0021 dataset over the same object. | - -**The one-line fix: give the measure an aggregate type.** There is no mechanical -rewrite — the column alone does not say whether `amount` meant its sum, its average -or its largest value — so `os migrate meta` lists nothing for this change. - -Each retired member is refused at parse with a prescription naming the six -aggregates, at the measure's `type`, and in `tsc` (the members are gone from the -`AggregationMetricType` type). A value the enum never declared keeps zod's own -message. - -### The retirement kit - -- **Value-level retirement.** `AggregationMetricType` is declared through - `enumWithRetiredValues` (`shared/retired-key.ts`), with the prescriptions - module-private. No authorable KEY and no def changed, so nothing lands in - `RETIRED_KEYS_BY_MAJOR`, and the four surface ratchets (`api-surface`, - `authorable-surface`, `json-schema.manifest`, `api-surface-signatures`) are - byte-identical. -- **No D2 conversion, by design.** A stored or built cube that still carries one of - the three is REFUSED, never rewritten or dropped: the boot door - (`ObjectStackDefinitionSchema`, which a built artifact is parsed through), the - `analytics_cube` write door and `defineStack` refuse it with the prescription, and - the rehydration seam replays no conversion over it. -- **D3 entry `cube-metric-expression-types-retired`**, with its step-18 rationale - fragment, carries the judgement the upgrader owes: which aggregate each measure - meant. -- **Liveness.** The `analytics_cube` row `measures.type` stays `live`, re-verified - 2026-10-02, with the narrowing recorded. -- **Docs.** The `data/analytics` reference page is regenerated. -- **No deprecation window**, per the project's startup-stage posture. - -### Reach, measured - -- This repository authors no cube measure of the three types outside tests: - `examples/**`, `packages/**` (the platform objects included) and the skills and - docs carry none. The showcase cube's `type: 'string'` entries are dimensions, - whose `DimensionType` is a separate enum and is unchanged. -- objectui at its pinned commit carries no `AggregationMetricType` mirror and no - cube measure of the three types. -- Out-of-repo authored cubes: NOT MEASURED. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21015-element-text-variant-heading-retired.md b/.changeset/21015-element-text-variant-heading-retired.md deleted file mode 100644 index 05d995172c8..00000000000 --- a/.changeset/21015-element-text-variant-heading-retired.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/platform-objects': patch ---- - -feat(spec)!: `element:text` `variant` refuses `heading` / `subheading` by name — the vocabulary is the nine `ui:text` publishes, and `os migrate meta` rewrites them to `h2` / `h3` (#21015) - -**BREAKING** — `heading` and `subheading` leave `ElementTextPropsSchema.variant` (an -`element:text` page component's `properties.variant`). This is the second release of -the ruled two-release convergence on the nine values `ui:text` publishes — `h1`-`h6`, -`body`, `caption`, `overline`. 17.5.0 added the nine and refused nothing; 17.6.0 was -the full release in which both vocabularies parsed; this release refuses the two old -spellings. A heading is a document level, not a text style: `heading` and -`subheading` named a style and left the renderer to pick the level. - -### FROM → TO - -| removed | what to write instead | -| --- | --- | -| `variant: 'heading'` | `variant: 'h2'` — the heading element `heading` always rendered — or the level the page outline means. | -| `variant: 'subheading'` | `variant: 'h3'` — the heading element `subheading` always rendered — or the level the page outline means. | - -**The one-line fix: `heading` → `h2`, `subheading` → `h3`.** -`os migrate meta --from 17` lists the mechanical edits for existing sources. - -The rewrite keeps the heading ELEMENT (so the document outline is unchanged) but not -the size: `heading` drew in the `h3` style and `subheading` in a medium-weight small -heading style, and `h2` / `h3` draw their own, larger styles. Where the old look -mattered more than the level, pick the level whose style you want. - -Each retired spelling is refused at parse with a prescription naming the level to -write, and in `tsc` (the two members are gone from the input type). Any other unknown -value keeps zod's own message. An `element:text` with no `variant` still parses to -`body`. - -### The retirement kit - -- **Value-level retirement.** The enum is declared through `enumWithRetiredValues` - (`shared/retired-key.ts`), with the two prescriptions module-private. No authorable - KEY and no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four - surface ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`, - `api-surface-signatures`) are byte-identical; the generated component reference - page drops the two values. -- **D2 conversion `element-text-variant-heading-levels`** (step 18, retired from the - load path): `heading` → `h2` and `subheading` → `h3` on every `element:text` page - component — regions, named slots and container nesting. Stored `sys_metadata` page - rows replay it at rehydration; one notice per rewritten block. -- **D3 entry `element-text-variant-heading-subheading-retired`** carries the judgement - the conversion cannot make: whether the rewritten level is the one the page means. -- **No further deprecation window**: 17.6.0 was the window the ruling asked for. - -### Producers moved in this repository - -- `@objectstack/platform-objects`: the four section headings on the `sys_user` record - page's Security tab (`Password & Sign-in`, `Two-Factor Authentication`, `Email - Verification`, `Danger Zone`) move from `subheading` to `h3`. They render the same - h3 element, in the `h3` style. -- `examples/app-showcase`: the `page-variables` detail heading moves to `h3`. - -⚠️ **The out-of-repo author population is NOT MEASURED.** `@objectstack/spec` is -published, and tenant-authored pages were not measured. In this repository the five -writers above were the only ones outside `packages/spec`. objectui at `main` authors -neither value; its `element:text` renderer, registry `inputs` enum, html tier and the -published `sdui.manifest.json` still list the two, and drop them once this release is -installable there (the objectui follow-up). - -Clause-②: no (narrowing) - - diff --git a/.changeset/21018-cli-generate-picklist.md b/.changeset/21018-cli-generate-picklist.md deleted file mode 100644 index 24ef66b8474..00000000000 --- a/.changeset/21018-cli-generate-picklist.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -feat(cli): `objectstack generate picklist NAME` scaffolds a shared option list, and the metadata summary counts picklists - -Clause-②: yes (widening) - -- **`objectstack generate picklist NAME`** (alias `os g picklist`) writes `src/picklists/NAME.picklist.ts`, a list declared with `definePicklist({ name, label, options })`, and adds its export line to `src/picklists/index.ts`. The list is collected under the `picklists` stack key. A select field takes its options from the list by naming it, `Field.select({ picklist: 'NAME' })`, in place of options of its own. The server serves that field with the list's options resolved onto it, together with any options other packages add through `picklistExtensions`, and judges writes against them. `objectstack validate` and `objectstack build` refuse a field whose `picklist` names no list the stack declares, and so does the boot. -- **`objectstack init`** wires the new `src/picklists` barrel in the `app` and `plugin` templates, the same way it wires every other directory `objectstack generate` writes into: an empty `src/picklists/index.ts` and a `picklists: exportsOf(picklists)` key in `objectstack.config.ts`. A project scaffolded by an earlier release keeps its config. `objectstack generate picklist` then reports the list as not wired and prints the import line and the `defineStack` key to add. -- **The metadata summary** that `objectstack validate`, `objectstack build` and `objectstack info` print counts the picklists a stack declares, in the `Data:` row: `Data: 1 Objects 3 Fields 1 Picklists`. A stack that declares none prints the row it printed before. The `stats` object in the `--json` output of the same three commands gains a `picklists` count. A `picklistExtensions` entry is not counted as a list. diff --git a/.changeset/21018-create-objectstack-picklists-barrel.md b/.changeset/21018-create-objectstack-picklists-barrel.md deleted file mode 100644 index 2acd832195c..00000000000 --- a/.changeset/21018-create-objectstack-picklists-barrel.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'create-objectstack': minor ---- - -feat(create-objectstack): the blank starter wires a `src/picklists` barrel for `objectstack generate picklist` - -Clause-②: yes (widening) - -A new blank project ships an empty `src/picklists/index.ts`, and its `objectstack.config.ts` imports it and hands its exports to `defineStack` under `picklists`, as it already does for every other directory `objectstack generate` writes into. `objectstack generate picklist NAME` then writes `src/picklists/NAME.picklist.ts` and its export line, and the list is part of the stack with no edit to the config. A select field takes its options from the list with `Field.select({ picklist: 'NAME' })`, and the server serves that field with the list's options. - -A project scaffolded by an earlier release keeps its config. There, `objectstack generate picklist NAME` writes the list, reports that it does not reach the stack, and prints the import line and the `defineStack` key that wire `src/picklists`. diff --git a/.changeset/21082-cube-member-json-stored-refused.md b/.changeset/21082-cube-member-json-stored-refused.md deleted file mode 100644 index 2b1e8559c15..00000000000 --- a/.changeset/21082-cube-member-json-stored-refused.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -fix(lint)!: `os validate`, `os build` and `os lint` refuse an `analyticsCubes` dimension over a JSON-stored column, and a cube `count_distinct` measure over one, which the analytics door already refuses at query time - -Clause-②: no (narrowing) - - - -**BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail. An authored analytics cube (`defineStack({ analyticsCubes })`) is queried through the same analytics door as a compiled dataset, and that door refuses a query that groups by a JSON-stored column, or counts its distinct values, with `400 INVALID_FIELD` before any SQL is built. So such a member could be declared but never served, and until now no authoring rule read `analyticsCubes` at all. The dataset rule's two ids now judge cube members as well: `dimension-json-stored-field-refused` and `measure-aggregate-field-type-refused` (gating, `error`). It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. - -**What is refused.** On a cube whose `sql` names an object the stack defines: a `dimensions` entry whose `sql` column is declared with a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) or a multi-value declaration (`multiselect`, `checkboxes`, `tags`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`); and a `measures` entry of `type: 'count_distinct'` whose `sql` column is either. The column is the member's `sql`: a column of the cube's object, or a relationship path read on the object its last hop reaches (the join the cube declares for that hop, else the lookup field's `reference`). The classes are `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, `isMultiValueField` and the `count_distinct` row of `AGGREGATE_FIELD_TYPE_COMPATIBILITY`, the predicates the door reads. - -**What an author sees now.** The finding names the cube, the member, the column, the object that declares it and its declaration, and says the analytics door refuses it with `400 INVALID_FIELD`. It names the route: group by, or count the distinct values of, a field that stores one scalar value; for a multi-value field, filter by one member with `$contains` in a record query. It is located at `analyticsCubes[N].dimensions.KEY.sql` or `analyticsCubes[N].measures.KEY.type`, where `KEY` is the member's key. - -**Unchanged.** Every dataset finding, word for word. A cube member over any other column, a single-value `select` or `lookup` included; a `count` measure, and a `sum`, `avg`, `min` or `max` measure, which this check does not judge; the row wildcard `'*'`; a member whose column does not resolve or declares no type; a cube whose `sql` names no object this stack defines. The runtime metadata write door: no authoring rule is dispatched for an `analytics_cube` save, and a `dataset` save's snapshot carries no cubes. diff --git a/.changeset/21091-inline-row-form-join-key.md b/.changeset/21091-inline-row-form-join-key.md deleted file mode 100644 index 2348612b2d4..00000000000 --- a/.changeset/21091-inline-row-form-join-key.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/lint': patch ---- - -`deriveInlineRowFormFields` and `isInlineRowFormOffered` (`@objectstack/spec/data`) state which fields an inline master-detail grid's per-row expand form draws and when that form is offered, and `field-no-consumers` stops calling four more kinds of in-use child field "inert" (#21091). - -Clause-②: yes (widening) - -- **`@objectstack/spec`.** Two new exports from `@objectstack/spec/data`, beside `deriveInlineGridColumns`: - - `deriveInlineRowFormFields(def, { relationshipField?, exclude? })` returns the child field names of the per-row expand form, in the child's field order. It skips the same system, audit, tenancy, ownership and sort-position names as the grid, the relationship field, `exclude`, `system` and `hidden` fields, and the computed types (`formula`, `summary`, `rollup`, `autonumber`, `auto_number`). Unlike the grid it keeps `readonly` fields and the rich types a cell cannot edit (`richtext`, `json`, `markdown`, …), so the derived grid's columns are always a subset of its fields. - - `isInlineRowFormOffered({ inlineMode?, formFields?, columns? })` is `true` when the form factor is `form`, or when the form has more fields than the grid has columns. - - Both are the renderer's current rule, reproduced exactly. No schema accepts anything new or refuses anything new. -- **`@objectstack/lint`.** `os validate` no longer warns that these fields are inert: - - a `lookup` field that sets `inlineEdit`: it is the inline grid's join key, read whatever columns the grid draws, as a `master_detail` field already was; - - a field a derived inline grid's per-row expand form draws, through `deriveInlineRowFormFields`, such as a `readonly`, `richtext` or `json` child field; - - a field named in an `object-master-detail-form` detail entry's `formFields`, now read against the entry's `childObject` instead of the block's object. When the form is never offered for the list, the list is reported as a carrier. That is judged on an entry that names both its `relationshipField` and its `columns` under its declared `inlineMode` or none. On any other entry it is judged under a declared `inlineMode` where the grid can be counted: authored `columns`, or the derived grid of a named `relationshipField`. Otherwise the list is credited as drawn; - - a field named in a `record:line_items` block's `columns`, `relationshipField`, `amountField`, `sort` or `filter`, now read against the block's `childObject`. - - A parent field that shares a name with one of those child fields was credited in the child's place, and is now reported if nothing else reads it. A child field nothing draws or names, such as a `hidden` one, is still reported. diff --git a/.changeset/21110-scheduled-work-host-reason.md b/.changeset/21110-scheduled-work-host-reason.md deleted file mode 100644 index d025bb59a89..00000000000 --- a/.changeset/21110-scheduled-work-host-reason.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@objectstack/types': minor -'@objectstack/service-automation': minor -'@objectstack/trigger-schedule': minor ---- - -feat(types,automation): a host's per-kernel scheduled-work OFF reports the host's own reason (#21110) - -Clause-②: yes (widening) - -`ScheduledWorkPolicy` (`@objectstack/types`) gains an optional -`hostDisabledReason`: the host's own sentence for why scheduled work is off on -this kernel, such as a plan that does not include scheduled flows. A new -export, `scheduledWorkDisabledReason(policy)`, gives the one answer for why -scheduled work is not armed under a policy. It returns the host's reason when -the policy carries one, and `SCHEDULED_WORK_DISABLED_REASON` otherwise. - -Every refusal site now reports that answer, read from the same policy reading -that refused: - -- the automation engine's bind log; -- the reason it records for `getTriggerBindingAudit()` and for the - `FlowRuntimeState.reason` that `GET /automation/_status` serves; -- the refusal of `ScheduleTrigger` and `TimeRelativeTrigger` when a host drives - them directly. - -Before this, a kernel that a host turned off through `scheduledWorkPolicy` -was reported with the deployment sentence. That sentence tells the reader to -set `OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true`, even on a process where the -variable is already set, and to a tenant who cannot set it. - -Nothing changes without the new field. A policy with no `hostDisabledReason`, -and the zero-argument deployment resolver `resolveScheduledWorkPolicy()`, which -never sets it, report `SCHEDULED_WORK_DISABLED_REASON` byte for byte. The field -is read only when `enabled` is `false`. - -To use it, a host that turns one kernel off for its own reason sets -`hostDisabledReason` on the `enabled: false` policy it already hands to that -kernel's `AutomationServicePlugin`, `ScheduleTriggerPlugin` and -`TimeRelativeTriggerPlugin`. Give the same policy to all three, as before, and -make the reason a whole sentence that names the cause and the remedy. It is -shown verbatim. diff --git a/.changeset/21135-liveness-readme-author-warnings.md b/.changeset/21135-liveness-readme-author-warnings.md deleted file mode 100644 index d429f5b89f6..00000000000 --- a/.changeset/21135-liveness-readme-author-warnings.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Liveness ledger README: the "Author warnings" section now describes the model the liveness lint ships. A `dead`, `live-elsewhere` or `experimental` verdict warns on its own, and `authorWarn` only opts a `planned` row in. - -Clause-②: no - -- The section said warnings were opt-in per ledger row, and that only `experimental` warned without the marker. That stopped being true when the lint made a `dead` or `live-elsewhere` verdict warn on its own. The section now has one table of which verdicts warn, and under which rule id. -- `authorHint` no longer "falls back to `note`". Every warning shows the row's `authorHint`, or else the verdict's default hint. The `note` never reaches an author. -- Rule 1 now talks about the verdict, not the marker. Grading a row `dead`, `live-elsewhere` or `experimental` warns every author who sets the key, and fails their `os lint --strict` / `os validate --strict` run. No marker keeps it quiet, so a benign display key is measured against the designer-previews ruling before it is graded `dead`. -- Rule 2 (booleans) now covers any key whose schema default materializes. It no longer points at an `_authorWarnSkipped` marker, which no ledger carries. -- The coverage paragraph states the walk's real reach: the types it visits, one level of `children`, and that a governed type it does not visit warns no author through this lint. -- Two sentences elsewhere in the README said a `dead` row needs `authorWarn` to warn. Both are corrected the same way. -- ⛔ Documentation only: no ledger row, schema, export or lint behaviour changes. diff --git a/.changeset/21197-internal-credentials-ledger.md b/.changeset/21197-internal-credentials-ledger.md deleted file mode 100644 index 9bae58e06c2..00000000000 --- a/.changeset/21197-internal-credentials-ledger.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -'@objectstack/plugin-audit': minor -'@objectstack/platform-objects': minor -'@objectstack/plugin-auth': minor -'@objectstack/plugin-sharing': minor -'@objectstack/plugin-approvals': minor -'@objectstack/objectql': minor -'@objectstack/runtime': patch ---- - -fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` - -Clause-②: no (narrowing) - - - -**BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** - -**What changed.** - -- The audit plugin's CRUD mirror now omits every field declared `internal: true` from the - rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an - update, delete `old_value`, and the activity row. It already masked `secret` and `password` - fields; `internal` is the same contract the generic data path already enforces ("never - returned on the generic data path"). An update that changes only an `internal` field still - writes its row, with neither value. -- These platform fields are now declared `internal: true`, so neither the generic data path - nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential - columns of the one-time verification object (`sys_verification`), the two-factor secret and - backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh - token columns, the OAuth client secret digest, the SCIM credential digest, the share link's - token and password hash, and the approval action-token digest. API key digests and email - headers were already `internal`; the ledger now honours that too. -- Every built-in consumer that needs one of these values reads it back through the engine's - privileged accessor rather than the generic path: JWT signing, password reset and the other - one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret - migration, OAuth client authentication, share-link redemption (the password gate is held) - and the creator's share-link list, which keeps returning each link's token. The runtime's - share-link resolve route (the dispatcher twin of the plugin's) still answers "password - required" for a protected link rather than the unknown-link shape. -- The one-time verification object's record title is now the fixed label `Verification`; it no - longer shows the identifier column. -- `@objectstack/objectql` exports two helpers from its main and `/core` entries: - `collectInternalReadFields` (the names of an object's `internal` fields) and - `readInternalColumn` (recovers one `internal` column for rows already read, through the - engine's privileged accessor, and fails closed when the value cannot be recovered). - -**What to do after upgrading.** - -- **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten - (the ledger is append-only), so a signing key that existed before the upgrade may have a copy - in the ledger. Rotate the keys so that copy signs nothing. -- **Revoke and re-mint share links that must stay private.** A share link's token is a - capability that stays valid until the link expires or is revoked, and links minted before this - release may have a copy in the ledger. -- A copy of a one-time verification credential is usable only while that credential is still - outstanding: once it is consumed or expires, its copy names nothing that will be accepted. -- An integration that read any of these columns through `GET /api/v1/data/...` no longer - receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO - providers through their auth routes. diff --git a/.changeset/21207-keyed-served-content-hash.md b/.changeset/21207-keyed-served-content-hash.md deleted file mode 100644 index ea544f92d63..00000000000 --- a/.changeset/21207-keyed-served-content-hash.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/objectql': minor -'@objectstack/mcp': minor -'@objectstack/plugin-audit': minor -'@objectstack/service-analytics': minor -'@objectstack/cli': minor ---- - -fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**Three things change for callers and operators.** - -1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". -2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. -3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. - -**What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. diff --git a/.changeset/21229-object-grid-export-options-closed.md b/.changeset/21229-object-grid-export-options-closed.md deleted file mode 100644 index 5892ffa7d96..00000000000 --- a/.changeset/21229-object-grid-export-options-closed.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-grid` page block's `exportOptions` is the list view's export options object, and a bare format array is refused (#21229) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`ComponentPropsMap['object-grid'].exportOptions`** was `z.unknown()`, so any value passed. The console's `ObjectGrid` reads `exportOptions.formats`, `.maxRecords`, `.includeHeaders`, `.fileNamePrefix` and `.streaming`, and lifts nothing: a bare format array — legal on a list view, which lifts it to `{ formats }` at parse — showed the export menu with its csv/json default and dropped the author's list without a report. The row now takes the list view's own five-member export options object, by identity and not the list view's union, so the legacy spelling does not spread to the grid: - - a bare array is refused with the object form named (`{ formats: ['csv', 'xlsx'] }`); - - a format outside `csv` / `xlsx` / `json` is refused at its index, and `pdf` keeps its retirement text; - - a key the object does not declare is named, with the rename a near-miss gets (`maxRecord` → `maxRecords`); - - `null` and other non-object values are refused. -- **`ObjectGridProps['exportOptions']`** (and `ObjectGridPropsParsed`) is the object type `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }` instead of `unknown`. -- The list view's `exportOptions` accepts and lifts exactly what it did. One message changed there, nested only: when a bare array also fails the array arm (a format outside the enum), the object arm's branch of the union now names the object form instead of zod's `expected object, received array`. - -## FROM → TO - -| you wrote on an `object-grid` | write instead | -|:--|:--| -| `exportOptions: ['csv', 'xlsx']` | `exportOptions: { formats: ['csv', 'xlsx'] }` — the grid now offers exactly those formats; write `{}` to keep the csv/json default it has been offering | -| `exportOptions: { formats: ['csv', 'pdf'] }` | `exportOptions: { formats: ['csv'] }` | -| `exportOptions: { formats: ['csv'], maxRecord: 100 }` | `exportOptions: { formats: ['csv'], maxRecords: 100 }` | -| `exportOptions: null` | omit `exportOptions` | - -The one-line fix: write `exportOptions` on an `object-grid` as the object `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, with `formats` drawn from `csv`, `xlsx` and `json`. - -## Who is affected, measured - -On `origin/main` `f148852752`: zero `object-grid` blocks authoring `exportOptions` in the examples, the package fixtures, the documentation and the published skills, against ten authored `object-grid` blocks through the same census (nine in TypeScript, one in a YAML documentation example) and four list-view `exportOptions` authorings as the key's control. No conversion is registered: nothing on the metadata load path refuses the shape, and a bare array has no rewrite that both keeps what the grid shows today and honours the author's list. Deployed metadata was not measured. diff --git a/.changeset/21236-core-json-column-refusal-field-class.md b/.changeset/21236-core-json-column-refusal-field-class.md deleted file mode 100644 index 0257f1b2c37..00000000000 --- a/.changeset/21236-core-json-column-refusal-field-class.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/core': patch ---- - -`jsonColumnOperatorRefusalText` takes an optional fourth argument: the class of JSON column the refused operator met, `JsonColumnFieldClass` (now exported). `'multi-value-or-json'` is the default, and its words are unchanged. `'single-value-media'` words the refusal for a single-value file-class field that a SQL deployment still stores as a JSON column. - -Clause-②: no - -A single-value file-class field (`file`, `image`, `avatar`, `video`, `audio`) is stored as a JSON column only on a deployment inside the ADR-0104 dual-encoding window, whose media columns have not moved. There it holds one JSON string, so `$contains` with the field's exact id answers no rows. That class's refusal no longer prescribes `$contains`. It says that the field answers these operators again once the deployment finishes the media-column move (the column step of `objectstack migrate files-to-references --apply`), and it still names `$null` / `$empty` for "no value". The message stays under the REST envelope's 500-character bound. Which operators are refused, and on which fields, does not change. diff --git a/.changeset/21236-driver-sql-single-value-media-refusal-remedy.md b/.changeset/21236-driver-sql-single-value-media-refusal-remedy.md deleted file mode 100644 index 6accac571ca..00000000000 --- a/.changeset/21236-driver-sql-single-value-media-refusal-remedy.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -On a deployment whose media columns have not moved, the JSON-column filter refusal on a single-value file-class field (`file`, `image`, `avatar`, `video`, `audio`) now names the repair that works there: the media-column move, not `$contains`. - -Clause-②: no - -The filter is still refused with `INVALID_FILTER` / 400, for the same operators as before (`$eq`, `$in`, `$startsWith`, `$icontains`, the orderings and the rest of that set, and the bare `{ field: value }` spelling). Before, the refusal told the caller to use `$contains`, the membership repair for a multi-valued field. On a single-value file-class field `$contains` with the field's exact id answers no rows. The refusal now says that the field answers these operators again once the deployment finishes the media-column move (the column step of `objectstack migrate files-to-references --apply`), and it still names `$null` / `$empty`, which answer there. A multi-valued field keeps the `$contains` words, byte for byte. Once the media columns have moved, these filters are not refused, as before. diff --git a/.changeset/21236-driver-turso-single-value-media-refusal-remedy.md b/.changeset/21236-driver-turso-single-value-media-refusal-remedy.md deleted file mode 100644 index b88963eac26..00000000000 --- a/.changeset/21236-driver-turso-single-value-media-refusal-remedy.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/driver-turso': minor ---- - -Both transports now word the JSON-column filter refusal on a single-value file-class field (`file`, `image`, `avatar`, `video`, `audio`) the way `@objectstack/driver-sql` does: the media-column move, not `$contains`, which answers no rows on that field. - -Clause-②: no - -The local transport inherits the new words from `SqlDriver`. The remote transport refuses in its own filter compiler, and now reads the same class from the driver, so one filter gets one message on both transports. The refused operators and fields do not change. Remote mode never moves its media columns, so a single-value file-class field is a JSON column there on every deployment; the remote transport refuses to plan the column step of `objectstack migrate files-to-references` (`NOT_IMPLEMENTED` / 501), as before, so on that transport the prescribed move is not yet available. - -`RemoteTransport.setJsonColumnResolver` now takes a resolver that answers the column's class (`JsonColumnFieldClass`, from `@objectstack/core`), or `undefined` for a column that is not JSON, in place of `true` / `false`. `TursoDriver` supplies it. A host that calls the method itself returns `'multi-value-or-json'` where it returned `true`, and `undefined` where it returned `false`. That replaces the setter's published parameter type, so a host resolver that returns a boolean no longer compiles: a host that injects its own resolver updates its signature, which is why this release is `minor`. diff --git a/.changeset/21237-member-identity-admin-fields.md b/.changeset/21237-member-identity-admin-fields.md deleted file mode 100644 index 1365801de2e..00000000000 --- a/.changeset/21237-member-identity-admin-fields.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-security': patch -'@objectstack/platform-objects': patch ---- - -fix(plugin-security,platform-objects): an org member reading a colleague's `sys_user` row is no longer served the identity object's `Admin` field group, directly or through the activity stream (#21237) - -Clause-②: no - -- **What a member was served.** The platform baseline `member_default` opens every org peer's `sys_user` row (the `sys_user_org_members` policy) and declared no field-level security on it. An org member reading a colleague's row was therefore served the whole `Admin` field group: the sign-in trail, the lockout state, the ban reason and expiry, the password and MFA stamps, the legacy platform role scalar and the AI-seat flag. With object-level read on `sys_activity`, the colleague's activity metadata carried the same fields, because the activity field redaction serves exactly what the data plane serves. -- **What changes.** `member_default` and `viewer_readonly` now declare the `Admin` group `readable: false` through the permission set's existing `fields` entries. The withheld set is built from the identity object's declaration, so a field the declaration adds to the group is withheld from the day it is declared. `admin_full_access` and `organization_admin` (and so `organization_admin_no_bypass`) declare the group readable and editable, the same state as a field no set names, so an administrator's reads and writes are unchanged. `member_default` is the additive baseline every authenticated user resolves, and field grants merge most-permissively, which is why the admin sets carry that keeping entry. -- **What a member sees now.** On the direct read, the list read and the activity metadata, a member is served no `Admin`-group field of a colleague's row. The directory fields (name, email, image) are still served. Field-level security does not distinguish rows, so the member's own row read through the generic data API is withheld the group too; every platform reader of those fields on a member's own row (the auth gates, the sign-in stamps, the session, the AI-seat resolution) reads under system or auth context and is unaffected. A member's query that filters or sorts on a withheld field is refused (`403 PERMISSION_DENIED`, the filter-oracle rule). A member's user-context write that names a withheld field is refused by the field-level write gate (`403 PERMISSION_DENIED`), and a payload mixing such a field with profile fields no longer lands partially. -- **The deactivation flag is directory data.** `sys_user.banned` moves from the `Admin` field group to the `Account` group in `@objectstack/platform-objects`, so members are still served it. Every user picker filters its candidates on it, and a filter on a withheld field would be refused. Its reason and expiry stay in the `Admin` group. In a record form the field now renders in the `Account` section. - -**Migration.** None for shipped apps. A custom permission set that grants an org member read on `sys_user` and is meant to show them the `Admin` group must name those fields `readable: true` in its `fields` entries. A client that filtered members' `sys_user` queries on an `Admin`-group field must drop that predicate or run it with an administrator's grant. diff --git a/.changeset/21242-formula-whole-day-copy-deleted.md b/.changeset/21242-formula-whole-day-copy-deleted.md deleted file mode 100644 index acff56e0d28..00000000000 --- a/.changeset/21242-formula-whole-day-copy-deleted.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/formula": minor ---- - -fix(formula)!: `matchesFilterCondition` compares a bare-day upper bound as written; its own whole-day copy is deleted (ADR-0053 D-D1 items 5 and 9) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the RLS write check admits on columns that are not `datetime`, and moves a few `engine.aggregate` answers that no seam lowers. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What is deleted.** `matchesFilterCondition` no longer reads a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, as "through that whole day", and no longer drops the bound on `9999-12-31`. It compares the value as written, as every other ordering operator here does, and as `driver-sql` compares it on the read. The whole day is applied once, at the seams that feed this evaluator, by the shared `lowerFilterCondition` (`@objectstack/spec/data`): the RLS compile seam lowers every policy filter on the object's declared `datetime` columns, the engine lowers `having` and `aggregations[i].filter` the same way, and the RLS write check judges a declared `date`, `datetime` or `time` column in its stored form. So a `check` on a `date` or `datetime` column answers exactly as before. - -**The RLS write check now agrees with the read on other columns.** Measured through `ObjectQL.insert` and `SecurityPlugin` on `SqlDriver` (better-sqlite3), as a member whose policy has the same `using` and `check`: - -- a `text` column under `record.title <= '2026-01-05'`, written as `'2026-01-05T15:00:00Z'` or `'2026-01-05 noon'`: the write was admitted while the read hid the stored row. It is now refused `PERMISSION_DENIED` / 403, and the read still hides it; -- two `text` columns, `record.title <= record.code`, with `code` holding `'2026-01-05'`: the same, admitted before and 403 now, with the read hiding the row; -- a `number` column under `record.amount <= '9999-12-31'`: the write was admitted because an epoch number read as an instant on the last supported day. A number is not less than a day string, so it is now 403. The engine refuses the same comparison in a `where` (`INVALID_FILTER` / 400: a day string is not a number). - -The access explanation (`explain`) judges a stored row with this evaluator, so its row verdict moves the same way: for the two `text` cells it now says hidden, as the read does. - -**`engine.aggregate` answers that no seam lowers.** A `{ $field }` referent is per row, so no seam can lower it. These positions are now compared as written: - -- two declared `text` columns of one class, at a per-aggregation `filter` or between two `having` group columns: `'2026-01-05 noon'` against `'2026-01-05'` is no longer counted or kept, which is what the same comparison answers in a `where`; -- the pairs the class rule cannot judge because a side has no declaration: an object the registry does not declare, and an audit-opt-out object's row-carried `created_at` / `updated_at` against a `date`. An instant on the due day is no longer counted against that bare day; -- a direct `applyInMemoryAggregation` call, which applies no class rule. - -**The remedy.** Compare a `datetime` with a `datetime` and a `date` with a `date`. A `datetime` against a calendar day has no single answer across SQL and memory, and a declared pair of the two is already refused. A number compared with a day string has no answer at all: compare a number with a number. A caller that evaluates a filter on a `datetime` column without passing a seam lowers it first with `lowerFilterCondition(filter, { isDatetimeColumn })` to get the whole-day reading. - -**Unchanged.** A `check` on a declared `date`, `datetime` or `time` column, a `{ $field }` pair of two `date` or two `datetime` columns (with or without `addDays`), a full-ISO bound, `$gte` / `$gt` / `$lt` and `$eq`. diff --git a/.changeset/21242-plugin-security-rls-number-comparand.md b/.changeset/21242-plugin-security-rls-number-comparand.md deleted file mode 100644 index 17a1a959a08..00000000000 --- a/.changeset/21242-plugin-security-rls-number-comparand.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@objectstack/plugin-security": minor ---- - -fix(plugin-security)!: a row-level policy that compares a numeric column with a comparand that is not a number is refused at the RLS compile seam, read and write alike, as the engine's `where` refuses the same comparison - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows which row-level policies the RLS compile seam hands to its two consumers, the read and the write check. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. One published type gains a member: `RlsFieldGuard`, the type of the optional `fieldGuard` argument of the root-exported `RLSCompiler.compileFilter`, gains the optional `number` member (each declared column's `type`, and a formula's `returnType`). It is an additive optional member, not a change of what is accepted. - -**What was accepted before.** A policy such as `record.amount <= '9999-12-31'` on a `number` column compiled, and its `using` and `check` both reached their consumers unjudged. Measured through `ObjectQL.insert` and `SecurityPlugin` on `SqlDriver` (better-sqlite3), as a member: the write of `amount: 5` was admitted (`@objectstack/formula`'s deleted whole-day copy read the number as an instant), and the read showed the stored row, because SQLite orders an integer before any text. That read was measured on SQLite only; PostgreSQL was not run for this change. The same comparison in a caller's `where` is refused `INVALID_FILTER` / 400 by the engine's number-comparand door. - -**What is refused now.** The seam runs the spec's number-comparand verdict (`numberComparandDoorVerdict`, `@objectstack/spec/data`), the one the engine's `where` door consults, on every compiled policy filter, after the shape door and before the comparand-type door. On a column the object declares numeric, a comparand that is not a number (a string the platform's numeric grammar does not read, such as `'9999-12-31'` or `'abc'`, a boolean, a `Date` or a list) drops the policy through the existing fail-closed route: the read is filtered by the deny sentinel and returns no rows, the write is refused `PERMISSION_DENIED` / 403, and a WARN line names the policy, the clause and the comparand. The line's detail is written for the clause it refused: for `check`, which the write check evaluates in-process, it names no driver bind. A granting sibling policy still grants. - -**Narrowed, as in `where`.** A numeric string (`'10'`, `'1e3'`) is replaced by the number it names before either consumer runs. So `record.amount == '10'` now matches a stored `10` on the write check, which compared the text with the number and refused it, while the read showed the row. - -**The remedy.** Compare a numeric column with a number: `record.amount <= 9999`, not `record.amount <= '9999-12-31'`. - -**Unchanged.** A numeric literal, a column that is not numeric, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one). diff --git a/.changeset/21243-sys-packages-portable.md b/.changeset/21243-sys-packages-portable.md deleted file mode 100644 index 592edd85ccd..00000000000 --- a/.changeset/21243-sys-packages-portable.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/service-package': patch -'@objectstack/metadata-protocol': patch ---- - -fix: on MySQL, `sys_packages` is now created and written, so installed and edited packages survive a restart. When a `sys_packages` write fails, a package install or edit now answers the failure instead of success (#21243) - -Clause-②: no - -**`@objectstack/service-package`.** The `sys_packages` DDL and the publish upsert are spelled for the dialect the default driver names (`SqlDriver.dialectName`). SQLite and PostgreSQL keep the exact statements they always ran, and so does any driver that names no SQL dialect. MySQL gets the same `(id, version)` key and columns in its own spelling. Its index is created only after `information_schema` reports it absent, and its upsert is `INSERT … AS incoming ON DUPLICATE KEY UPDATE`, which needs MySQL 8.0.19 or later. Before this, the table was never created on MySQL. That DDL failed with `ER_INVALID_DEFAULT`, `ER_BLOB_KEY_WITHOUT_LENGTH` and `ER_PARSE_ERROR`. The DDL refusal was logged only at `debug`, as "may already exist". The `ON CONFLICT` upsert also failed with `ER_PARSE_ERROR`, so `POST /api/v1/packages/publish` answered `500 DATABASE_ERROR`. A refused DDL statement now fails the plugin's `start()` and is logged at `error`. - -**`@objectstack/metadata-protocol`.** `installPackage` and `updatePackage` no longer answer success when the `package` service's `sys_packages` write fails. The registry write is undone first. A fresh install leaves no package and releases the namespace it registered. A re-install puts the prior row back, and an edit puts the prior manifest back. Then the failure is thrown. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, `POST /api/v1/packages` answered `201` and `PATCH /api/v1/packages/:id` answered `200` over a write that never landed, and the package was gone after the next restart. A host with no `package` service still installs in memory only and says so with a warning. That degraded path is unchanged. diff --git a/.changeset/21248-strict-nav-label-describe.md b/.changeset/21248-strict-nav-label-describe.md deleted file mode 100644 index 8e3e384cd6a..00000000000 --- a/.changeset/21248-strict-nav-label-describe.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the strict blueprint nav item's `label` describe says `null` inherits the target's current label - -Clause-②: no - -`SolutionBlueprintStrictSchema` is the output contract the AI design step generates against, and -strict mode makes every nav entry's `label` a required decision. Its describe read only "Nav entry -label, or null", so nothing the model reads said which of the two choices follows a rename of the -target, and the model was steered toward writing one. The describe now states the lenient -`BlueprintNavItemSchema.label` rule in the strict spelling: `null` ⇒ the entry inherits the CURRENT -label of what it opens at render time (a renamed target shows its new name); a string ⇒ rendered -verbatim, never a copy of the target's label. Write a label only when the entry must read -differently from what it opens. - -Describe text only: the key stays `z.string().nullable()`, so the schema accepts and refuses the -same blueprints. A pin holds the lenient and strict `label` describes to one rule. diff --git a/.changeset/21254-rls-write-check-json-column-operator-refusal.md b/.changeset/21254-rls-write-check-json-column-operator-refusal.md deleted file mode 100644 index c8bb61e39d5..00000000000 --- a/.changeset/21254-rls-write-check-json-column-operator-refusal.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): a row-level `check` refuses an operator the read refuses on a field declared JSON-stored, with the read's `INVALID_FILTER` / 400, so a policy whose read is refused no longer admits writes (#21254) - -Clause-②: no - -The read a row-level policy scopes refuses a scalar comparison, an ordering or a text operator (`@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, and implicit equality) on a field the object declares JSON-stored: a structured-JSON type (`json`, `address`, …), or a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `lookup` / `user` / `file` / `image` flagged `multiple: true`). The write `check` evaluated the same operators against the stored list instead. Measured through `ObjectQL.insert` with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set, with the same predicate as `using` and `check`: - -| `check` | written | write, before | read | -|---|---|---|---| -| `record.tags != 'x'` | `['x']` or `'x'` | admitted, stored `["x"]` | 400 | -| `!(record.tags in ['x'])` | `['x']` | admitted, stored `["x"]` | 400 | -| `record.tags == 'x'` / `record.tags in ['x']` | `['x']` | 403 | 400 | -| `record.tags > 'a'` | `['x']` | 400 | 400 | -| `record.meta != 'x'` / `record.meta == 'x'` (`meta` is `json`) | a scalar | admitted, stored | 400 | - -Now the write check refuses every one of these with the read's answer: `INVALID_FILTER` / 400 and the read's words, which withhold the field and the operator. The refusal reads the object's declaration, never the record, so a policy is refused for every row or for none, on the insert, a by-id update and a predicate update. The diagnostic, which names the field, the operator and the policy, goes to the server log. Rows that already refused still store nothing; their answer is now the read's. - -Unchanged: `contains` and its negation (`$contains` / `$notContains`), and the presence checks (`== null`, `!= null`), answer on such a field as before; a field declared neither way keeps every operator; an object whose schema cannot be loaded is judged as before. To repair a refused policy, test membership with `contains` (for example `!record.tags.contains('x')`). diff --git a/.changeset/21255-having-plain-reference-class.md b/.changeset/21255-having-plain-reference-class.md deleted file mode 100644 index 0afb4b2533a..00000000000 --- a/.changeset/21255-having-plain-reference-class.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: `having` and the per-aggregation `filter` refuse a plain `{ $field }` reference between two columns of different comparison classes with `INVALID_FILTER` / 400, as `where` refuses it - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a `{ $field }` reference may pair at two positions of `engine.aggregate`. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What was accepted before.** At `having` and at a per-aggregation `filter` (`aggregations[i].filter`), the comparison-class rule was applied only to a reference carrying `addDays`. A plain reference across two classes was answered: `{ closed_at: { $lte: { $field: 'due_on' } } }`, with `closed_at` a `datetime` and `due_on` a `date`, counted rows by `@objectstack/formula`'s whole-day reading of the bare day, and a `having` of `max(closed_at)` against a `day` date bucket kept groups the same way. The same comparison in a `where` is refused `INVALID_FILTER` / 400 by `driver-sql`. - -**What is refused now.** A scalar comparison (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) whose comparand is a plain `{ $field }` naming a column of a different comparison class. The classes are the spec's `CROSS_FIELD_COMPARISON_CLASSES` (`numeric`, `text`, `boolean`, `date`, `datetime`, `time`), judged by the spec's `crossFieldComparisonVerdict`, the classification `driver-sql`'s `where` compiler reads. The refusal is `INVALID_FILTER` / 400, raised before any driver is asked for a row, on an empty set as on a populated one, through `engine.aggregate` and `POST /api/v1/data/:object/query`: - -- in a per-aggregation `filter`, the fields, the operator and the reason are withheld from the message and written to the server log, as `where` withholds them; the message now names the same-class rule beside the `addDays` one; -- in `having`, the message names the two columns of the query's own projection and their classes, in the sentence `where` logs for the same pair. A `having` column's class is read off the query: a `day` date bucket is a `date`, a coarser bucket a `text` label, `count` / `count_distinct` / `sum` / `avg` are `numeric`, and `min` / `max` take the type of the field they read. - -**The remedy.** Compare same-class columns: a `datetime` with a `datetime`, a `date` with a `date` (a `day` bucket is one), a number with a number. A comparison between a `datetime` and a calendar day has no single answer across SQL and memory, so the platform does not define one. - -**Unchanged.** A reference between two columns of one class answers as before. A `{ $field, addDays }` pair keeps its judgement and its words. A column whose class the declaration cannot tell is not judged, as an `addDays` pair is not: a host with no registered object, a column the field map does not list (`id`), an aggregation over an undeclared field. A column the spec gives no comparison class at all (a structured-JSON, multi-valued or file field, a formula) is not judged by this rule either. diff --git a/.changeset/21257-widget-sub-caption-retired.md b/.changeset/21257-widget-sub-caption-retired.md deleted file mode 100644 index e332eebfe46..00000000000 --- a/.changeset/21257-widget-sub-caption-retired.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/sdui-parser': minor -'@objectstack/lint': minor ---- - -The metric sub-caption is retired at both ends. A dashboard widget keeps one authored description, `widget.description`, which renders as the card-header subtitle and is translated by the widget's `description` translation key. The widget translation key `subCaption` is refused, and the server no longer writes a widget's `options.description`. - -Clause-②: no (narrowing) - - - -**What is retired.** `dashboards.DASHBOARD.widgets.WIDGET.subCaption` in a translation bundle (`defineTranslationBundle`, `stack.translations`, the platform bundle) and in a registered `translation` item. It overlaid a caption under a metric's value onto the widget's `options.description`. The dashboard schema never declared `options.description`, and no authored widget wrote it, so `translateDashboard`'s overlay was the key's only writer. That overlay is removed: `translateDashboard` now translates a widget's `title` and `description` and carries `options` through untouched. - -**BREAKING** — an accept-set narrowing, shipped as `minor` under the launch-window convention. - -### FROM → TO - -| wrote | write instead | -| --- | --- | -| `dashboards.DASHBOARD.widgets.WIDGET.subCaption: 'TEXT'` | delete the entry. If the copy belongs on the card, put it in the widget's `description` and translate it under `dashboards.DASHBOARD.widgets.WIDGET.description`. | -| `dashboards.DASHBOARD.widgets.WIDGET.subtitle: 'TEXT'` | `subtitle` was only ever a rename suggestion for `subCaption`. Card-header copy goes under `description`; a caption under the value has nowhere to render, so delete it. | - -**The one-line fix: delete every `subCaption:` entry under `dashboards.*.widgets.*` in your translation bundles.** `os migrate meta --from 17` lists the mechanical edits for existing sources; stored `translation` items are converted when they are read. - -**What an author now sees.** Writing `subCaption` fails `tsc` (its input type is the retired-key mark) and fails the parse with a prescription naming the widget's `description`. Writing `subtitle` on a widget translation fails the parse with both readings named, instead of a rename suggestion onto a key that is refused next. `os validate`, `os build` and `os lint` now raise the `unconsumed-widget-option` warning on an authored widget `options.description`, like any other options key the dataset-bound render path does not read. It is a warning, so none of the three fails on it. - -**Measured producers: none.** Zero `subCaption` entries and zero authored widget `options.description` in the four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and in the bundles `@objectstack/platform-objects` ships, so no shipped exit code changes. - -### The retirement kit - -- **Tombstone.** `subCaption` is a `retiredKey()` tombstone on the widget translation node, so the refusal carries the prescription on all three faces the node is spread into (per-app bundle entry, platform bundle entry, `translation` item). The node sits under two records (`dashboards`, `widgets`), below the authorable-surface walk, so it has no `RETIRED_KEYS_BY_MAJOR` row, the same as the `submitLabel` component-copy key before it. -- **The former alias.** The `subtitle` → `subCaption` rename suggestion moves to the node's `guidance` table. An alias whose target is a tombstone is the shape the alias-integrity audit refuses, and repointing it at `description` would silently change what the word is taken to mean. -- **Conversion.** `translation-widget-sub-caption-removed` (protocol 18) strips the key from bundle entries and bare translation items as a lossless delete. It is retired from the load path, so authors are refused at parse while stored rows and `os migrate meta` replay it. Its D3 record is the semantic entry `translation-widget-sub-caption-retired`. -- **`@objectstack/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `description`, its one undeclared member, which existed only because the overlay wrote it. `check:widget-option-census`'s `NON_DECLARED_MEMBERS` ledger is now empty, so the census asserts that nothing writes an undeclared key into `options`. diff --git a/.changeset/21260-ledger-audit-capability.md b/.changeset/21260-ledger-audit-capability.md deleted file mode 100644 index d6662f117ef..00000000000 --- a/.changeset/21260-ledger-audit-capability.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-audit': minor ---- - -feat(spec,plugin-audit): the compliance ledger's audit capability, `view_all_audit_log`, exempts its holder from the ledger's parent-record read gate; platform administrators hold it by default (#21260) - -Clause-②: yes (widening) - -- **The capability.** `PLATFORM_CAPABILITIES` (`@objectstack/spec/security`) gains `view_all_audit_log` ("View All Audit Log", `scope: 'org'`). It is seeded into `sys_capability` like every other curated capability, and a permission set grants it through `systemPermissions`. It is a platform capability, so an app that declares a capability of the same name cannot bind a set carrying it to the `everyone` or `guest` anchor. -- **Who holds it.** `ADMIN_FULL_ACCESS_CAPABILITIES` (`@objectstack/spec`) now lists it, so platform administrators hold it by default: through the `admin_full_access` grant, and through the envelope a configured platform owner resolves to. No other shipped permission set carries it. Any other position holds it only through a permission set that grants it. -- **What it does.** A read of `sys_audit_log` keeps only the rows whose parent record the caller can read. The holder skips that gate and is served every ledger row its grant on `sys_audit_log` reaches: rows about deleted records, sign-out rows, sign-in rows whose session has ended, and rows about records it cannot open. A broad read is served whole. The gate's 2,000-row pre-scan does not run for a holder, so the read is not cut off at that bound. -- **What still applies to the holder.** The holder still needs object-level read on `sys_audit_log`. The field-level redaction still narrows every before/after snapshot it is served. Under a walled tenancy posture, the tenant wall still keeps the holder to its own organization's rows, which is why the capability is declared `org`. -- **What it does not touch.** The activity stream (`sys_activity`) keeps its own parent-record gate for every caller, holders included. A non-holder's ledger reads are unchanged. - -**Migration.** None: no metadata, code or configuration change is needed. Platform administrators get the deletion and sign-out trail back with no action. To give an auditor the trail, grant `view_all_audit_log` through `systemPermissions` in a permission set that also grants read on `sys_audit_log`. diff --git a/.changeset/21261-bound-action-global-fallback.md b/.changeset/21261-bound-action-global-fallback.md deleted file mode 100644 index 66af4993805..00000000000 --- a/.changeset/21261-bound-action-global-fallback.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): a bound action's translation is read only under its own object, never from `globalActions` - -Clause-②: no - -The i18n resolver reads an action's translated copy at one address, chosen by the action's own `objectName`. This covers `translateAction`, `resolveActionLabel`, `resolveActionConfirm`, `resolveActionSuccess`, `resolveActionResultDialog`, and `translateObject` for an object's inline actions. - -- An action with an `objectName` reads only `objects.OBJECT._actions.ACTION`. -- An action with no `objectName` reads only `globalActions.ACTION`. - -Before this, a bound action with no object-scoped copy fell back to `globalActions.ACTION`. The fallback covered its label, description, confirm text, success message, outcome messages, params and result dialog. `TranslationDataSchema.globalActions` declares that group for object-less actions only. `os validate` already refuses, at error level, a `globalActions` key that names a bound action, and says the key is never read. The resolver now matches both. - -**What changes for a project.** A bundle that passes `os validate` is not affected. A bundle that keeps a bound action's copy under `globalActions` now shows that action's source text instead of the translation. `os validate` does not check a translation stored at runtime, so such a translation changes the same way. The fix is to move the keys from `globalActions.ACTION` to `objects.OBJECT._actions.ACTION`, where OBJECT is the action's `objectName`. The example apps under `examples/` and the translation bundles shipped in this repository's packages have no such key. diff --git a/.changeset/21262-audit-failure-line.md b/.changeset/21262-audit-failure-line.md deleted file mode 100644 index d6d258b8501..00000000000 --- a/.changeset/21262-audit-failure-line.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -The `Audit write FAILED` line names the table whose insert was refused and the row that is lost, gives a missing table the two causes the evidence cannot tell apart, and says it is printed once per audited object, refused table and error code - -Clause-②: no - -The record writer stores the `sys_audit_log` row that records who did it, then, when activities are enabled and the write has one, its `sys_activity` timeline row. When either insert was refused, the line always said the `sys_audit_log` row never landed. When the refused insert was `sys_activity`, every ledger row had in fact landed. - -- The line now opens `Audit write FAILED on TABLE` and names the table the writer had in flight when it threw. A refused `sys_activity` insert says the ledger row landed and only the activity row is lost. A refused `sys_audit_log` insert says the ledger row is lost, and so is the activity row due after it when the object writes one. -- A missing table no longer gets only the telemetry-datasource split as its remedy. The table may never have been created because schema sync's DDL for it was refused at boot. The line cannot tell the two causes apart, so it names both, in order: look for `Schema sync FAILED for object 'TABLE'` in the boot log first, then the split and `OS_TELEMETRY_DB=0`. Any other cause keeps the driver-fault remedy. -- Whether the table is missing is asked about the refused table first. An error code that means "missing" without a phrase naming a relation is now attributed to that table, not to `sys_audit_log` by list order. -- The line is printed once per audited object, refused table and error code, and it now says so in place of "reported ONCE". The refused table joins the key, so the other table refusing with the same code on the same object gets its own line. The same missing table still prints one line per audited object that writes through it. Repeats stay at `debug`, which now also carries the `table`. - -Log text and log metadata only: no status, error code, route, row or control flow changes. A log filter that matches the old text (`Audit write FAILED (`, `reported ONCE`) needs the new spelling. diff --git a/.changeset/21263-crypto-provider-keyed-digest.md b/.changeset/21263-crypto-provider-keyed-digest.md deleted file mode 100644 index 05d2d3464f3..00000000000 --- a/.changeset/21263-crypto-provider-keyed-digest.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-settings': minor ---- - -feat(spec): `ICryptoProvider` gains a required `keyedDigest(plain): Promise` member, and `LocalCryptoProvider` implements it (#21263) - -Clause-②: yes - -**BREAKING** for `ICryptoProvider` implementers: the new member is required, so a -provider that does not declare it stops compiling (`TS2420` on a class, `TS2741` -on an object literal), and the compiler names the missing member. Code that only -calls a provider is unaffected. - -`keyedDigest` is a digest of `plain` under the provider's server-held key, for a -value that is handed to a caller but must not let that caller check a guess about -the input offline. The contract requires three things of every implementation: - -- **Keyed.** The output cannot be computed without the provider's key. A provider - that holds no key material rejects; it never returns an unkeyed value. -- **Stable per key.** Under one key, equal input gives equal output in every - process and on every node that holds the key. Replacing the key changes every - output. -- **Not a substitute for `digest`.** `digest` keeps its contract and the stability - the audit trail relies on. - -The output is `hmac-sha256:` followed by the 64 lowercase hex characters of an -HMAC-SHA-256: 76 characters from `[0-9a-z:-]`, which travel unchanged in an HTTP -header, a query string and JSON, and never collide with the `sha256:` spelling of -an unkeyed content hash. - -`LocalCryptoProvider` computes it from the 32-byte data key it already resolves -(`OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY`, the persisted key file, or the ephemeral -test-mode key), through a MAC key derived from that data key, so the AES-GCM key -is never used as a MAC key. There is no new secret or environment variable to -configure. An instance constructed with an explicit key that is not 32 bytes holds -no usable key material, and its `keyedDigest` rejects with -`KeyedDigestKeyUnavailableError`. - - diff --git a/.changeset/21264-result-dialog-leaves.md b/.changeset/21264-result-dialog-leaves.md deleted file mode 100644 index fbdf7194748..00000000000 --- a/.changeset/21264-result-dialog-leaves.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -`os validate`, `os build` and `os lint` now check an action translation's result-dialog copy against whether the action declares a `resultDialog`, under both `objects.OBJECT._actions.ACTION` and `globalActions.ACTION`. - -Clause-②: no (narrowing) - - - -**What is refused.** `resultDialog.title`, `resultDialog.description` and `resultDialog.acknowledge` under an action that declares no `resultDialog` are now `translation-target-unknown` errors, one per key: the code and level an undeclared `params`, `outcomeMessages` or `resultDialog.fields` key already gets. `translateAction` returns no dialog for such an action, so the copy is never read. Before this, only `resultDialog.fields.PATH` was checked under the dialog, and these three keys passed. - -**What still passes.** The same three keys under an action that declares a `resultDialog` are read and pass, whether or not the dialog sets that text itself. - -**BREAKING** — an accept-set narrowing at the `os validate`, `os build` and `os lint` doors, shipped as `minor` under the launch-window convention. **What changes for a project.** A bundle that carries one of these keys under an action with no `resultDialog` now fails `os validate` with exit 1 instead of passing, and `os build` refuses it. The fix is to move the keys under the action that declares the dialog, declare the `resultDialog` on the action if it should show one, or delete the keys. The four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and the bundle shipped with `@objectstack/platform-objects` produce no finding on these keys, so none of their exit codes change. diff --git a/.changeset/21267-analytics-order-key-selected.md b/.changeset/21267-analytics-order-key-selected.md deleted file mode 100644 index 4d9953e23a5..00000000000 --- a/.changeset/21267-analytics-order-key-selected.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: an analytics `order` key that names no member the query selects is refused with `INVALID_FIELD` / 400 at the analytics door, on both strategies, before either runs - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**The rule.** Each `order` key must be a column the answer carries: one of the query's own `dimensions` entries, one of its `measures` entries, or a `timeDimensions` entry that carries a `granularity`, spelled exactly as it is selected (a `.`-qualified measure keeps its qualifier in the answer, so the bare spelling names no column beside it, and the other way round). A `timeDimensions` entry with only a `dateRange` bounds the rows and is not a column. Any other key is refused with `400 INVALID_FIELD`, naming every such key and the members the query does select, and nothing is executed. The thrown error carries `param: 'order'` and `field` (the first such key). - -**Before**, measured through `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14, for a cube that declares no join over an object whose lookup target also declares `note`: - -- `dimensions: ['owner.email']` with `order: { note: 'asc' }`: native-SQL strategy `500` on both drivers (PostgreSQL 42702, `note` is ambiguous); ObjectQL strategy `200`. -- `dimensions: ['note']` with `order: { amount: 'asc' }`: native-SQL strategy `200` on SQLite, ordered by an arbitrary row's `amount`, and `500` on PostgreSQL (42803, must appear in GROUP BY); ObjectQL strategy `200`. -- `dimensions: ['note']` with `order: { 'owner.email': 'asc' }`: native-SQL strategy `500` on both drivers (PostgreSQL 42703, no such column); ObjectQL strategy `200`. - -**Now** each of those answers `400 INVALID_FIELD` on both strategies and both drivers, and `POST /api/v1/analytics/sql` refuses them the same way instead of returning a statement whose `ORDER BY` cannot run. - -**What to write instead.** Add the key to the query's `dimensions` (or `measures`), so the answer carries it, or drop it from `order`. - -**Who is affected.** A caller that posted an `order` key it did not select. On the native-SQL strategy those queries were already a 500 everywhere but the one SQLite shape, whose order was arbitrary. No example app, shipped dashboard, report, dataset or cube authors such a key, and the console's analytics adapter sends no `order` to this route. - -**Unchanged.** Ordering by a selected dimension, a selected measure or a bucketed time dimension; the dataset door (`POST /api/v1/analytics/dataset/query`), which already refused an unselected `selection.order` key with `400 DATASET_INVALID` and pushes an `order` down only when the selection selects every key; and a key naming a field the caller may not read, which keeps the `403 PERMISSION_DENIED` the field-level read gate answers for every position. diff --git a/.changeset/21274-driver-fault-boundary-redaction.md b/.changeset/21274-driver-fault-boundary-redaction.md deleted file mode 100644 index d7bb8389b62..00000000000 --- a/.changeset/21274-driver-fault-boundary-redaction.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a driver error that leaves the engine no longer carries the failing statement or the caller's values - -Clause-②: no - -The engine has cut the bound statement out of its own log line for a failed driver call for a long time, but it rethrew the driver's raw error. Any in-process code that logged what it caught, such as an auth library's error logger, printed the statement and the row's values. The same cut now runs where the error leaves the engine, so no consumer needs a patch of its own. - -- **Where.** Every engine operation that reaches a driver: `find`, `findOne`, `count`, `aggregate`, `insert` (batch included), `update` and `delete` (by id and by predicate), `execute`, `transaction`, `resolveSecretField` and `resolveInternalField`. -- **What is cut.** The statement and the caller's values, from the error's `message` and `stack`, from the properties drivers attach (mysql2's `sql` and `sqlMessage`; node-postgres' `detail`, `where` and `internalQuery`), and down the `cause` chain. A `DuplicateRecordError` keeps its own fields and carries a cut `cause`. -- **What stays.** The error's class (`instanceof` still holds), `name`, `code`, `errno`, `sqlState`, Postgres' identifier fields (`constraint`, `table`, `column`, …) and the database's own diagnostic. The message now reads as the statement's kind, a `[statement and bound values redacted]` marker and the diagnostic. A Postgres key-shaped `detail` keeps its column list. Every REST answer keeps its status, code and `field`. -- **What changes for a caller.** Code that read the statement or a value out of a driver error's message or properties now gets the marker instead. Branch on the class, `code` or `errno` instead. The driver error on a `DuplicateRecordError`'s `cause` is an equivalent copy, no longer the object the driver threw. An import's row report for a value-bearing database error no longer repeats the rejected value. diff --git a/.changeset/21276-package-delete-store-refusal.md b/.changeset/21276-package-delete-store-refusal.md deleted file mode 100644 index 058850d9076..00000000000 --- a/.changeset/21276-package-delete-store-refusal.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch -'@objectstack/runtime': patch -'@objectstack/objectql': patch ---- - -fix: when the store refuses an uninstall's `sys_packages` delete, the uninstall now answers the failure and removes nothing else, instead of answering success and coming back after the next restart (#21276) - -Clause-②: no - -**`@objectstack/metadata-protocol`.** `deletePackage` now deletes the package's `sys_packages` row first, before its `sys_metadata` rows, its tables, its registry entry and the rows the uninstall cleanups own. When the `package` service refuses that delete, whether it returns `{ success: false }` or throws, `deletePackage` throws and nothing else is removed. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, the refusal was logged as a warning, and `DELETE /api/v1/packages/:id` answered `200` after the package's metadata, tables and grants had been removed. The package then came back after the next restart. - -Before that store delete, `deletePackage` now also asks the registry whether the uninstall would be refused because another package extends an object this package owns (ADR-0029). If so, it throws the registry's own refusal with nothing removed. A registry without the new question is not asked, and the refusal then surfaces at the registry withdrawal, as before. - -**`@objectstack/objectql`.** New: `SchemaRegistry.assertPackageUninstallable(packageId)`. It throws the refusal `unregisterObjectsByPackage` and `uninstallPackage` raise for an object another package extends, with the same message, and it changes nothing. `unregisterObjectsByPackage` now calls it, so there is still one copy of that check. - -**`@objectstack/runtime`.** `DELETE /api/v1/packages/:id` now asks `deletePackage` before it touches anything. It checks that the package exists with a read, and it withdraws the package from the running registry and clears its saved disable record only after `deletePackage` has answered. So when the store refuses, the door answers `500`, the same process keeps serving the package, and a package that was disabled stays disabled after a restart. Before this, the door withdrew the package and cleared its disable record first. A refused delete then left the package missing until a restart, and brought a disabled package back enabled. - -An uninstall refused because another package extends an object this package owns still answers `500` with nothing changed: the stored rows, the registry entry and the disable record all stay as they were, in the same process and after a restart. That refusal is now decided before the store delete, instead of by the door withdrawing the package first. An ordinary uninstall, and a host with no `package` service, are unchanged. diff --git a/.changeset/21277-agent-structured-output-json-only.md b/.changeset/21277-agent-structured-output-json-only.md deleted file mode 100644 index 1a946812ad1..00000000000 --- a/.changeset/21277-agent-structured-output-json-only.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an agent's `structuredOutput` is JSON-only — the `regex` / `grammar` / `xml` formats and the `coerce_types` step are retired, and the block is `live`, enforced by the cloud AI runtime (#21277) - -**BREAKING** — four members leave the agent's structured-output vocabulary: -`regex`, `grammar` and `xml` from `StructuredOutputFormat` (so from -`agent.structuredOutput.format` and `agent.structuredOutput.fallbackFormat`), and -`coerce_types` from `TransformPipelineStep` (so from -`agent.structuredOutput.transformPipeline`). ADR-0049 enforce-or-remove, ruled -retire. The cloud AI runtime, the one runtime that executes agents, enforces -`structuredOutput` on every final answer and refused an agent declaring any of the -four before its first turn: the spec never had a key to carry the pattern or -grammar a `regex` / `grammar` answer would be checked against, an answer is checked -only as JSON, and no coercion engine exists. So no authored value of the four ever -did what it named, and authoring now refuses them by name instead of the first -live turn refusing the agent. `json_object`, `json_schema`, `trim`, `parse_json` -and `validate` are unchanged. - -### FROM → TO - -| removed | what to write instead | -| --- | --- | -| `structuredOutput.format: 'regex'`, `'grammar'` or `'xml'` | `format: 'json_schema'` with a JSON Schema in `schema` when the answer must have a shape, or `format: 'json_object'`; or delete the `structuredOutput` block if the agent needs no output contract. | -| `structuredOutput.fallbackFormat: 'regex'`, `'grammar'` or `'xml'` | `'json_object'` or `'json_schema'`, or delete the key. | -| `'coerce_types'` in `structuredOutput.transformPipeline` | delete the step, and declare the exact types in `schema` so the answer is validated as the model wrote it. | - -**The one-line fix: use `json_schema` with a JSON Schema; drop `coerce_types`.** -`os migrate meta --from 17` lists the mechanical edits for existing sources. - -Each retired member is refused at parse with a prescription naming the JSON -formats, and in `tsc` (the members are gone from the `StructuredOutputFormat` / -`TransformPipelineStep` types). Any other unknown value keeps zod's own message. - -### The retirement kit - -- **Value-level retirement.** Both enums are declared through - `enumWithRetiredValues` (`shared/retired-key.ts`), the house mechanism for a - narrowed vocabulary, with the prescriptions module-private. No authorable KEY and - no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface - ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`, - `api-surface-signatures`) are byte-identical. -- **D2 conversion `agent-structured-output-refused-members-removed`** (step 18, - retired from the load path): it deletes a `structuredOutput` block whose `format` - was retired (the format is required, and no rewrite can say which JSON contract - was meant), deletes a retired `fallbackFormat`, and drops `coerce_types` from the - pipeline, keeping the other steps in order. Stored `sys_metadata` agent rows replay - it at rehydration; one notice per edit. -- **D3 entry `agent-structured-output-refused-members-retired`** carries the - judgement the conversion cannot make: whether an agent whose block was deleted - should now carry a `json_schema` contract. -- **No deprecation window**, per the project's startup-stage posture. - -### Describes and the liveness ledger - -- `agent.structuredOutput` drops `[EXPERIMENTAL — not enforced]`: it states that the - cloud AI runtime enforces it on every final answer and that the open framework - edition does not run agents. Its ledger row moves `experimental` → `live`, citing - the cloud readers (`agent-runtime.ts#compileStructuredOutput`, - `ai-service.ts#AIService.settleFinalAnswer`) as attested by the cloud seat's - reading at cloud `cb62c3ea`, `verifiedAt` 2026-10-02. `os lint` / `os validate` no - longer warn `liveness-experimental-property` on an agent that sets it. -- `fallbackFormat`'s describe states what the runtime does with it: once the primary - format's retries are spent, the last answer is checked against the fallback. -- `guardrails.blockedTopics`'s describe states the enforced match: an exact, - case-sensitive match on the tool name, on `action_` plus the action type, or on - the tool category. -- The generated agent reference page follows. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is -published, and tenant-authored agents were not measured. This repo authors no -`structuredOutput` outside `packages/spec`, and the cloud seat's reading found no -producer in cloud. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21279-record-activity-host-feed-guidance.md b/.changeset/21279-record-activity-host-feed-guidance.md deleted file mode 100644 index 7f048f767c7..00000000000 --- a/.changeset/21279-record-activity-host-feed-guidance.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): `record:activity`'s props row names `items` / `loading` as the host's feed slot when it refuses them - -Clause-②: no - -`ComponentPropsMap['record:activity']` (`RecordActivityProps`) refused an authored `properties.items` or `properties.loading` with only the generic "Unrecognized key(s) on this `record:activity`" line. Both are keys the objectui `record:activity` renderer reads, as a feed a host that composes the block in code already owns, so an author copying a TSX composition into a JSON page met no reason for the refusal. - -- The refusal now says who reads each key on each mount the row reaches. On a standalone `record:activity`, `items` is the host's data channel and `loading` the host's fetch state for that feed. On a `record:chatter` / `record:discussion` `feed`, which is the same object, nothing reads either. The remedy is the same on both: omit them. The block then presents the record page's discussion feed, and a standalone `record:activity` with no discussion context fetches the record's own `sys_activity` rows. This is the same `guidance` shape `record:history`'s row already uses for `entries` / `loading`. -- The accept set does not change. Both keys stay refused, through the row and through `record:chatter` / `record:discussion`'s `feed`, which is the same object. Only the message text changes; `record:history` is unchanged. diff --git a/.changeset/21284-detail-entry-describes.md b/.changeset/21284-detail-entry-describes.md deleted file mode 100644 index d59378e53a7..00000000000 --- a/.changeset/21284-detail-entry-describes.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): an `object-master-detail-form` detail entry's `inlineMode` and `formFields` describes say what happens when the key is omitted on both of the renderer's paths (#21284) - -Clause-②: no - -- **Derived entry** (any entry that does not name both `relationshipField` and at least one column): an omitted `inlineMode` is resolved from the relationship field's `inlineEdit`, else from the child object's shape, and an omitted `formFields` is derived from the child object's fields. This is unchanged. -- **Entry kept as authored** (one that names both `relationshipField` and at least one column): the renderer resolves and derives nothing. An omitted `inlineMode` renders the collection as a grid, which offers the per-row form only when `formFields` lists more fields than `columns`. An omitted `formFields` means the per-row form is offered only when `inlineMode` is `form`, and it then draws the child object's full field list. -- The `inlineMode` describe used to say only "resolved from the relationship field's `inlineEdit` when omitted", and the `formFields` describe only "derived from the child object's editable fields when omitted". Neither holds for an entry kept as authored. The `formFields` describe also no longer says "editable": the derived list keeps `readonly` fields, as `deriveInlineRowFormFields` (`@objectstack/spec/data`) does. -- No schema accepts or refuses anything new. Only the two describes, the reference page that lifts them, and one source comment change. diff --git a/.changeset/21285-drop-dead-oclif-plugins.md b/.changeset/21285-drop-dead-oclif-plugins.md deleted file mode 100644 index f3f7b395668..00000000000 --- a/.changeset/21285-drop-dead-oclif-plugins.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -The published `package.json` no longer declares `oclif.plugins`, and the package no longer lists `@oclif/plugin-help` or `@oclif/plugin-plugins` as devDependencies. The array named both plugins, but they were only devDependencies, and oclif loads an `oclif.plugins` entry only when the same name is in `dependencies`. Neither plugin ever loaded. - -Clause-②: no - -**What changes for an operator.** Nothing. `os --help`, every command and topic, and the output of `os help` and `os plugins` read byte-identical before and after the change. `os help` and `os plugins …` were never commands, and each still exits 2 with `command … not found`. Use `os --help` or `os --help` for help. - -**What the README now says.** It said `os plugins install`, `uninstall` and `update` came from `@oclif/plugin-plugins` and installed CLI extensions. That was never true. This CLI ships no plugin manager. To add commands to it, build an `os` distribution: a package whose own `package.json` lists the extension in both `oclif.plugins` and `dependencies`. diff --git a/.changeset/21286-cli-extension-tsdoc-discover.md b/.changeset/21286-cli-extension-tsdoc-discover.md deleted file mode 100644 index a6783502f5d..00000000000 --- a/.changeset/21286-cli-extension-tsdoc-discover.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The `kernel/cli-extension` module documentation no longer tells plugin authors to run `os plugins install`. Step 2, "Discover", said the plugin is listed in `@objectstack/cli`'s `oclif.plugins` array, or that users install it with `os plugins install`. Neither is true: `@objectstack/cli` declares no `oclif.plugins` and ships no plugin manager, so `os plugins` is not a command. The step now says what loads a plugin: oclif loads a plugin that the CLI's own `package.json` lists in both `oclif.plugins` and `dependencies`. To add a plugin's commands to `os`, build an `os` distribution whose own `package.json` lists the plugin in both places. The generated reference page carries the same text. - -Clause-②: no - -Documentation only. No schema, export or type changes. diff --git a/.changeset/21289-ai-json-schema-untyped-subschema-refused.md b/.changeset/21289-ai-json-schema-untyped-subschema-refused.md deleted file mode 100644 index 8a94c45bb90..00000000000 --- a/.changeset/21289-ai-json-schema-untyped-subschema-refused.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: `action.ai.outputSchema` and `agent.structuredOutput.schema` refuse an untyped subschema that carries a type-scoped keyword, at its path, as the AI runtime does (#21289) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on two published authoring slots, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. Every schema it refuses was already refused by the AI runtime before the action or agent ran, so nothing that worked stops working; what moves is where the refusal is reported — at authoring, at the subschema's path, instead of at the first invocation. - -**`@objectstack/spec`** - -- **`action.ai.outputSchema`** (stack actions and object-nested actions) and **`agent.structuredOutput.schema`** were open records. The cloud AI runtime compiles both through one guard whose schema reader does not check a type-scoped keyword on a subschema with no `type`, and refuses the whole schema. Both slots are now declared by one factory that mirrors that guard exactly: - - **refused:** an object node whose `type` is absent and which carries any of the 22 type-scoped keywords (`properties`, `required`, `additionalProperties`, `patternProperties`, `propertyNames`, `minProperties`, `maxProperties`, `items`, `prefixItems`, `contains`, `minItems`, `maxItems`, `uniqueItems`, `minLength`, `maxLength`, `pattern`, `format`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`), with any value; - - **where:** the schema root, every value of `properties`, `patternProperties`, `$defs`, `definitions` and `dependentSchemas`, and the subschema (or each array entry) of `items`, `additionalProperties`, `contains`, `propertyNames`, `not`, `if`, `then`, `else`, `unevaluatedProperties`, `unevaluatedItems`, `anyOf`, `oneOf`, `allOf` and `prefixItems` — under typed parents too; `$ref` is not followed; - - **accepted:** boolean subschemas, `{}`, a node with any `type` value, and an untyped node carrying only keywords outside the list (`enum`, `const`, `$ref`, `anyOf`, `title`, `description`, `default`, …); - - each offending subschema is its own issue, located at the slot path plus the subschema path (`ai.outputSchema.properties.customer`), and the message names the keyword and the `type` to declare. -- The TypeScript types of both slots are unchanged (`Record`). The published JSON Schema does not state the rule: it is a refinement, which the JSON Schema projection does not carry, so a JSON Schema validator still accepts such a schema in either slot. The affected published schemas name the slot in their `x-dropped-refinements` list. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `outputSchema: { properties: { id: { type: 'string' } }, required: ['id'] }` | `outputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }` | -| `schema: { type: 'object', properties: { tags: { items: { type: 'string' } } } }` | `schema: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' } } } }` | -| `{ properties: { code: { pattern: '^[A-Z]+$' } } }` anywhere in either slot | `{ type: 'object', properties: { code: { type: 'string', pattern: '^[A-Z]+$' } } }` | - -The one-line fix: declare its `type` on every subschema that carries a type-scoped keyword — `"object"`, `"array"`, `"string"`, or `"number"` / `"integer"`, as the refusal names. - -## Who is affected, measured - -On `origin/main` `135daaa06b`: the package fixtures author either slot three times (one action `ai.outputSchema`, two `structuredOutput.schema`), every subschema typed; the examples, the documentation and the published skills author neither slot. No fixture needed a change. Deployed metadata was not measured. A stored action or agent carrying such a schema still loads; its next save is refused until the `type` is declared. diff --git a/.changeset/21293-single-series-multi-measure-refused.md b/.changeset/21293-single-series-multi-measure-refused.md deleted file mode 100644 index 4a151c41bef..00000000000 --- a/.changeset/21293-single-series-multi-measure-refused.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: a `pie` / `donut` / `funnel` / `treemap` / `sankey` dashboard widget takes ONE measure with a dimension too — two or more are refused at `values`, and the check export is renamed `checkDashboardWidgetChartMeasureArity` (#21293; extends #20958) - -Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface swaps one export for another: `checkDashboardWidgetDimensionlessMeasureArity` is removed and `checkDashboardWidgetChartMeasureArity` is added in its place, the same check with a second arm. - - - -**BREAKING** accept-set narrowing at `dashboard.widgets[].values`, plus one renamed -export, shipped as `minor` under this repo's launch-window convention for breaking -changes (`check-changeset-no-major` refuses `major` while the window is open, so -breaking-ness is carried by this banner and by the ADR-0087 disposition above, -never by the bump level). The prescription is registered under protocol major 18 -as `dashboard-widget-single-series-multi-measure-refused`. - -**What was wrong.** The previous release refused two or more measures on a -dimensionless `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / -`sankey`, and stepped aside for any widget that declared a dimension. Five of those -types draw ONE series whatever the dimension: objectui's chart renderer binds the -first series on its `pie` / `donut`, `funnel`, `treemap` and `sankey` arms and reads -no other, so `{ type: 'pie', dimensions: ['stage'], values: ['revenue', 'cost'] }` -drew one slice per stage for `revenue` and no trace of `cost`. Measured on this tree -before the change: that body parsed through `DashboardWidgetSchema` on all five -types (and on `scatter` / `radar` / `bar` / `table`), while `bogusProp` on the same -widget was refused by name, the lit control. After it, the five are refused at -`widgets[N].values`; `scatter` and `radar` with a dimension are outside the ruling -and parse as before. - -### Write instead - -| wrote | write instead | -|---|---| -| `{ id: 'mix', type: 'pie', dataset: 'sales', dimensions: ['stage'], values: ['revenue', 'cost'] }` | `{ id: 'mix', type: 'table', dataset: 'sales', dimensions: ['stage'], values: ['revenue', 'cost'] }` — a column per measure | -| the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure in each stage | -| the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['revenue'] }` **and** `{ id: 'mix_cost', type: 'pie', …, values: ['cost'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) | -| `import { checkDashboardWidgetDimensionlessMeasureArity } from '@objectstack/spec/ui'` | `import { checkDashboardWidgetChartMeasureArity } from '@objectstack/spec/ui'` — same `(widget, ctx)` signature; chain it where the old name was chained | - -No conversion does this for you: whether a two-measure pie by stage meant a table, a -grouped bar chart or two pies is an authoring choice. The refusal is ONE `custom` -issue at `widgets[N].values` naming the widget's `id`, the number of measures and -the authored `type`, and saying that type draws one series whatever its -`dimensions`. - -**Why the export is renamed.** The dimensionless rule's check now has a second arm -that judges widgets WITH a dimension, so its old name described a boundary that no -longer exists. It refuses everything the old name refused, word for word on a -dimensionless widget. No first-party consumer chained the old name: objectui's -`DashboardWidgetSchema` mirror chains `checkDashboardWidgetStageOrder` and -`checkDashboardWidgetMetricMeasureArity` only, measured at the pinned objectui -commit and on objectui's `main`. - -**Nothing else moves.** One measure parses on every type; `scatter` and `radar` -keep accepting several measures with a dimension; every type in -`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` keeps accepting any number of measures with -or without a dimension; a dimensionless widget of the five keeps the dimensionless -refusal, word for word and still ONE issue; the metric family's refusal is -unchanged; an empty `values` keeps its `too_small`; a `type` outside -`ChartTypeSchema` reports the type refusal alone. Census at the branch point -(`4b20c8474`), every tracked `.ts` / `.tsx` / `.js` / `.mjs` / `.cjs` / `.json` / -`.md` / `.mdx` / `.yml`: 496 literals carry `values: [...]`, 33 of them on one of -the seven types, and the only dimensioned multi-measure one on the five is a spec -test fixture that pinned the old acceptance (moved to the refusal in this change). -The same scan over objectui at its pinned commit (`89cad75d5`) finds no authored -widget of that shape — its one hit is the prose example in a changeset. diff --git a/.changeset/21299-having-no-class-reference.md b/.changeset/21299-having-no-class-reference.md deleted file mode 100644 index eb6f77759cf..00000000000 --- a/.changeset/21299-having-no-class-reference.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: `having` and the per-aggregation `filter` refuse a `{ $field }` comparison against a column with no comparison class (a file field, a list, a formula) with `INVALID_FILTER` / 400, as `where` refuses it; `applyInMemoryAggregation` takes the same reference rules - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what a `{ $field }` reference may pair at two positions of `engine.aggregate`, and what `applyInMemoryAggregation` accepts when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What was accepted before.** The spec's comparison-class verdict (`crossFieldComparisonVerdict`) answers `no-class` for a pair in which either column has no comparison class: a list or an object (a structured-JSON type, a multi-option type, a multi-capable type flagged `multiple: true`), a file field (`FILE_REFERENCE_TYPES`), or a formula. `having` and a per-aggregation `filter` (`aggregations[i].filter`) did not judge that answer. Measured on `SqlDriver` over better-sqlite3 through `engine.aggregate`, beside a `where` twin that `driver-sql` refused `INVALID_FILTER` / 400 each time: - -- a per-aggregation `{ customer_id: { $ne: { $field: 'photo' } } }` (text against an image) counted 6 of 6 rows; -- a per-aggregation `{ closed_at: { $lte: { $field: 'due_f' } } }` (datetime against a formula) counted 0 of 6; -- a per-aggregation `{ amount: { $ne: { $field: 'tags' } } }` (number against a multiselect) counted 6 of 6 when the column held no value, 0 on an empty table, and was refused by the per-row array check, in other words, when the column held a list; -- `having: { photo: { $ne: { $field: 'n' } } }` over a groupBy on an image field kept all 6 groups. - -A `{ $field, addDays }` pair against a formula was answered at both positions. `applyInMemoryAggregation`, called with a field map, applied none of `engine.aggregate`'s reference rules: a reference to a field the map does not declare, a pair across two classes and a pair against a column with no class were all counted. - -**What is refused now.** At `having` and at a per-aggregation `filter`, a scalar comparison (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) whose `{ $field }` comparand, or whose own column, has no comparison class, with or without `addDays`. The refusal is `INVALID_FILTER` / 400, raised before any driver is asked for a row, on an empty set as on a populated one, in the reason `driver-sql`'s `where` logs for the same pair: the column it names (the referenced one first, as `where` asks it first) "has no scalar stored column a comparison can read". The per-aggregation `filter` withholds the fields, the operator and the reason from the message and writes them to the server log, as `where` does; `having` names the two columns of the query's own projection. A `{ $field, addDays }` pair against a file or list column was already refused, in the `addDays` pair rule's words (that rule reads those types as text, so it answered with a cross-class or an offset sentence); it is now refused in this one. - -`applyInMemoryAggregation(rows, ast, timezone, fields, reportWithheld)`, when `fields` is passed, judges each per-aggregation `filter` by the reference rules `engine.aggregate` applies at that position, through the same function, before any row is judged: the referenced column (and an `addDays` offset column) is declared, a pair across two classes or against a column with no class is refused, and an `addDays` pair follows its class rule. The refusal is `INVALID_FILTER` / 400; the withheld diagnostic goes to `reportWithheld`, and it names no object (this function is not told one). - -**The remedy.** Compare two columns that each have a comparison class, and the same one: a file field, a list and a formula have no stored scalar a comparison can read. Compare the scalar column the value is derived from, or filter the column with a literal. - -**Unchanged.** A reference between two columns of one class answers as before, and the cross-class refusal keeps its words. A side with no declaration is not judged at any of the three positions: a host with no registered object, a column the field map does not carry (`id`, and an audit-opt-out object's row-carried `created_at` / `updated_at`), an aggregation over an undeclared field. A declared type outside `FieldType` is not judged either. `applyInMemoryAggregation` called without `fields` judges nothing it did not judge before. diff --git a/.changeset/21310-cli-readme-flags.md b/.changeset/21310-cli-readme-flags.md deleted file mode 100644 index cdebf1609df..00000000000 --- a/.changeset/21310-cli-readme-flags.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -The published README now describes the `os` that ships. Five things it said were false. - -Clause-②: no - -- **Short flags.** The README listed `-v, --version` and `-h, --help` as global options. `os -v` and `os -h` exit 2 with `command -v not found` / `command -h not found`, because only `--version` and `--help` are registered. It now lists `--version` and `--help` alone and says there is no short form. `-v` already belongs to commands of their own: it is `--verbose` on `os dev`, `os serve`, `os start` and `os doctor`, and `--version` on `os package publish` and `os package install`. -- **The `os plugin` group.** The README said there is no `os plugin` command group. `os plugin build`, `os plugin sign` and `os plugin publish` are registered, and the README now lists them. It also says the group has no `install`, and that `os plugin` is a different thing from `os plugins`, which is not a command. -- **Two command rows.** `os init [name]` creates a new directory of that name when a name is given, so it no longer says "in the current directory" for every case. `os dev` restarts the server after each rebuild, so it no longer says "with hot reload". -- **Cloud credentials and flags.** The README said every cloud command takes its credentials from `os cloud login` or from `--token` / `OS_CLOUD_API_KEY` and `--server` / `OS_CLOUD_URL`. That holds only for `os package publish` and `os plugin publish`. `os environments list`, `show`, `create`, `bind` and `switch` take `-u, --url` (env `OS_CLOUD_URL`) and `-t, --token` (env `OS_TOKEN`), and otherwise use the `os login` session in `~/.objectstack/credentials.json` — never the `os cloud login` session. With only `os cloud login` done they exit 1 with `Authentication required`. The README now has a per-command table, and its typical publish flow says so at the `os environments create` step. -- **`os serve --ui`.** The README said it enables "Studio UI". It enables the bundled Console portal at `/_console/` when `@object-ui/console` is installed, which is what `os serve --help` says. - -**What changes for an operator.** Nothing at runtime. No command, flag, environment variable, exit code or help page changes. diff --git a/.changeset/21315-detail-entry-sortfield-describe.md b/.changeset/21315-detail-entry-sortfield-describe.md deleted file mode 100644 index ebcac8f27ea..00000000000 --- a/.changeset/21315-detail-entry-sortfield-describe.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -docs(spec): an `object-master-detail-form` detail entry's `sortField` and `amountField` describes say what happens when the key is omitted on each of the renderer's paths (#21315) - -Clause-②: no - -- **Entry the renderer resolves** (any entry that does not name `relationshipField` together with at least one column whose every column has a `type`): an omitted `sortField` is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, and an omitted `amountField` is picked from the grid's number and currency columns. This is unchanged. It includes an entry that names `relationshipField` and columns of which some have no `type`: the renderer keeps that entry's `formFields` and `inlineMode` as authored, but it still derives these two. -- **Entry kept exactly as authored** (one that names `relationshipField` and at least one column, and gives every column a `type`): the renderer derives neither. An omitted `sortField` means the grid stamps no line position, so a drag-reorder is not saved. An omitted `amountField` means the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set. -- The `sortField` describe used to say only "derived from a `position` / `sort_order` / … field when omitted", which does not hold for an entry kept exactly as authored. The `amountField` describe said nothing about omission. -- No schema accepts or refuses anything new. Only the two describes and the reference page that lifts them change. diff --git a/.changeset/21316-objectql-face-order-limit.md b/.changeset/21316-objectql-face-order-limit.md deleted file mode 100644 index 53459e25103..00000000000 --- a/.changeset/21316-objectql-face-order-limit.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/service-analytics": patch ---- - -fix(service-analytics): the ObjectQL strategy applies a query's `order`, then its `offset` and `limit`, to the aggregated answer, as its echoed `sql` says - -Clause-②: no - -**Before**, the ObjectQL strategy passed none of the three keys to `engine.aggregate`, which has no ordering or window grammar, and applied none of them itself. Every date-bucketed query lands on that strategy, because the native-SQL strategy declines `granularity`. Measured through `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14: - -- `timeDimensions: [{ dimension: 'closed_on', granularity: 'month' }]`, `order: { closed_on: 'desc' }`, `limit: 1` answered every month, unordered (ascending on SQLite, `04, 03, 05` on PostgreSQL). -- A selected dimension with `order: { note: 'desc' }`, and a selected measure with `limit: 2, offset: 1`, answered every group in the engine's order. - -The echoed `sql` and `POST /api/v1/analytics/sql` rendered `ORDER BY … LIMIT … OFFSET …` for all three. - -**Now** the strategy orders the answer by `order`, in the key order given, and then applies `offset` and `limit`. This happens on the direct path and on the cross-object (FK-expand) path, after the re-bucket. A bare `limit` with no `order` slices the engine's order, as `LIMIT` without `ORDER BY` does. Where the native-SQL strategy answers the same query, the two answer the same rows for numbers and for text of single-case ASCII letters. The comparison is the dataset door's own `applyOrdering`, which sorts NULL and `''` last in both directions, while SQL places NULL by driver (lowest on SQLite, highest on PostgreSQL), so the two faces can still order NULL, `''`, numeric text and mixed-case text differently. - -**Dataset door.** `POST /api/v1/analytics/dataset/query` pushes a single query's `order`, `limit` and `offset` down to the strategy, and then windowed the answer a second time, so `offset` was applied twice. `limit: 2, offset: 1` over five groups answered one row, the third, on the native-SQL strategy. It now windows only a grid it could not push down. The ObjectQL strategy answered that page correctly before, because it dropped the window; it still does. - -**Unchanged.** A query with no `order`, `limit` or `offset` answers exactly the engine's aggregate rows. Which `order` keys are accepted is unchanged: the analytics door still refuses a key the query does not select. The dataset door's own ordering is unchanged too: label sort keys, derived measures, the implicit dimension order for a bare `limit`, and the chronological default. diff --git a/.changeset/21319-explain-json-column-operator-refusal.md b/.changeset/21319-explain-json-column-operator-refusal.md deleted file mode 100644 index 66293ba0077..00000000000 --- a/.changeset/21319-explain-json-column-operator-refusal.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): `security/explain` answers the read's `INVALID_FILTER` / 400 for a row-level policy that aims an operator the read refuses at a field declared JSON-stored, instead of a "visible" verdict for a request enforcement refuses (#21319) - -Clause-②: no - -The read a row-level policy scopes refuses a scalar comparison, an ordering or a text operator (`@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, and implicit equality) on a field the object declares JSON-stored: a structured-JSON type (`json`, `address`, …), or a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `radio` / `lookup` / `user` / `file` / `image` flagged `multiple: true`). The row-level write `check` refuses them too, by the same rule. `security/explain` (the `security` service's `explain()` and `POST /api/v1/security/explain`) evaluated them in JS instead. Measured with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set whose `using` is the predicate: - -| `using` | find | explain, before | -|---|---|---| -| `record.tags != 'x'` (`tags` is `tags`, multi-valued) | 400 | `visible: true`, decided by `rls` | -| `record.meta == 'x'` (`meta` is `json`) | 400 | `visible: true`, decided by `rls` | -| `!(record.tags in ['x'])` | 400 | `visible: true`, decided by `rls` | -| `record.owners != 'x'` (a `select` or `lookup` flagged `multiple`) | 400 | `visible: true`, decided by `rls` | - -The report without a record id said `allowed: true`, and a record id no row carries was reported `visible: false`. Now explain answers every one of these with the read's refusal, `INVALID_FILTER` / 400 and no verdict, for every operation, the answer it already gives a policy comparing two fields of different classes; a by-id update or delete is itself refused 403, at the row-level gate whose pre-image re-read is the refused read. The message leads with the full diagnostic, which names the field and the operator and says how to repair the policy, then the policy that carries it; the error's `cause` carries the read's refusal, with the find's code, status and message. The rule is the one the write check applies, and it reads the object's declaration, never the record. - -Unchanged: `contains` and its negation (`$contains` / `$notContains`), and the presence checks (`== null`, `!= null`), answer on such a field as before; a field declared neither way keeps every operator; an object whose schema cannot be loaded is judged as before. To repair a refused policy, test membership with `contains` (for example `!record.tags.contains('x')`). diff --git a/.changeset/21320-agent-lifecycle-retired.md b/.changeset/21320-agent-lifecycle-retired.md deleted file mode 100644 index 5156d116e17..00000000000 --- a/.changeset/21320-agent-lifecycle-retired.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/platform-objects': patch ---- - -feat(spec)!: retire `agent.lifecycle`, the agent conversation state machine, and with it the XState `StateMachineSchema` family — a conversation phase is a skill with `triggerConditions`, orchestration is Flow, record transitions are the `state_machine` validation rule (#21320) - -**BREAKING** — `agent.lifecycle` was parsed and never read. No runtime, in this -repository or in the cloud AI runtime that executes agents, moved an agent through a -declared state or refused an undeclared transition, so an authored machine changed -nothing an agent did (ADR-0049 enforce-or-remove). Enforcing it would have meant a -statechart interpreter beside Flow, the two-engine shape ADR-0020 rejected. Authoring -now refuses the key by name, with a prescription, and TypeScript rejects it. - -Its value schema had no other authorable door: ADR-0020 had already retired the XState -shape as a record-lifecycle declaration and kept the file only for this key. So the -family leaves the package with it. - -### FROM → TO - -| before | what to write instead | -| --- | --- | -| `agent.lifecycle` — any value | delete the key. | -| a conversation phase in the machine (its own instructions and tools) | a skill with its own `instructions` and `tools`, selected by its `triggerConditions`, listed in the agent's `skills`. | -| a multi-step process in the machine | a Flow. | -| a record's status transitions in the machine | a `state_machine` validation rule in the object's `validations`: `{ type: 'state_machine', field, transitions: { from: [to, …] } }`. | -| `StateMachineSchema`, `StateNodeSchema`, `TransitionSchema`, `ActionRefSchema`, `GuardRefSchema` and the types `StateMachineConfig`, `StateNode`, `StateNodeConfig`, `Transition`, `ActionRef`, `GuardRef` from `@objectstack/spec/automation` | no replacement: declare the shape your code needs itself, or drop it. For record transitions, `StateMachineValidationSchema` in `@objectstack/spec/data` is the enforced shape. | -| `StateNodeConfig` from `@objectstack/spec` or `@objectstack/spec/ai` | removed with the family; nothing in those entries mentions it any more. | - -**The one-line fix: delete `lifecycle`; put phase-scoped instructions and tools in -skills with `triggerConditions`, and orchestration in Flow.** `os migrate meta --from 17` -lists the mechanical edits for existing sources (the `lifecycle` deletion). Where each -deleted machine's intent goes is the author's judgement. - -The refusal is a parse error at `lifecycle` naming the key and the fix, and the key -fails `tsc` (its input type is `never`). - -### The retirement kit - -- **Tombstone.** `lifecycle` is a `retiredKey()` on `AgentSchema` carrying the - prescription; the agent metadata form no longer offers it. -- **D2 conversion `agent-lifecycle-removed`** (step 18, retired from the load path): - it deletes `lifecycle` from every agent, whatever it holds. The delete is lossless, - because no value of it ever changed what an agent did. Stored `sys_metadata` agent - rows and built artifacts replay it; one notice per agent. An object's ADR-0057 - `lifecycle` block shares the name and is not touched. -- **D3 entry `agent-lifecycle-retired`** carries the judgement the conversion cannot - make: which of the three destinations each deleted machine meant. -- **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:lifecycle`, and - **`RETIRED_DEFS_BY_MAJOR[18]`** registers the five published defs - `automation/StateMachine`, `automation/StateNode`, `automation/Transition`, - `automation/ActionRef` and `automation/GuardRef`. Their reference page - (`references/automation/state-machine`) is gone. -- **No deprecation window**, per the project's startup-stage posture. - -### The liveness ledger - -The `agent.lifecycle` row moves `experimental` → `dead` with a REMOVED note -(`verifiedAt` 2026-10-02); the tombstone keeps it in the walked shape. No `agent` row is -`experimental` any more. `os validate` and every other parsing door refuse the key at -parse, before any advisory runs. `os lint` reads the unparsed stack, so it now grades the -key `liveness-dead-property` where it used to say `liveness-experimental-property`. - -### `@objectstack/platform-objects` - -The agent metadata-form catalogs drop the `lifecycle` row's label and help text in all -four locales. - -⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is -published: tenant-authored agents, and code outside this repository importing the -family's exports, were not measured. This repository authors no `agent.lifecycle` -outside `packages/spec` and imports none of the family outside it; the pinned objectui -checkout imports none of the family and reads no `agent.lifecycle`. - -Clause-②: yes (narrowing) - - diff --git a/.changeset/21321-install-local-binds-artifact-handlers.md b/.changeset/21321-install-local-binds-artifact-handlers.md deleted file mode 100644 index 26cac0aa683..00000000000 --- a/.changeset/21321-install-local-binds-artifact-handlers.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/runtime': minor -'@objectstack/cloud-connection': patch ---- - -An app installed with `os package install ` now runs its `type: 'script'` action bodies and its body hooks, and MCP `list_actions` lists a script action only when `run_action` can run it (#21321). - -Clause-②: yes (widening) - -- **`@objectstack/runtime`.** New export `bindAppArtifactHandlers(ql, bundle, { appId, logger, source? })`. It binds every action `body` of an artifact through `ql.registerAction`, and every hook `body` and bundle function through `ql.bindHooks`, all under the owner `app:`. `appArtifactHandlerOwner(appId)` returns that owner key. Each call first removes the action handlers and hooks the same owner bound before. A reinstall therefore leaves one handler per action, and an action or hook that the new version dropped stops running. `AppPlugin.start` now binds through this function, with the same log lines and the same results for a boot artifact. -- **`@objectstack/runtime`, MCP `list_actions`.** A `script` action is listed only when the engine has a handler registered for it. The check reads `listRegisteredActions()` and uses the same object and key order as `run_action`. Before, a declared `target` or `body` was enough to be listed, so `list_actions` could list an action that `run_action` refused with "No handler registered". An engine without `listRegisteredActions` gets no script actions listed. Declarative update actions and `flow` actions are listed as before. -- **`@objectstack/cloud-connection`.** The install-local plugin calls `bindAppArtifactHandlers` on `POST /api/v1/marketplace/install-local` and when it rehydrates its ledger at `kernel:ready`. Before, an installed package's script actions answered REST `404 RESOURCE_NOT_FOUND` and MCP "No handler registered", before and after a restart, and its body hooks never ran. The same artifact booted with `os start --artifact` was not affected. diff --git a/.changeset/21322-hot-install-binds-boot-steps.md b/.changeset/21322-hot-install-binds-boot-steps.md deleted file mode 100644 index 6119ed936c9..00000000000 --- a/.changeset/21322-hot-install-binds-boot-steps.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/cloud-connection": patch -"@objectstack/plugin-security": patch ---- - -fix(cloud-connection,plugin-security): a package installed into a running runtime fires its record-change flows and has its permission sets in `sys_permission_set` right away, not after a restart - -Clause-②: no - -**Before**, `os package install ./dist/objectstack.json` into a running `os start` (the install-local route) registered the package, bound its script actions and body hooks, and stopped there. Two things the boot does for a package happen at `kernel:ready`, and that moment had already passed. The automation engine binds flows at `kernel:ready`, so the package's record-change flows never fired: a task updated to `done` wrote no note. The security plugin seeds declared permission sets at `kernel:ready`, so the package's set had no `sys_permission_set` row. `/meta/permission` listed the set, but an admin could not grant it. A restart fixed both, because the restart re-registers the package before those two steps run. Nothing in the CLI output or the install response said a restart was needed. - -**Now** the install route announces `metadata:reloaded` once the package is registered, bound, persisted and seeded. That is the same event a Studio package publish, a per-item publish and an artifact reload already announce. The automation engine already re-syncs its flows on it. The security plugin now re-runs its declared-permission seeding on it: the same function and organization passes as the boot, with the same provenance rules (`managed_by: 'package'`, `package_id`). Right after the install, the flow fires and the set's row exists, with the same state a restart gives. The seeding is idempotent and writes nothing when no permission set changed. It runs only after the boot's own pass has finished. A failed re-sync does not fail the install. It is logged at `warn` with the restart that repairs it. - -**Unchanged.** The restart path (the ledger rehydrate) announces nothing and behaves as before. The install response and the CLI output keep their fields and text. A package's `defineStack({ jobs })` are still not scheduled by install-local, on install or after a restart, because a job's handler is code from the artifact's runtime module and an inline install carries only the JSON. diff --git a/.changeset/21323-verify-author-time-rules.md b/.changeset/21323-verify-author-time-rules.md deleted file mode 100644 index f9d8fece4d4..00000000000 --- a/.changeset/21323-verify-author-time-rules.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -fix(cli)!: `os verify` runs the author-time rules first, and a stack they refuse fails `verify` with the findings `os validate` reports (#21323) - -Clause-②: yes (narrowing) - - - -**BREAKING** — `os verify` narrows what it passes. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What was accepted before.** `os verify` booted the app and exercised CRUD round-trip fidelity and, with `--rls`, the RLS invariant — and nothing else. A stack carrying a lookup to an object that does not exist, an action `visible` expression naming a field without `record.`, or a list column naming no field booted, round-tripped its records and printed `✓ verify passed` at exit 0, while `os validate`, `os build` and `os lint` all refused it. The documented done-bar ("`objectstack verify` is green") was green on a stack the build refuses to ship. - -**What is refused now.** `os verify` runs two stages. The first is the author-time rule registry `os validate` runs, over the stack prepared the way `os validate` prepares it: normalized, inline handlers lowered, parsed against the protocol schema, the SDUI manifest read beside the config, judged whole and then once per package of a multi-package artifact. A gating finding, or a stack that does not parse, exits 1 with those findings and the runtime stage never starts: - -- text face: `✗ Author-time rules failed (N issues) — the runtime stage did not run`, then each finding with its rule and location (the per-package and schema refusals have their own sentence); -- `--json`: the command's failure envelope, `error` (the sentence), plus a new key, `errors`, carrying the findings in the shape `os validate --json` carries them under `errors` — rule findings (with `package` on a per-package one), or the schema issues. - -Advisories never fail the stage; the text face counts them and points at `os validate`. On a passing stack the text face prints one step line and `✓ Author-time rules passed (N rules)` before the runtime stage, and the `--json` report of a run that reaches the runtime stage is unchanged. - -**Who is affected.** Only a stack `os build` already refuses: the first stage runs the same gating rules over the same prepared stack, so every stack it refuses, `os build` refuses too. The remedy is the one `os validate` prints for each finding. Measured with this branch's CLI over the examples at `222ecc27f9` (unchanged on this branch): `os validate` exits 0 on `examples/app-todo`, `examples/app-crm`, `examples/app-showcase` and `examples/app-multi-package`, so none of the four is refused by the new stage. diff --git a/.changeset/21324-verify-json-stdout.md b/.changeset/21324-verify-json-stdout.md deleted file mode 100644 index 2506b76a3ae..00000000000 --- a/.changeset/21324-verify-json-stdout.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os verify --json` writes exactly one JSON document to stdout; the booted stack's log lines move to stderr (#21324) - -Clause-②: no - -`os verify --json > report.json` used to exit 0 and leave a file no JSON parser accepts. On a two-object stack that reaches the runtime stage, 318 lines landed on stdout ahead of the report: the kernel logger's `INFO` and `WARN` records, the ObjectQL registry's `[Registry] …` lines and the HTTP server's stop line. `JSON.parse` failed at position 4. - -Under `--json`, stdout now carries the report and nothing else, and every other line the run writes goes to stderr. Nothing is dropped: the boot records, the warnings among them and the shutdown lines all still reach the operator, on stderr. The document is unchanged, and so is the shape of each of the three `--json` documents (the runtime report, the author-time refusal, and the could-not-run envelope). - -`os verify` without `--json` is unchanged: the log lines stay on stdout beside the text report. - -A script that read those log lines from `os verify --json`'s stdout now reads them from stderr. diff --git a/.changeset/21325-generate-binds-from-stack.md b/.changeset/21325-generate-binds-from-stack.md deleted file mode 100644 index 1bb34ee9265..00000000000 --- a/.changeset/21325-generate-binds-from-stack.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -fix(cli)!: `objectstack generate` binds a view, flow, action or app to an object (and an action to a flow) that you name or that the stack declares, never to one derived from the new item's name, and every scaffold passes `objectstack validate`, `objectstack build` and `objectstack lint` with zero findings - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows which `objectstack generate` invocations write a file. It ships as `minor` under the launch-window convention for narrowings. No export or published type changes. - -**Why.** `view`, `flow`, `action` and `app` scaffolds took the object they bind from their own name, and an action took its flow the same way. On a fresh `npm create objectstack` project holding `project` and `task`, `objectstack generate flow task_done` wrote a flow triggered by an object called `task_done` that nothing declares (a flow that never fires) and reported success, while `objectstack generate action complete_task` and `objectstack generate app tasks` were refused, because no object was called `complete_task` or `tasks`. Nothing let the author name the object they meant. - -**New options.** - -- `--object ` names the object a `flow`, `action` or `app` binds, as the stack declares it or without the namespace prefix (`--object task` binds `tasks_app_task` under `namespace: 'tasks_app'`). Without it, the scaffold binds the stack's only object. -- `--flow ` names the flow an `action` runs. Without it, the action runs the stack's only flow. - -**What is now refused, with nothing written.** In each case the command names what the stack declares and the command to run instead. - -- A `flow`, `action` or `app` with no `--object` in a stack that declares no object, or several. -- `--object` or `--flow` naming nothing the stack declares. -- An `action` with no `--flow` in a stack that declares no flow, or several. -- A `view` whose name is not an object the stack declares. A view is still named after the object it binds: `objectstack generate view task` writes the views of `tasks_app_task`. -- Any of these four outside a project, where there is no config and so no stack to check the binding against. -- `--object` or `--flow` on a type that takes neither (`object`, `dashboard`, `skill`, `picklist`, and the `types`, `client` and `migration` routes), instead of reading as honoured. - -**What the scaffolds now write.** Each was measured adding at least one finding to `os validate`, `os build` or `os lint`, and now adds none. - -- `object`: the record's title field (`name`) and no `description` field. Nothing read the `description` field, so `field-no-consumers` reported it on every generated object as soon as the project held any view, flow, action, app, dashboard or skill. -- `view`: no container `name` or `label`. The container is registered under its `object`, so `name` could only restate that key or contradict it, and no reader reaches a container's `label`. Both were `liveness-dead-property` warnings. The list now carries the `label` that `os lint` requires (`required/label` was an error). Its columns are every field the bound object declares, and it is sorted by the object's title field. It used to show a fixed `name` column, which an object without a `name` field refused. -- `flow`: `status: 'active'` in place of `'draft'`. A draft flow already fires its trigger (only `obsolete` and `invalid` disable one), so the runtime behaviour is unchanged. `flow-draft-status-ambiguous` warned on every scaffold. -- `action`: `locations: ['record_header']`. With no placement, `action-no-placement` warned that the button renders nowhere. -- `app`: its navigation entry opens the bound object and is labelled with that object's plural label. - -**What to write instead.** Name the object a flow, action or app binds, for example `objectstack generate flow task_done --object task`. Name the flow an action runs when the stack has more than one, for example `objectstack generate action complete_task --object task --flow task_done_flow`. Run the command in the project's directory. Generate a view under the name of an object the stack declares. - -**Unchanged.** `objectstack generate object`, `dashboard`, `skill` and `picklist`, and every name, namespace, parse and import check in front of the bindings. Files generated by earlier releases are not touched. diff --git a/.changeset/21326-crypto-context-scope-discriminant.md b/.changeset/21326-crypto-context-scope-discriminant.md deleted file mode 100644 index d18367d90de..00000000000 --- a/.changeset/21326-crypto-context-scope-discriminant.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-settings': minor -'@objectstack/objectql': patch -'@objectstack/service-datasource': patch ---- - -feat(spec): `CryptoContext` gains a required `scope` discriminant, and `LocalCryptoProvider` binds it into a delimiter-safe, versioned AAD (ADR-0128 D1–D3, #21326 stage 1) - -Clause-②: yes - -**BREAKING** for `ICryptoProvider` implementers and for every direct caller of -`encrypt`, `decrypt` or `rotateKey`: `CryptoContext.scope` is required, so a -context literal without it stops compiling (`TS2741`), and the compiler names the -missing member. `LocalCryptoProvider` also refuses such a context at runtime with -`CryptoContextScopeError`, for a caller the compiler never saw. Code that only -injects a provider is unaffected. - -`scope` is a member of the new closed set `CRYPTO_CONTEXT_SCOPES` (type -`CryptoContextScope`), one member per producer of `CryptoContext`: -`settings` (`SettingsService`), `object_secret_field` (the ObjectQL engine's -secret-field path) and `datasource_credential` (the datasource secret binder). -Each producer in this release passes its own member on every call. A new producer -adds its own member; it never borrows an existing one. - -What the contract now requires of every provider that binds AAD: - -- **Producer-discriminated (D1).** The AAD binds `(scope, namespace, key)`, so a - ciphertext sealed by one producer does not authenticate under another - producer's context, whatever the two `(namespace, key)` pairs are. -- **Delimiter-safe (D2).** Distinct triples produce distinct AAD bytes. An - unescaped join is not permitted. -- **Versioned.** A ciphertext records which AAD derivation sealed it, and is - opened only with that derivation. An unknown derivation fails closed. No second - derivation or scope is ever tried after a failure (D3). - -`LocalCryptoProvider` seals every new value under derivation version 2: a lead -byte that never occurs in UTF-8, a versioned label, then the scope, namespace and -key, each prefixed with its 4-byte length. The ciphertext carries a `v2:` marker. -A ciphertext with no marker is version 1, the bare base64 every earlier release -sealed, and it still opens with the older `(namespace, key)` binding. Existing -secrets therefore keep working with no action, and carry the older binding until -they are re-wrapped. Re-wrapping existing ciphertext at rest is stage 2 of -#21326. `rotateKey` already re-seals a version-1 handle under version 2. Any other -marker is refused with `UnknownCiphertextVersionError`. - -Operational note: a secret set or rotated by this release carries the `v2:` -marker, and an earlier release cannot open it. A rollback past this release needs -those values to be set again. - -`@objectstack/objectql` and `@objectstack/service-datasource` pass their own -scope on every seal and open. Their public surface is unchanged. - - diff --git a/.changeset/21326-secret-rewrap.md b/.changeset/21326-secret-rewrap.md deleted file mode 100644 index a6c0af00b6a..00000000000 --- a/.changeset/21326-secret-rewrap.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@objectstack/cli': minor -'@objectstack/service-settings': minor ---- - -feat(cli): `os secret rewrap` re-wraps version-1 `sys_secret` ciphertext under the current AAD derivation, each row under its holder's producer scope (ADR-0128 §4.2, #21326 stage 2) - -Clause-②: yes (widening) - -A ciphertext sealed before ADR-0128 D1–D3 carries the older binding over -`(namespace, key)` alone, and still opens in this release. `os secret rewrap` moves -the stored values to the current binding through `rotateKey`, the seam ADR-0128 §4 -names. It is an operator command: a dry run by default, `--apply` to write, and -nothing on any boot or upgrade path invokes it. It has no HTTP surface. - -- **The scope comes from the holder.** `sys_secret` records no producer, and a - version-1 ciphertext binds no scope, so each row is re-sealed under the scope of - the producer whose holder references it: `settings` for a `sys_setting.value_enc` - handle, `object_secret_field` for a `secret:` ref on a business row, - `datasource_credential` for a `sys_secret:` `credentialsRef`. The holders come from - the same cross-producer reference union `os secret orphans` reads. A row nothing - references, a row whose holders belong to different producers, and every row while - a holder family could not be read are left as they are and counted, never re-sealed - under a guessed scope. `--apply` refuses an incomplete union and names the family. -- **Resumable.** A row already sealed under the current derivation is skipped as - done, so a stopped run finishes the rest when re-run and a finished run writes - nothing. -- **Safe against a live deployment.** Each row is written by one conditional update, - keyed on its id and the ciphertext the run read. A row a producer changed in - between is not overwritten, and a re-run picks it up. A driver with no - `updateMany` is refused before any row is opened. -- **Fails closed.** A row that does not open, or whose re-seal does not open to the - same plaintext under the same scope, is not written. The run finishes the rest and - exits 1. The check happens before the write. -- **Output is classes and counts only.** It never prints a plaintext, a ciphertext - or a row id. - -The command resolves its data key from `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the -persisted key file, in the strict posture: it never mints a key, and it hands the -settings service it boots the same provider so that service does not mint one -either. With no key it refuses before opening any row. - -`@objectstack/service-settings` publishes `ciphertextDerivationStatus` (and its -`CiphertextDerivationStatus` type). It is `LocalCryptoProvider`'s own reading of -which derivation sealed a stored ciphertext, read off its marker without opening it: -`current`, `superseded` or `unknown`. The re-wrap classifies rows with it rather than -restating the marker grammar. diff --git a/.changeset/21328-share-links-self-scoped-list.md b/.changeset/21328-share-links-self-scoped-list.md deleted file mode 100644 index cb9baeb6ca4..00000000000 --- a/.changeset/21328-share-links-self-scoped-list.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/plugin-sharing": patch -"@objectstack/spec": patch ---- - -A plain member's share-link list now answers: `GET /api/v1/share-links` is self-scoped for every signed-in caller, as ADR-0111 rules it - -Clause-②: no - -`ShareLinkService.listLinks` read `sys_share_link` under the caller's context. Both share-link doors force the list's `createdBy` to the caller, but the read still needed an object-level grant on `sys_share_link`, and the platform's member baseline does not grant one. So every plain member's list was refused, with or without an object filter, and the Share dialog, which loads this list when it opens, showed an error for them on every record. An admin's list answered. - -- The caller's own list is now read under the system context. This happens only when the caller has a non-empty user identity and the creator filter equals it. The read is constrained server-side to that identity, and each row it returns must pass the creator rule before it leaves. -- One creator rule now serves both `listLinks` and `revokeLink`. It never matches a caller with no user identity. Neither HTTP door reaches that case, because both answer 401 first, so for an internal caller with no user identity, `revokeLink` now refuses a link whose `created_by` is absent or empty instead of treating it as theirs. -- Every other list shape keeps the caller's context, as before: no creator filter, another user as creator, no user identity, or an admin listing someone else's links. A system caller keeps its bypass. -- The rows carry the same columns as before. The token comes back so the console can build the link URL, and the password hash never does. -- `@objectstack/spec`: the `IShareLinkService.listLinks` doc comment now describes the self-scoped own list. It previously said every listing is read under `context`. This is a doc comment only, with no type or export change. -- ⛔ No permission set changes, and no new grant on `sys_share_link`. diff --git a/.changeset/21329-share-link-owner-mint.md b/.changeset/21329-share-link-owner-mint.md deleted file mode 100644 index 85e38a7a52d..00000000000 --- a/.changeset/21329-share-link-owner-mint.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-sharing': minor -'@objectstack/spec': patch ---- - -feat(plugin-sharing): the record owner and an explicit Modify-All holder may mint a share link on a record the data door refuses them (ADR-0111 D8 rule 1, ruling A′) (#21329) - -Clause-②: yes (widening) - -- **Who may mint.** `ShareLinkService.createLink` admits the caller when they can see the record, **or** own it, **or** hold `modifyAllRecords` on the object. The object's `publicSharing` opt-in is still checked first, and `publicSharing.eligibility` still last. On an object declared `access: { default: 'private' }` no wildcard grant covers the record, so its owner's own read is refused; the owner can now share it anyway. A member who neither sees nor owns the record is refused exactly as before, with the same envelope. -- **Who still needs visibility.** A hierarchy manager whose write depth covers the record's owner manages the record's shares (revoke, grant, list), but is not admitted to mint without seeing the record: a link creates access. -- **The organization wall.** Under the `group` and `isolated` tenancy postures the owner and Modify-All alternatives are withheld and visibility alone admits, as before this release. A member who left an organization still owns the records they created there, and must not be able to publish them by link. -- **A required capability.** Neither alternative applies past a capability the object requires (`requiredPermissions`). An owner or Modify-All holder who lacks it is refused with the capability gate's own refusal, as before this release; an owner who holds it, refused only because no permission set grants the object, mints. The verdict is read from the `required_permissions` layer of `ISecurityService.explain`, so a security service the sharing service reaches must implement `explain`. If it does not, the two alternatives are withheld. -- **API.** `SharingService.canMintWithoutVisibility(object, recordId, context)` answers the two alternatives with the owner and Modify-All branches `canManageShares` reads. `ShareLinkServiceOptions.canMintWithoutVisibility` is the late-bound probe `createLink` asks once the visibility read refuses, and `SharingServicePlugin` wires it. A host that constructs `ShareLinkService` itself without it keeps the visibility rule alone. The probe slice `SharingServiceOptions.securityService` returns gains an optional `explain`, the part of `ISecurityService.explain` the capability verdict reads. -- **`@objectstack/spec` (documentation only).** The `IShareLinkService.createLink` TSDoc states who may mint, replacing "you may only link-share a record you can yourself see". The `ISharingService.canManageShares` TSDoc describes the hierarchy-manager branch, which is implemented, and says it is not mint authority. No schema, key, type or export changes. diff --git a/.changeset/21331-public-form-withdrawal.md b/.changeset/21331-public-form-withdrawal.md deleted file mode 100644 index 6ee260e57de..00000000000 --- a/.changeset/21331-public-form-withdrawal.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -Withdrawing a public form from anonymous intake now takes effect on every intake door - -Clause-②: no - -When an administrator withdraws a public form, both anonymous form routes (`GET /forms/:slug` and `POST /forms/:slug/submit`) now answer `404 FORM_NOT_FOUND` and no record is created. Republishing the form restores both routes. If a service the routes need to resolve the form is registered but cannot be reached, both routes refuse the request instead of serving the form. diff --git a/.changeset/21333-objectql-boolean-comparand-door.md b/.changeset/21333-objectql-boolean-comparand-door.md deleted file mode 100644 index db91ee4fb6f..00000000000 --- a/.changeset/21333-objectql-boolean-comparand-door.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: a comparand against a declared boolean field is narrowed to its boolean at the engine's filter door, and any string other than "true" / "false" / "1" / "0" is refused with `INVALID_FILTER` / 400 - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what a filter may compare a declared `boolean` or `toggle` field with, at every filter position and through every door that reaches the engine's filter walk (`engine.find` / `findOne` / `count` / `aggregate` / `update` / `delete`, and every spelling the data API hands it). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. - -**What was accepted before.** A string compared with a boolean field was neither refused nor read as a boolean: the engine handed it to the driver as written, and every answer was a 200. Measured on two rows (one `true`, one `false`) on InMemoryDriver and on SqlDriver over SQLite, through `engine.find`, `engine.aggregate` and the protocol's `findData` with each spelling the `POST /api/v1/data/:object/query` and `GET /api/v1/data/:object` routes hand it: - -- `"true"` (implicit, `$eq`, `$in`) and `"false"` (implicit), and both through `?filter=`, `?$filter=`, the filter AST and the bare query parameter (`?flag=true`), matched no row on either driver; -- `$ne "true"` and `$nin ["true"]` returned both rows, the true row included; -- `"yes"` matched no row, and `$ne "yes"` both rows; -- `1`, `"1"`, `0` and `"0"` at `where` (and `"1"` / `"0"` through every spelling above) matched the right row on SQLite and no row on InMemoryDriver (`$ne 1` returned both rows there); -- the per-aggregation `filter` and `having` (the engine's own evaluator) answered `"true"` with no row and no group, and `$ne "true"` with every one. - -**What is answered now.** At `where` (both spellings), the per-aggregation `filter` and `having`, on every verb that collects a filter, before any driver is asked for a row: - -- `true` / `false` are handed to the driver as written; -- `1` / `0`, `"1"` / `"0"` and `"true"` / `"false"` are narrowed to `true` / `false`, so every driver receives the one boolean each names. Measured on InMemoryDriver and on SqlDriver over SQLite, `?flag=true` and `?flag=1` now return the true row; any other driver receives the same narrowed boolean by mechanism (PostgreSQL and MySQL not measured); -- any other string, a different letter case (`"TRUE"`), surrounding whitespace, a blank and a `{placeholder}` included, is refused `INVALID_FILTER` / 400. The message names the field, its declared type, the comparand and its position, and says what is wrong with it. - -The accepted set is the one the record validator already admits when a boolean field is WRITTEN. The rule lives in `@objectstack/spec/data`'s `filter-boolean-comparand-declared-type.ts`, and the engine applies it in the same walk that judges number comparands. - -**The remedy.** Write `true` or `false`. In a querystring, where every value is a string, write `true` / `false` or `1` / `0`. - -**Unchanged.** A boolean comparand, `null` (the null test) and the flag operators (`$null`, `$exists`, `$empty`) answer as before, and so does every comparand against a field that is not boolean. A number other than `1` / `0` against a boolean field is still handed to the driver as written. A filter on a `formula` field is still refused one step earlier, as before. diff --git a/.changeset/21333-spec-boolean-comparand-contract.md b/.changeset/21333-spec-boolean-comparand-contract.md deleted file mode 100644 index df894647417..00000000000 --- a/.changeset/21333-spec-boolean-comparand-contract.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): the boolean-comparand declared-type contract in `@objectstack/spec/data` — the comparands a declared boolean field accepts in a filter, the boolean each narrows to, and the refusal words - -Clause-②: yes - -**What it declares.** `filter-boolean-comparand-declared-type.ts`, the boolean twin of `filter-number-comparand-declared-type.ts`: - -- `BOOLEAN_COMPARAND_SPELLINGS`: the accepted non-boolean spellings, `1` / `0`, `"1"` / `"0"` and `"true"` / `"false"`, each with the boolean it narrows to. This is the set the record validator admits when a boolean field is written. `readBooleanComparand` reads a comparand by it, and names why a string is not one (`NON_BOOLEAN_STRING_FORMS`: `empty`, `padded`, `letter-case`, `placeholder`, `not-a-boolean`). -- `BOOLEAN_COMPARAND_DOOR_JUDGED_TYPES` (`BOOLEAN_VALUE_TYPES` itself), and the judged positions, which are the number door's lists by identity. -- `booleanComparandFieldVerdict` and `booleanComparandDoorVerdict`, the pure verdict: `narrows`, `door-refusal` (`INVALID_FILTER` / 400), `passes` or `deferred`. -- `booleanComparandRefusalMessage`: the refusal words, inside the 500-character client bound. -- `BOOLEAN_COMPARAND_READING_CASES`, `BOOLEAN_COMPARAND_DOOR_FIXTURE` and the derived `BOOLEAN_COMPARAND_DOOR_CASES`, for a door's suite to drive. - -**What the verdict answers `door-refusal` for.** A string other than the four accepted ones, compared with a declared boolean field, at the value positions of a filter (the implicit comparand, `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte`, and each member of `$in` / `$nin` / `$between`). - -**What moves for consumers.** Nothing in this package refuses or narrows a filter, and every existing export is unchanged. The door that applies the verdict ships in the same release in `@objectstack/objectql`, whose changeset states what changes for a caller. diff --git a/.changeset/21334-view-container-cross-package-default.md b/.changeset/21334-view-container-cross-package-default.md deleted file mode 100644 index 6797400a880..00000000000 --- a/.changeset/21334-view-container-cross-package-default.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): a view container saved for an object another package ships no longer replaces that package's views or its default - -Clause-②: no - -- **What was wrong.** A runtime view container expands each member to `.`. A `list` that names no key becomes `.default`, a `form` becomes `.form`, and every member that names a key uses that key. Saved under another name, in another package or in none, for an object a code package ships, those expansions replaced that package's views of the same names on `GET /api/v1/meta/view?object=`. The replacements were still stamped with the shipping package's `_packageId` and `_provenance: 'package'`. - - On an environment-scoped kernel, the by-name read `GET /api/v1/meta/view/` kept the packaged view, so the two reads disagreed. - - On an unscoped kernel, the by-name read served the replacement too, for a container saved into a package or environment-wide. - - The container's own default kept `isDefault: true`. It either replaced the object's default view or stood beside it as a second list default. -- **What it does now.** For an object a code package ships, a container that belongs to another package, or to none, expands every member under its own name: - - a `list` that names no key becomes `.`; - - every other member becomes `..`. That covers a `list` that names its key, each `listViews` and `formViews` entry, and `form`. - - None of these views carries `isDefault`. Every name the shipping package serves answers its packaged view on both reads, unchanged, and the only `isDefault` views the object lists are the shipping package's. -- **One exception.** When the shipping package itself serves `.` (a container named after one of that package's keys), the container's default list becomes `..` instead. -- **A container with no name of its own** expands nothing on such an object. -- **What these views carry.** The container's own package as `_packageId` (none for a package-less container), and no other package's `_provenance` or protection envelope. -- **What stays.** Three kinds of container expand exactly as before, `isDefault` included: - - a container bound to the package that ships the object; - - a package-less overlay of that package's own container, saved under that container's name; - - a container on an object no code package ships. - - A write to `.default` by its own name still overrides it on both reads. -- **What changes for a caller.** Such a container's views are now served under new names: - - its default list as `.`, instead of `.default`; - - each keyed member as `..`, instead of `.`. - - A navigation `viewName` or a form-action `target` that used an old name to reach one of these views now reaches the shipping package's view. Use the new name instead. diff --git a/.changeset/21345-driver-fault-redaction-residue.md b/.changeset/21345-driver-fault-redaction-residue.md deleted file mode 100644 index 8a3d8971f78..00000000000 --- a/.changeset/21345-driver-fault-redaction-residue.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -fix(objectql): a raw statement's driver fault, and a lifecycle sweep's direct-driver fault, no longer carry the statement or the caller's values - -Clause-②: no - -Two paths the engine-boundary cut did not reach now take it. - -- **`ObjectQL.execute`.** The cut ran on a driver error's message only when the shared leak predicate recognised a statement in it, and the predicate recognises four leading verbs. A raw statement opening with any other word, such as a common-table-expression form or a dialect's own upsert or merge verb, kept the statement and the bound values on the declared fault's `cause` (its `message` and `stack`), where any logger that prints an error's cause chain wrote them out. The door now tells the cut that it sent a statement, so the cut runs whatever word the statement opens with. The predicate's list is unchanged. -- **The lifecycle sweep.** The Archiver copies rows to the cold store and deletes them from the hot store through the drivers directly, not through an engine door. A driver fault there, such as a cold write the archive store refused, put the archived row's values into the sweep's warning line and its `report.errors` entry. The sweep now cuts the fault the same way before it reports or logs it. -- **What stays.** The error's class, `code`, `status` and the database's own diagnostic, on the fault and on its `cause`. A raw statement opening with one of the four recognised verbs is cut exactly as before. A sweep failure that is not a driver error is reported word for word as before. -- **What changes for a caller.** Code that read the statement or a value out of a raw statement's fault, or out of a lifecycle sweep's error entry, now gets a `[statement and bound values redacted]` marker followed by the diagnostic. Branch on the error's class and `code` instead. diff --git a/.changeset/21349-migrate-preview-read-only.md b/.changeset/21349-migrate-preview-read-only.md deleted file mode 100644 index 1988c0c8d80..00000000000 --- a/.changeset/21349-migrate-preview-read-only.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -`os migrate meta --stored` and `os migrate audit-metadata-bodies` without `--apply` no longer write to the database they preview. Both now boot the stack the way `os migrate plan` does: schema DDL is held back, the app's inline seed loader does not run, and a SQLite file that does not exist is not created. - -Clause-②: yes (narrowing) - - - -**BREAKING** — a preview of either command at a database that lacks the table it reads now exits 1, where it used to exit 0. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What was wrong.** Both previews booted the full data stack before reading, and that boot ran schema sync and the app's seed loader. The seed loader upserts every seeded row, so a preview bumped `updated_at`, stamped `organization_id` on seeded rows that had none, put an operator's edit to a seeded row back to the seed's value, and re-evaluated relative-date seed values. On a database that was behind the app's schema, the boot also added the missing columns and created the missing tables. The 17.6.0 upgrade checklist runs both previews before their `--apply` runs, so the safety step changed the data. - -**What changes for an operator.** A preview leaves the schema and every row byte-identical, and its report is the same as before. `--apply` boots and writes exactly as before. One edge changes: a preview pointed at a database that lacks the table it reads (a SQLite file that does not exist, an unbooted database, or the wrong `--database-url`) now fails and exits 1 instead of creating the table and reporting nothing to examine. Point `--database-url` at the deployment's database, or boot the deployment once first. diff --git a/.changeset/21350-my-pending-position-address.md b/.changeset/21350-my-pending-position-address.md deleted file mode 100644 index d3dd9314529..00000000000 --- a/.changeset/21350-my-pending-position-address.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -The approvals inbox's "My Pending" now lists a request routed to a position for the users who hold that position, whichever spelling of the position address the client asks for - -Clause-②: no - -A request whose approver position nobody held when it opened keeps the literal `position:` slot. A user staffed into that position afterwards could already decide it, by naming `position:` as the actor. `resolveActor` admits a holder under `position:` and under `role:` (the deprecated pre-rename spelling) as the caller's own identity, but the decision's slot test is literal: on that slot, `role:` or no actor at all answers 403. The list read did not agree with either half. - -- `GET /api/v1/approvals/requests?approverId=…` matched each value literally. The stock console sends `role:` for every position the session carries, so the request never appeared in "My Pending". A position address now matches under both spellings `resolveActor` admits a holder under, and no others. A `team:`, `org_membership_level:` or bare-name value still matches only itself. -- The participant gate behind every approvals read counted a "current approver" by user id alone. A holder of the position who neither submitted the request nor holds admin standing got an empty list under both spellings and a `404` on `GET /api/v1/approvals/requests/:id`, though their approve call naming `position:` succeeded. The gate now also counts the slot addresses of every position on the caller's server-resolved context. A request becomes visible only to someone who can decide it. -- The decision routes are unchanged. They admit exactly the identities they admitted before, and a pin compares them against the previous predicate. - -A caller who sent the stored `position:` spelling and was already the submitter or an admin sees no change. diff --git a/.changeset/21360-environments-cloud-session.md b/.changeset/21360-environments-cloud-session.md deleted file mode 100644 index c2980efb69d..00000000000 --- a/.changeset/21360-environments-cloud-session.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -`os environments list | show | create | bind | switch` run on the `os cloud login` session - -Clause-②: yes (widening) - -The documented hosted flow is `os cloud login`, then `os environments create`. The five -`os environments` subcommands read only `~/.objectstack/credentials.json` (the `os login` -session), so with only `~/.objectstack/cloud.json` they exited 1 with -`Authentication required` before sending any request, while `os login --help` sends hosted -users to `os cloud login`. - -All five now choose their session in one shared resolver: - -- With no `--url` / `OS_CLOUD_URL`, they use the `os login` session when there is one, which - is the same behaviour as before. Otherwise they use the `os cloud login` session and the URL - it recorded. -- With a `--url`, they use the session whose file names that server, `credentials.json` first. - When neither file names it, they use `credentials.json`'s session as before. The cloud token - is never sent to a URL other than its own. -- The active environment sent with each request comes from the chosen session's file. - `os environments switch` and `create --activate` no longer write a cloud environment id into - `credentials.json` when they ran on the cloud session. - -With no session at all, the `Authentication required` message now names `os cloud login` as -well as `os login`. `os package publish` is unchanged: it still reads only `cloud.json`. diff --git a/.changeset/21365-analytics-query-window.md b/.changeset/21365-analytics-query-window.md deleted file mode 100644 index f0fa8e5c1cf..00000000000 --- a/.changeset/21365-analytics-query-window.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/service-analytics': patch ---- - -fix(spec)!: an analytics query's `limit` and `offset` are non-negative integers, and the native face runs an `offset` with no `limit` on SQLite - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing of a published request schema, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads it: the `/analytics` doors, which parse every body with `AnalyticsQueryRequestSchema` (`POST /analytics/query`, `POST /analytics/sql`) or `DatasetSelectionSchema` (`POST /analytics/dataset/query`), and answer `400 VALIDATION_FAILED` before any engine runs. - -**`@objectstack/spec`** - -- **`AnalyticsQuerySchema.limit` and `.offset`** were a bare `z.number()`. They are `z.number().int().nonnegative()` now. A negative number, a fraction, and an integer above `Number.MAX_SAFE_INTEGER` are refused at the member. `limit: 0` stays legal and answers no rows. -- **`DatasetSelectionSchema`** reads the same two declarations off `AnalyticsQuerySchema.shape`, so the dataset door holds the same accept set with no second copy. **`AnalyticsQueryRequestSchema`** extends the query, so it holds it too. -- The TypeScript types are unchanged (`number`). Only the parse narrows. - -Before, no refused value had one answer. Measured at `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14, `order { note: 'asc' }` over four groups: - -| window | native SQLite | native PostgreSQL | ObjectQL face | -|:--|:--|:--|:--| -| `limit: -1` | every row | 500 | all but the last row | -| `limit: 1.5` | 500 | two rows | one row | -| `offset: -1` | 500 | 500 | every row | - -Each one now answers `400 VALIDATION_FAILED`, with `details.fields[].field` naming `limit` or `offset` (`selection.limit` / `selection.offset` at the dataset door), on both drivers and both faces. - -**`@objectstack/service-analytics`** - -- **An `offset` with no `limit`** is a valid window: every row after the offset. The native-SQL strategy wrote `OFFSET n` with no `LIMIT` in front of it, and SQLite's grammar has no `OFFSET` without a `LIMIT`, so the query answered `500` (`near "OFFSET": syntax error`) on SQLite, while PostgreSQL and the ObjectQL face answered rows. The statement now carries the executing driver's no-limit spelling, read off the `sqlDialect` hook: `LIMIT -1 OFFSET n` on SQLite, `OFFSET n` alone on PostgreSQL (unchanged bytes), and `LIMIT 9223372036854775807 OFFSET n` when the host names no dialect. The MySQL arm is `LIMIT 18446744073709551615`, asserted as text only (no MySQL server was available to run it). -- The echoed `sql` and `POST /analytics/sql` show the statement that ran, byte for byte, on this face. - -## FROM → TO - -| you wrote in an analytics query or dataset selection | write instead | -|:--|:--| -| `limit: -1` (meant: no limit) | omit `limit` | -| `limit: 1.5` | the integer page size you meant, for example `limit: 2` | -| `offset: -1` | omit `offset`, or `offset: 0` | -| `offset: 2.5` | the integer number of rows to skip, for example `offset: 2` | - -The one-line fix: write `limit` and `offset` as non-negative integers, or leave them out. - -## Who is affected, measured - -At `origin/main` `ee75aae1a`: no example, package fixture, document or published skill writes a negative or fractional analytics `limit` or `offset`. The one stored producer that lowers into a dataset selection, a dashboard widget's `limit`, is already declared a positive integer (`z.number().int().positive()`). The sibling console repository and deployed metadata were not measured. The service does not parse a query passed to it in-process, so a host that builds an `AnalyticsQuery` in code parses it with `AnalyticsQuerySchema` before handing it over. diff --git a/.changeset/21365-objectql-echo-offset-window.md b/.changeset/21365-objectql-echo-offset-window.md deleted file mode 100644 index 251a304654c..00000000000 --- a/.changeset/21365-objectql-echo-offset-window.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/service-analytics": patch ---- - -fix(service-analytics): the ObjectQL strategy's echoed `sql` renders an offset with no limit in the dialect's own spelling, so SQLite runs the statement it prints - -Clause-②: no - -**Before**, the ObjectQL strategy wrote its own row window into the statement it echoes: `LIMIT n` when a limit was set, then `OFFSET n` when an offset was. An `offset` with no `limit` therefore echoed a bare `OFFSET`, which SQLite's grammar does not have. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` on SQLite, for a composition served by the engine aggregate, with `order: { note: 'asc' }` and `offset: 1`: the rows were right (every group after the first), but the echoed `sql` and the `/analytics/sql` body both ended `ORDER BY "note" ASC OFFSET 1`, and SQLite refuses that statement with `near "OFFSET": syntax error`. - -**Now** the statement ends with the same window clause the native-SQL strategy runs, for the dialect of the driver that serves the object: `LIMIT -1 OFFSET 1` on SQLite, which runs and answers the same rows. One function renders the window for both strategies. - -**Unchanged.** The rows either strategy answers. A window with a `limit` keeps its bytes (`LIMIT 2 OFFSET 1`) on every dialect, and on PostgreSQL an offset with no limit still echoes `OFFSET 1` alone. A host that wires no `sqlDialect` hook gets the native strategy's dialect-neutral spelling, `LIMIT 9223372036854775807 OFFSET 1`. A date-bucketed dimension still echoes as `date_trunc(…)`, which SQLite does not run; this change touches only the window. diff --git a/.changeset/21370-starter-field-groups.md b/.changeset/21370-starter-field-groups.md deleted file mode 100644 index 516899f0a86..00000000000 --- a/.changeset/21370-starter-field-groups.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'create-objectstack': patch -'@objectstack/cli': patch ---- - -fix: a fresh project no longer warns about its own starter fields after the first `objectstack generate` - -Clause-②: no - -The blank starter's `note` object (`npm create objectstack`) and the item object of the `app` template (`objectstack init -t app`) now declare one field group, `fieldGroups: [{ key: 'details', label: 'Details' }]`, and place every field in it with `group: 'details'`. Before this, the first view, flow, dashboard or other metadata that can read a field made `objectstack validate` and `objectstack lint` report `field-no-consumers` on a field the author never wrote: the note's `body`, or the item's `description` and `status`. That held whether the author generated it or wrote it by hand. Both commands still exited 0. A field placed in a declared group is drawn by the object's form and detail page, and the rule counts that as displayed, so a fresh project now reports nothing. The `plugin` and `empty` templates are unchanged: the plugin's one field is the record's title, which the rule never reports, and the empty template declares no object. - -**What changes for an author.** In a new project, the object's form and detail page show the starter fields in one section labelled Details instead of a flat list. A field you add joins a section the same way, by naming its `key` in `group`. A project scaffolded by an earlier release keeps its files. To clear the warning there, add the same `fieldGroups` entry to the object and `group: 'details'` to each field the warning names, or give each field another consumer, such as a view column. diff --git a/.changeset/21374-agent-structured-output-form-offer.md b/.changeset/21374-agent-structured-output-form-offer.md deleted file mode 100644 index e6c00873b31..00000000000 --- a/.changeset/21374-agent-structured-output-form-offer.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/platform-objects": patch ---- - -The agent metadata form now offers `structuredOutput`, the output contract the cloud AI runtime enforces on every final answer. It is a `composite` row in the AI Configuration section, spelled like the `memory` and `guardrails` rows: Studio derives its seven sub-rows from the served JSON Schema. - -Clause-②: no - -- Before this, the block had no row on the agent form, so the only way to author it in Studio was the Source tab. The form's reconciliation test excused that with a ledger row saying the key was declared but not enforced. The key has been enforced since the structured-output enforcement landed (liveness `live`), and that row is gone. -- What Studio renders, read in the console's metadata form renderer: `format` and `fallbackFormat` are selects over `json_object` / `json_schema`. `strict` and `retryOnValidationFailure` are switches, and `maxRetries` is a number. `transformPipeline` is a multi-select over `trim` / `parse_json` / `validate`. `schema`, the free-form JSON Schema record, is a JSON text editor: the stored value is shown as JSON and saved back as parsed. That is the same editor the action form already gives `ai.outputSchema`, which is the other slot this JSON Schema rule governs. -- Two editing limits of those controls. A multi-select toggle stores the steps in the order the enum declares them (`trim`, `parse_json`, `validate`). And the schema editor keeps the last valid JSON while the text does not parse. A value nobody edits is saved back unchanged. -- No schema, parse or export change. The accept set of `AgentSchema` is unchanged, and so is the refusal of an untyped JSON subschema at `structuredOutput.schema`. What moves is the form payload `getMetaTypes()` serves, and the two new leaves of the `platform-objects` metadata-form catalogs (the row's label and help text). Those are authored in `zh-CN`, `ja-JP` and `es-ES`, not left as copies of the English source. diff --git a/.changeset/21376-boolean-comparand-compilers.md b/.changeset/21376-boolean-comparand-compilers.md deleted file mode 100644 index f33ef6fe24c..00000000000 --- a/.changeset/21376-boolean-comparand-compilers.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-security': minor -'@objectstack/service-analytics': minor ---- - -Row-level security policies and the analytics native-SQL path judge a comparand against a declared boolean field by the platform's boolean-comparand rule, the one the data engine's `where` already applies - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what two compilers outside the engine's `where` door accept. The RLS compile seam now drops a row-level policy, and the analytics native-SQL face now refuses a query, when either compares a declared boolean field with a comparand outside the accepted set. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. - -- **Row-level security (`@objectstack/plugin-security`).** A compiled `using` / `check` predicate on a `boolean` or `toggle` column (or a `formula` returning `boolean`) is judged by `booleanComparandDoorVerdict` from `@objectstack/spec/data`, in the same pass as the number rule. `'true'` / `'false'`, `'1'` / `'0'` and `1` / `0` are read as the boolean each names. Anything else the rule refuses (a string such as `'yes'`, `'TRUE'` or `''`, a number other than `1` / `0`) drops the policy as a refused comparand: the read is filtered by the deny sentinel, the write is refused 403, and the WARN line names the clause, the field and the position. Before, `record.flag != 'true'` kept every row on SQLite and the write check admitted every row, so the exclusion the author wrote was not applied. -- **Analytics native SQL (`@objectstack/service-analytics`).** The query's `where` (and the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter` are judged by the same rule before the statement compiles. An accepted spelling is read as its boolean, and anything else the rule refuses is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. The native strategy now answers what the engine-aggregate strategy answers. Before, `{ flag: 'true' }` counted no rows on SQLite, `{ flag: { $ne: 'true' } }` counted every row, and `{ flag: 'yes' }` answered 200. -- **What you may notice.** A policy or analytics filter that compared a boolean field with a value outside the accepted set now refuses instead of answering. Write `true` / `false`. A policy `record.flag == 1` now admits writing a `true` row, which its read already showed. -- **Unchanged.** A boolean literal, a column that is not boolean, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one). diff --git a/.changeset/21379-position-address-readers.md b/.changeset/21379-position-address-readers.md deleted file mode 100644 index 72fa9e49b2e..00000000000 --- a/.changeset/21379-position-address-readers.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -A holder of a position whose approval slot reads `position:` now decides it from the stock console, sees `can_act` on it, and keeps sight of it after deciding; a reviewer named by a `user` approver authored as an email does too - -Clause-②: no - -A request whose approver position nobody held when it opened keeps the literal `position:` slot. After the position is staffed, its holder found the request in "My Pending", but `viewer.can_act` was `false`, an approve with no `actorId` (what the console's approve action sends) or with `role:` (the deprecated pre-rename spelling the console uses) answered 403, only naming `position:` decided it, and `GET /api/v1/approvals/requests/:id` then answered 404 to the holder who had just decided it. A `user` approver authored as an email had the same shape: its reviewer saw neither the request nor `can_act`, and only naming the email decided it. - -Every place the approvals service compares a slot with the caller now reads the caller's acting addresses, the set its decision routes already admitted: the user id, the email the caller's own account carries, and both spellings of each position on the caller's server-resolved context. - -- **Decisions** (approve, reject, send back, reassign, request info, comment): with no `actorId`, the caller takes the first pending slot keyed by one of those addresses, their user id first. A named `role:` or `position:` takes that position's slot under either spelling. Nobody new may decide: a user who holds another position is still refused with 403. -- **What is recorded:** `sys_approval_action.actor_id` holds the slot the action took, in that slot's stored spelling. That is what naming the slot always recorded, and the multi-approver tally counts approvals by matching it against the slate. -- **`viewer.can_act`** is computed by the same slot test the decision routes run with no `actorId`, so it is `true` exactly when such an approve would be admitted as a slot holder. -- **Visibility:** the participant gate counts a current approver by the email half too. "Already acted" is counted by the same addresses, so a request decided under `position:` stays visible to whoever holds that position. - -An admin who holds the routed position now decides it as a slot holder (`via_override: false`, one vote in a multi-approver tally), exactly as when they named the slot; an admin who holds no slot is unchanged. diff --git a/.changeset/21382-objectql-boolean-comparand-non-string.md b/.changeset/21382-objectql-boolean-comparand-non-string.md deleted file mode 100644 index 5e7d4d0cb9d..00000000000 --- a/.changeset/21382-objectql-boolean-comparand-non-string.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql)!: a number other than `1` / `0`, a `Date` or an array compared against a boolean field is refused with `INVALID_FILTER` / 400 at `where`, a per-aggregation `filter` and `having`, instead of a PostgreSQL 500 or an empty 200 - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what a filter may compare a declared `boolean` or `toggle` field with, at every filter position and through every door that reaches the engine's filter walk (`engine.find` / `findOne` / `count` / `aggregate` / `update` / `delete`, and every spelling the data API hands it). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type of this package changes; the rule is `@objectstack/spec/data`'s `booleanComparandDoorVerdict`, whose own changeset lists what moved there. - -**What was answered before.** A non-string comparand outside the accepted set reached the driver as written. Measured on two rows (one `true`, one `false`) through `engine.find` / `engine.aggregate`, on InMemoryDriver, SqlDriver over SQLite and SqlDriver over PostgreSQL 16: - -| position | comparand | before: memory · SQLite · PostgreSQL | now, on all three | -|:--|:--|:--|:--| -| `where` | implicit / `$eq` `2`, `-1`, `0.5`, a `Date` | no row · no row · `DATABASE_ERROR` (500) | `INVALID_FILTER` / 400 | -| `where` | `$ne` the same | both rows · both rows · 500 | `INVALID_FILTER` / 400 | -| `where` | a `$in` member `2` or a `Date` | the other members' rows · the same · 500 | `INVALID_FILTER` / 400 | -| `where` | a `$in` member `[true]` | the other members' rows (200) · a driver 400 · a driver 400 | `INVALID_FILTER` / 400, in one set of words | -| per-aggregation `filter` / `having` | any of the above | count 0 and no group (every row and group under `$ne`), a `$in` member ignored, on all three | `INVALID_FILTER` / 400 | -| all three positions | `true`, `1`, `"true"` (the controls) | the true row, count 1, the true group | the same | - -**The remedy.** Write `true` or `false` (or `1` / `0`). To match either value, use `$in`, each member a boolean. Compare a `Date` with a date or datetime field. - -**Unchanged.** `true` / `false` pass as written, the accepted spellings (`1` / `0`, `"1"` / `"0"`, `"true"` / `"false"`) narrow as before, and any other string is refused in the same words as before. `null` (the null test) and the flag operators answer as before. A value outside the accepted comparand types (`undefined`, a plain object, a `Map`) keeps the comparand-type door's own refusal and words. A filter on a `formula` field is still refused one step earlier. Driver-direct callers that never pass through the engine keep each driver's native binding. diff --git a/.changeset/21382-spec-boolean-comparand-non-string.md b/.changeset/21382-spec-boolean-comparand-non-string.md deleted file mode 100644 index 0764408fefd..00000000000 --- a/.changeset/21382-spec-boolean-comparand-non-string.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -fix(spec)!: the boolean-comparand verdict refuses a number other than `1` / `0`, a `Date` and an array compared against a boolean field, the same as a string that is not a boolean - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what a filter may compare a boolean field with. `booleanComparandDoorVerdict`, the published verdict the engine's boolean-comparand arm consumes, judged strings only; it now also answers `door-refusal` (`INVALID_FILTER` / 400) for a number other than `1` / `0`, a `Date` and an array, so the engine refuses them before any read, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. The accepted set is unchanged: `true`, `false`, `1`, `0`, `"true"`, `"false"`, `"1"` and `"0"`, and `null` is still the null test. - -What moves in `@objectstack/spec/data`: - -- `booleanComparandDoorVerdict(field, comparand)` answers `door-refusal` with a new `form` for each non-string: `number`, `date` or `array`. `readBooleanComparand` reads a `bigint` as the number it names, so `1n` / `0n` narrow like `1` / `0` and any other `bigint` is refused as a number. That is the number the comparand-type door rewrites a `bigint` to, so the answer no longer depends on which door met it first. -- Three additive exports: `NON_BOOLEAN_VALUE_FORMS` (`number`, `date`, `array`) and the types `NonBooleanValueForm` and `NonBooleanComparandForm`. The refusal's `form` (on `BooleanComparandDoorVerdict`, `BooleanComparandRefusalSite` and `BooleanComparandDoorRefusalCase`) widens from `NonBooleanStringForm` to `NonBooleanComparandForm`, and the refusal site's `value` now carries a non-string. A consumer that switches over `form` exhaustively gains three cases. -- `booleanComparandRefusalMessage` gains one clause per non-string form, and renders a `Date` as `Date(ISO)` and a non-finite number by name instead of as JSON. -- `BOOLEAN_COMPARAND_DOOR_CASES` gains a `value` group: `-1` at `$ne` on every judged field, and `2`, a `Date` and an array at every judged position of `f_boolean` (no array at the equality slots, where the comparand-shape door refuses one first), plus the passing rows beside them. The `2` / `-1` reading rows, and a new `0.5` row, now derive refusals. - -FROM a number other than `1` / `0` (`2`, `-1`, `0.5`), a `Date`, or an array where one value belongs (a scalar operator's comparand, or a member of `$in` / `$nin` / `$between`), compared against a `boolean` or `toggle` field (or a groupBy / `min` / `max` column of one in `having`) → TO `INVALID_FILTER` / 400, naming the field, its declared type, the comparand, its position and what is wrong with it. The fix is one line: send `true` or `false`, or `1` / `0`; to match either value use `$in`, each member a boolean. - -**Unchanged.** Every string the verdict accepted or refused is answered as before, in the same words. A boolean, `null` and the flag operators (`$null`, `$exists`, `$empty`) pass. A value outside the accepted comparand types (`undefined`, a plain object, a `Map`) keeps the comparand-type door's own refusal and words, and a `{ $field }` reference is not judged. A comparand against a field that is not boolean is not this verdict's subject. diff --git a/.changeset/21385-driver-sql-refusal-log-lines-cut.md b/.changeset/21385-driver-sql-refusal-log-lines-cut.md deleted file mode 100644 index dda264659d6..00000000000 --- a/.changeset/21385-driver-sql-refusal-log-lines-cut.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -fix(driver-sql): the driver's own refusal log lines no longer write the statement or the values bound into it - -Clause-②: no - -Five warning lines wrote the dialect's message to the server log as it came back. That message opens with the statement, with its bound values inlined on SQLite and MySQL, and on PostgreSQL a value-bearing diagnostic carries the value itself. The lines are the read terminal, the raw-statement terminal, and the refusals for a WHERE, a groupBy or aggregation, and a listed-distinct column the backend could not resolve. Each line now writes the dialect's text through the driver-fault redaction in `@objectstack/types`, the cut the engine applies at its boundary. - -- **What stays on each line.** Its code, the class of fault it reports, the object and column it names, the dialect's error code where the line printed one, and the dialect's own diagnostic. -- **What goes.** The statement and the values bound or inlined into it, replaced by `[statement and bound values redacted]`, and the value slot of each diagnostic the redaction's templates own, replaced by `[value redacted]`. The raw-statement line no longer writes the statement it was sent, which also holds for `@objectstack/driver-turso`'s remote transport, whose refusals reach the same line. The two debug lines the read terminal writes inside a pre-DDL question, or for a table whose DDL the driver deferred, take the same cut. -- **The envelopes.** The code, status, `cause` and withheld text of every refusal are unchanged. Two composed messages, the read terminal's `DATABASE_ERROR` and the raw-statement terminal's, said the statement was written to the server log; they now say the diagnostic was written with the statement and its bound values cut. -- **What changes for an operator.** A log reader that took the statement or a bound value from these lines now finds the marker where the dialect's text carried them, and nothing where the raw-statement line wrote the sent statement on its own. The diagnostic, the codes and the named object and column are where they were. diff --git a/.changeset/21385-objectql-redaction-from-types.md b/.changeset/21385-objectql-redaction-from-types.md deleted file mode 100644 index 8dbf04dc245..00000000000 --- a/.changeset/21385-objectql-redaction-from-types.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -refactor(objectql): the engine takes its driver-fault redaction from `@objectstack/types` - -Clause-②: no - -The redaction the engine applies to its write-path log lines, at its boundary, at the raw-statement door and in the lifecycle sweep now lives in `@objectstack/types`, so `@objectstack/driver-sql` calls the same cut. The engine calls it as before, with the same arguments, and its answers are unchanged. None of the moved names was exported from `@objectstack/objectql`'s entries, so its public surface does not move. diff --git a/.changeset/21385-types-driver-fault-redaction-home.md b/.changeset/21385-types-driver-fault-redaction-home.md deleted file mode 100644 index 6b925a73f37..00000000000 --- a/.changeset/21385-types-driver-fault-redaction-home.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/types': minor ---- - -feat(types): the driver-fault redaction is exported from types, so a driver's own log lines take the same cut the engine applies - -Clause-②: no - -- **New exports.** `redactBoundStatement`, `redactStatementFromMessage`, `redactPropagatedDriverFault` and the `DriverFaultOrigin` type are exported from `@objectstack/types`, by name. They moved here from `@objectstack/objectql`, which never exported them from its entries. The cut is unchanged by the move: the same split, the same structural cut at the separator, the same value templates and the same property rules. -- **Why here.** `@objectstack/driver-sql`, `@objectstack/objectql` and `@objectstack/core` all depend on this package, and `operatorFacingErrorText` lives in it, so this is the lowest package all of them can import the cut from. The module imports only this package's own leak predicate, which is unchanged. -- **One widening, on the log face.** `redactStatementFromMessage` takes an optional second argument, `{ statementSent: true }`. With it the cut runs without asking the shared leak predicate, as `redactPropagatedDriverFault` already did with the same flag. Without it the function answers exactly as before. -- **Why minor.** The package gains four exported names, and `redactStatementFromMessage` gains the optional parameter above. No existing export of `@objectstack/types` changes. diff --git a/.changeset/21387-retire-role-arm.md b/.changeset/21387-retire-role-arm.md deleted file mode 100644 index 7e3e89263b4..00000000000 --- a/.changeset/21387-retire-role-arm.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@objectstack/plugin-approvals": minor -"@objectstack/spec": patch ---- - -fix(plugin-approvals)!: `role:` is no longer a position address, and the deprecated `role` approver type stops writing `role:` slots (ADR-0090 D3) - -Clause-②: no (narrowing) - -`position:` is now the one spelling of a position address. ADR-0090 D3 retired the word `role` with no alias window; the approvals service still read `role:` as a second spelling of the same position everywhere it compares a slot with the caller ("My Pending", the participant gate, `viewer.can_act`, and the slot test of every decision). The stock console now sends `position:`, so that arm is gone. - -**FROM → TO.** FROM `role:` → TO `position:`, wherever a caller names a position: the `approverId` filter of `GET /api/v1/approvals/requests`, and the `actorId` of approve, reject, send back, reassign, request info and comment. A `role:` ask now matches only a slot stored under that exact spelling, and a `role:` actor is refused with 403 `FORBIDDEN` ("cannot act as …"). - -**The writer.** An approver authored with the deprecated type `{ type: 'role', value: … }` already resolved as `org_membership_level` (the org-membership tier: owner, admin, member). When that lookup found no one, the request's fallback slot kept the authored spelling, `role:`, and a holder of a position with the same name decided it through the `role:` arm. That fallback now writes the canonical `org_membership_level:`, so no path writes a `role:` slot. A stored slot is never rewritten. - -Two classes of pending request are now decided only by an admin override: - -- a request a 15.x-era release opened, whose slot is stored as `role:`; -- a new request opened from a flow that still authors `{ type: 'role', value: '' }` and whose membership-tier lookup finds no one (its slot is `org_membership_level:`). - -**Author's one-line fix:** write `{ type: 'position', value: '' }`. `os lint` already reports the old form as `approval-approver-not-membership-tier` or `approval-approver-type-deprecated`. - -**Admin's one-line handling, both classes:** a platform admin (`admin_full_access`) or a tenant admin of the request's organization approves or rejects it (`POST /api/v1/approvals/requests/:id/approve` or `/reject`; recorded with `via_override: true`, and the flow run resumes), or reassigns it to the position's holder (`POST /api/v1/approvals/requests/:id/reassign` with `{ "to": "" }`), who then decides it normally. - - diff --git a/.changeset/21388-activity-withheld-update-row.md b/.changeset/21388-activity-withheld-update-row.md deleted file mode 100644 index cb77a15be71..00000000000 --- a/.changeset/21388-activity-withheld-update-row.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -fix(plugin-audit): an activity row recording an update whose every changed field the reader is withheld is no longer served to that reader, on any listing face - -Clause-②: no - -A `sys_activity` row's recorded change (`metadata.old` / `metadata.new`) is narrowed key by key for each reader, through the security service's served-fields answer. An update whose every changed field the reader is withheld still reached that reader as a row with an empty change, and its summary, actor and timestamp said that the record changed, and when. An org member holding object-level `sys_activity` read was served that row for each sign-in stamp on a colleague's identity record (`last_login_at`), and for each failed-sign-in counter bump, lockout, password-change stamp and MFA-required stamp. - -Such a row is now withheld from that reader as a row: - -- **What counts as one.** An update row (its stored change has both an `old` and a `new` side) whose stored change had at least one key, where the reader is served none of those keys. The keys are read from the STORED change, not the redacted one. -- **What is unaffected.** A create or a delete keeps its row. A row whose stored change is empty on both sides (an update that touched only `internal` fields) is unaffected. A mixed update keeps its row, with the served keys only. A reader served every field (an administrator) still reads every row with its change, within the pre-scan's bound. A system-context read is not narrowed. -- **Every face agrees.** The rule is a WHERE built from a system-context pre-scan on `find`, `findOne`, `count` and `aggregate`. So a list's `total`, its pages, a by-id read (`404`) and a grouped count agree with the rows served. A pre-scan that reaches its 2,000-row bound answers a broad read from the rows it judged, for every reader, administrators included, and logs a warning. The remedy is to scope the query by `object_name` and `record_id`. - -No migration: no key, export or config changes. A reader the security service gives no answer for (no security plugin wired) is not narrowed, as before. diff --git a/.changeset/21391-one-shot-boot-read-only.md b/.changeset/21391-one-shot-boot-read-only.md deleted file mode 100644 index a201088b6f0..00000000000 --- a/.changeset/21391-one-shot-boot-read-only.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/cli': minor -'@objectstack/runtime': minor ---- - -The CLI's one-shot commands no longer write to the database as a side effect of booting. No `os migrate *`, `os meta resync`, `os secret orphans` or `os storage orphans` run loads the app's inline seed data, apply and delete modes included, and every mode that writes nothing now boots read-only. - -Clause-②: yes (narrowing) - - - -**BREAKING** — a no-write run of `os migrate value-shapes`, `os migrate recorded-by`, `os migrate resume`, `os secret orphans` or `os storage orphans` at a database that lacks a table it reads now exits 1, where it used to exit 0. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What was wrong.** Eight commands booted the full data stack in a mode their documentation says writes nothing: `os migrate value-shapes` (scan), `summary-nulls`, `files-to-references` and `recorded-by` (dry run), `os migrate resume` (list), `os secret orphans` and `os storage orphans` (report), and `os meta resync` without `--yes`. That boot ran schema sync and the app's inline seed loader. The seed loader upserts every seeded row, so each run bumped `updated_at`, stamped `organization_id` on seeded rows that had none, and put an operator's edit to a seeded row back to the seed's value. On `examples/app-crm` that was all 28 seeded rows on every run. On a database behind the app's schema, the boot also added columns and created tables. The apply and delete modes ran the same seed loader alongside the write the operator confirmed. - -**What changes for an operator.** - -- Every mode that writes nothing boots the way `os migrate plan` does: the schema sync is held back, no seed rows are written, and a SQLite file that does not exist is not created. The database is left byte-identical, and the report is the same as before. -- No one-shot CLI boot loads the app's inline seed data. `--apply`, `--delete`, `os migrate resume --run` and `os meta resync --yes` write what they report and nothing else. Seeding stays with `os dev` and `os serve`. -- The deferred schema sync now covers every SQL datasource the boot connects, not only the default one. `os migrate plan` lists a second datasource's pending tables, and `os migrate apply` creates them after you confirm. -- One edge changes: a no-write run pointed at a database that lacks a table it reads (a SQLite file that does not exist, a database that was never booted, or the wrong `--database-url`) refuses and exits 1 instead of creating the table and reporting nothing. Point `--database-url` at the deployment's database, or boot the deployment once first. `os secret orphans --json` answers that refusal with `"error": "scan_failed"`. -- `os migrate value-shapes --json` prints one JSON document when the scan fails its gate. It used to print a second one, `{"error":"EEXIT: 1"}`. - -**For embedders of `@objectstack/runtime`.** `createStandaloneStack` accepts `armLifecycleSweep` (default `true`). With `false`, the ADR-0057 lifecycle sweep (rotation, retention reaping, archiving and the dangling-reference audit that rides its clock) is never armed on that boot, and an explicit `sweep()` call on it returns an empty report. The CLI passes `false` on every one-shot boot. diff --git a/.changeset/21397-null-ordering-message-faces.md b/.changeset/21397-null-ordering-message-faces.md deleted file mode 100644 index e96c007002b..00000000000 --- a/.changeset/21397-null-ordering-message-faces.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -fix(spec): the null ordering-comparand refusals name only evaluation faces that exist, and say only what was measured - -Clause-②: no - -`FieldOperatorsSchema` and `ComparisonOperatorSchema` refuse a `null` comparand of `$gt` / `$gte` / -`$lt` / `$lte` with a pointed message. Its example of the evaluation faces disagreeing named -driver-memory's reference matcher, which has been deleted, so an author or agent reading the -refusal went looking for a face that no longer exists. The example now names two faces that exist -and were measured to disagree: driver-memory's query path reads a stored `null` as equal to the -comparand, so `{"$gte": null}` admits that row, while driver-sql compares against SQL `NULL` and -admits no row. - -That refusal and its runtime twin, the `parseFilterAST` refusal for the same comparand -(`Operator "$gt" on field "…" does not accept a null comparand …`), both said "no two evaluation -faces agree" on what an ordering against `null` matches. Measured, two faces do agree (driver-sql -and formula both admit no row), so both now say "the evaluation faces do not agree". - -Text only: each message's first sentence, its prescription (`{"$eq": null}` / `{"$ne": null}`), the -schema door's ruling sentence and the runtime door's "NOT applied" sentence are unchanged, and both -doors accept and refuse exactly the same filters. A client or log filter that matches the old -wording needs the new spelling. diff --git a/.changeset/21405-permission-denied-status.md b/.changeset/21405-permission-denied-status.md deleted file mode 100644 index d25380cc13d..00000000000 --- a/.changeset/21405-permission-denied-status.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -`PermissionDeniedError` declares its 403 as `status` as well as `statusCode`, so a permission refusal answers 403 at every door (#21405). - -Clause-②: no - -The class declared `statusCode` alone, unlike every other error class in `errors.ts`, and a door that reads `status` alone derived no status from it. On a showcase boot, a plain member's `POST /api/v1/share-links` on a record they cannot read answered `500` with code `PERMISSION_DENIED` through `plugin-sharing`'s route door, while the runtime dispatcher's `/share-links` domain answered the same refusal with `403`. Both doors now answer `403 PERMISSION_DENIED`. The code, the message and `statusCode` are unchanged. diff --git a/.changeset/21409-analytics-row-wildcard-count-only.md b/.changeset/21409-analytics-row-wildcard-count-only.md deleted file mode 100644 index 6eb81a6057a..00000000000 --- a/.changeset/21409-analytics-row-wildcard-count-only.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: the analytics row wildcard `'*'` is admitted only where a `count` consumes it — a cube or dataset measure over `'*'` under any other aggregate, and a cube dimension over `'*'`, are refused at parse (#21409) - -Clause-②: no (narrowing) - -**BREAKING** — shipped as `minor` under the launch-window convention -(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by -this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, -never by the level). - -`'*'` is the row wildcard: what a `count` aggregates (`COUNT(*)`), reading no -field value. It is now admitted in exactly one place, a measure that counts: - -- `MetricSchema.sql` — a cube measure's `sql` — admits `'*'` under - `type: 'count'` only; under any other `type` it is refused at `sql` - (code `custom`). -- `DatasetMeasureSchema.field` — an ADR-0021 dataset measure's `field` — admits - `'*'` under `aggregate: 'count'` only; under any other aggregate, or on a - measure with no aggregate (a `derived` one), it is refused at `field` - (code `custom`). A count may still omit `field`. -- `DimensionSchema.sql` — a cube dimension's `sql` — never admits `'*'` - (code `invalid_format`): it takes the column path without the wildcard arm, - the pattern a dataset dimension's `field` already takes. - -Each refusal names the slot and the aggregate the author wrote, and prescribes -the two ways out: a `count`, or a column. A column or a relationship path parses -byte-identically to before on every slot, and so does a `count` over `'*'`. - -Why: no aggregate but `count` has a column to read over `'*'`, and a dimension -has no aggregate at all, yet the contract admitted the wildcard on any measure -and on a cube dimension, and the analytics strategies sent it to the database as -written. Measured at `POST /api/v1/analytics/dataset/query` over a real SQLite -driver, on the native-SQL and the ObjectQL strategy alike: a dataset measure -aggregating `'*'` under `sum`, `avg`, `min`, `max` or `count_distinct` answered -`500 DATABASE_ERROR`. A dataset measure compiles to the cube measure it names -verbatim, so the same reading covers an authored cube measure. Such a member -never produced an answer, so no working document changes meaning: the failure -moves from the query to the authoring parse. The two measure slots ask ONE -shared predicate; the rule is cross-field (the slot and its aggregate), so it is -a refinement, which the published JSON Schema cannot carry — both sites are -declared in `dropped-refinements.baseline.json`. The dimension half is a -`pattern`, so `json-schema/**` states it. - -## FROM → TO - -``` -FROM { name: 'deal_metrics', label: 'Deal Metrics', object: 'deal', - dimensions: [{ name: 'stage', field: 'stage' }], - measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] } - -> DatasetSchema.parse accepted it; a dataset query selecting `deals` - answered 500 DATABASE_ERROR -TO -> DatasetSchema.parse throws a ZodError at measures.0.field (custom): - `measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. … - defineStack({ datasets }) refuses it at datasets.N.measures.0.field (422 - STACK_SCHEMA_INVALID), and POST /api/v1/analytics/dataset/query answers - 400 VALIDATION_FAILED for an inline or a saved copy - - measures: [{ name: 'deals', aggregate: 'count' }] // a row count - measures: [{ name: 'deal_value', aggregate: 'sum', field: 'amount' }] // an aggregate of a column - -FROM defineCube({ name: 'deals', sql: 'deal', - measures: { total: { label: 'Total', type: 'sum', sql: '*' } }, - dimensions: { everything: { label: 'All', type: 'string', sql: '*' } } }) -TO -> refused at measures.total.sql (custom) and dimensions.everything.sql (invalid_format) - - measures: { total: { label: 'Total', type: 'sum', sql: 'amount' } }, - dimensions: { stage: { label: 'Stage', type: 'string', sql: 'stage' } } -``` - -**The one-line fix:** parse each cube and dataset; every refusal at `…sql` / -`…field` naming `'*'` is one member to change — declare a `count` to count rows, -or name the column the measure aggregates (a dimension names the column it -groups by). On a `derived` dataset measure, delete `field`: nothing read it. -There is no mechanical rewrite: `os migrate meta` rewrites nothing for it, and -lists the entry `analytics-row-wildcard-outside-count-refused` as a manual -change that requires your judgment. - -**What a stored document meets.** A metadata read still serves it as stored, -with the refusal on its read diagnostics (`_diagnostics`), and a re-save through -the metadata write door is refused at the slot. `POST -/api/v1/analytics/dataset/query` parses every dataset it is handed, inline or -saved, so a stored dataset carrying such a measure answers `400 -VALIDATION_FAILED` at `measures.N.field` on every query — including a query that -selects only its other measures, which used to answer — until the member is -fixed: it fails closed. An authored cube reaches the analytics runtime through -the stack definition, whose parse refuses it. - -## The kit - -- **Schema.** `data/analytics-column-reference.ts` (not published API) declares - the predicate `rowWildcardOutsideCount` and its refusal once; `MetricSchema` - and `DatasetMeasureSchema` call both from a refinement, and - `DimensionSchema.sql` takes `ANALYTICS_COLUMN_PATH`. No export, key or enum - member changes, so the api-surface, authorable-surface and JSON-schema - manifest ratchets are unchanged. -- **ADR-0087.** D3 entry `analytics-row-wildcard-outside-count-refused`. No D2 - conversion: rewriting to `count` would change the figure the author asked for, - and only the author can name the column. No `RETIRED_KEYS_BY_MAJOR` row. -- **Dropped refinements.** `data/Metric` and `ui/DatasetMeasure` gain their root - site, and every published schema embedding them gains the embedded site. -- **Liveness.** `analytics_cube` `measures.sql` / `dimensions.sql` and `dataset` - `measures.field` stay `live`, re-verified, their notes re-pointed here. -- **Docs.** The `ui/dataset` reference page is regenerated. -- **Runtime.** Unchanged. - -## Reach, measured - -- This repository: no example, platform object, doc, skill, script or test - fixture authors `'*'` outside a `count` at the three slots (`git grep` of every - `field` / `sql` value spelled `'*'`, 173 hits, each read in its enclosing - object: 154 under a `count`, the rest QueryAST aggregations, comments and - strategy-level literals). One spec pin admitted `'*'` on a cube dimension; it - now pins the refusal. -- objectui at the pinned `.objectui-sha`: zero `field` / `sql` values spelled - `'*'` (lit controls: 51 `aggregate: 'sum'`, 438 `field: 'amount'`). -- Out-of-repo authored metadata: NOT MEASURED. - - diff --git a/.changeset/21411-approval-actor-person.md b/.changeset/21411-approval-actor-person.md deleted file mode 100644 index be4e1b85e0e..00000000000 --- a/.changeset/21411-approval-actor-person.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -An approval action now records the user who took it in `sys_approval_action.actor_id`, and the pending-approver slot it was taken as in a new `acted_as` column; rows stored before this move their slot out of `actor_id` at the next boot - -Clause-②: no - -`actor_id` is a lookup to `sys_user`, so under ADR-0118 D1 it holds a user id or nothing. A slot-gated action used to record the slot it took there instead: a `position:` literal for a position staffed after the request opened, or an email for a `user` approver authored as one. On those decisions no record named the person who decided. The audit ledger and activity rows the write produces carry no user, so the attribution was lost, and every join or report on the lookup silently dropped the row. - -**This supersedes the "What is recorded" sentence of the unreleased `21379-position-address-readers` changeset**, which says `actor_id` holds the slot. From this release it holds the person. - -- **What is recorded.** - - `actor_id` is the user the request's context vouches for: the signed-in caller, whatever address they named. - - `acted_as` is the slot the action took, in the slot's stored spelling (a user id, an email, or `position:`). It is empty on actions no slot admitted: the submitter's own actions, system actions, and an admin override, which `via_override` still marks. - - An emailed action link records the one account that carries the token's email. If no account carries it, the link records no person. - - The SLA sweep keeps its reserved `system:sla` actor for now. -- **What reads it.** - - The multi-approver tally and `decision_progress` count `acted_as`. - - A participant who already acted keeps sight of a request by either of two facts: `actor_id` is their user id, or `acted_as` is a slot they act under (so a decision taken as `position:` stays visible to that position's holders). - - Nothing compares a slot with `actor_id` any more. - - The action log (`GET /api/v1/approvals/requests/:id/actions`, `listActions`) returns `acted_as` beside `actor_id` and `actor_name`, filling the `ApprovalActionRow.acted_as` member `@objectstack/spec` declares. It is omitted when the action took no slot, or when no stored record kept the slot. -- **Stored rows.** A repair runs on every boot and is idempotent. - - Pass 1: a row whose `actor_id` still holds a slot address gets `acted_as` set to it and `actor_id` cleared. No stored record names who decided it, so it shows the slot and no person. - - Pass 2: the approve votes a still-pending request's tally counts get their `acted_as`, so in-flight `unanimous`, `quorum` and `per_group` requests keep the approvals they already collected. - - A failure is logged at error level and retried at the next boot. -- **For a report or integration that read `actor_id` as the slot:** read `acted_as` instead. `actor_id` now always joins to `sys_user`. diff --git a/.changeset/21412-metadata-protocol-save-door-container-name.md b/.changeset/21412-metadata-protocol-save-door-container-name.md deleted file mode 100644 index 21e5e137d12..00000000000 --- a/.changeset/21412-metadata-protocol-save-door-container-name.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -The runtime save door refuses a view container whose own `name` disagrees with the name it is saved under - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the ObjectQL boot loop's refusal of the same divergence shipped with. - -**What was accepted before.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, accepted an aggregated view container (`list` / `form` / `listViews` / `formViews`) whose body carried a `name` different from the name it was saved under. It stored the row under the save name and registered the container under the body's `name`, so one document answered under two names. The source registrars (the ObjectQL boot loop and the artifact/HMR loader) and `os validate` already refused a container whose `name` disagrees with the key they file it under. - -**What is refused now.** That body, with `VALIDATION_ERROR` / 400, before anything is stored or registered, through the same judge the source registrars call (`@objectstack/metadata/view-container-name`). The key judged here is the save name: a container saved under a name other than the object it binds to still saves, and so does the body the door stores for it when it is read and sent back. - -**The fix.** Drop the body's `name` (the door stamps the save name), or set it to the name the container is saved under. - -A standalone view record (`viewKind`) and every other metadata type are judged too, by the same release's every-type refusal at the save, restore and publish doors (its own entry). diff --git a/.changeset/21412-metadata-view-container-name-judge.md b/.changeset/21412-metadata-view-container-name-judge.md deleted file mode 100644 index 5d52bb952d2..00000000000 --- a/.changeset/21412-metadata-view-container-name-judge.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/metadata': minor ---- - -One judge for a view container's own `name` at every door that files a container: the new `@objectstack/metadata/view-container-name` entry - -Clause-②: yes - -- New subpath `@objectstack/metadata/view-container-name`. It exports `viewContainerNameRefusal(container, sourceLabel, ownerId)`, the source registrars' entry, whose key is the object the container binds to (its own `object`, else `list.data.object` / `form.data.object`). It returns a `VALIDATION_ERROR` / 400 refusal for an aggregated view container whose own `name` is set and differs from that key, and `undefined` otherwise; a container with no `name`, and a standalone view record (`viewKind`), are not judged by it. The subpath also exports `savedItemNameRefusal(type, item, saveName, door)`, the runtime write doors' entry, which judges every metadata type against the name the row is written under, and the `ViewContainerNameRefusal` type both entries return. -- The artifact/HMR loader's container branch now refuses such a container through the judge, before it files anything. What it refuses and the envelope are unchanged (`VALIDATION_ERROR` / 400). The message is now the judge's, the words the ObjectQL boot loop and `os validate` print, where it was the generic `IMetadataService.register` contract's. diff --git a/.changeset/21412-objectql-view-container-name-reexport.md b/.changeset/21412-objectql-view-container-name-reexport.md deleted file mode 100644 index c955c30b144..00000000000 --- a/.changeset/21412-objectql-view-container-name-reexport.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -`viewContainerNameRefusal` is now re-exported from `@objectstack/metadata/view-container-name` - -Clause-②: no - -The divergent view-container `name` judge moved to `@objectstack/metadata`, the one layer the boot loop, the artifact/HMR loader and the runtime save door all depend on, so all three call one judge. `@objectstack/objectql` keeps the `viewContainerNameRefusal` export, its signature and the `ViewContainerNameRefusal` type. The boot loop's refusal and the words it and `os validate` print are unchanged, byte for byte. diff --git a/.changeset/21412-spec-view-container-name-comment.md b/.changeset/21412-spec-view-container-name-comment.md deleted file mode 100644 index 25442ac709c..00000000000 --- a/.changeset/21412-spec-view-container-name-comment.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The comment above `ViewSchema`'s `guidance:` states who writes a view container's `name`, and the rule every door applies to it - -Clause-②: no - -`src/ui/view.zod.ts` ships as source, and the comment also ships in the `ui` JavaScript output. It used to say that `saveMetaItem` sends a container's `name`, that artifact-shipped containers do, and that the validation sweep injects it. It now says the metadata door's own stamp (`normalizeViewMetadata`) is the only platform writer of the key. Artifact-shipped containers carry none, and the sweep passes its name as the request name. It also states the rule: when an authored `name` is set, it must equal the key the door files the container under, or the door refuses it. ⛔ No schema, parse, export or accept-set change. diff --git a/.changeset/21417-analytics-faces-one-lowering.md b/.changeset/21417-analytics-faces-one-lowering.md deleted file mode 100644 index 2df22edbbb2..00000000000 --- a/.changeset/21417-analytics-faces-one-lowering.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: the analytics read scope, the `where` tree and the draft preview take the shared lowering's bound and NULL guards; their own whole-day and NULL-polarity copies are deleted (ADR-0053 D-D1 items 7 to 9) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows the rows the native analytics strategy and the draft preview (`queryDataset` with `previewDrafts`) select for a bare-day upper bound on a column the host declares as neither `datetime` nor `date` — a `text` column, for example. It ships as `minor` under the launch-window convention for answer narrowings. No export, published type, accepted input or error code changes. - -**What is deleted.** The native SQL strategy no longer reads a bare `YYYY-MM-DD` `$lte`, a `$between` maximum or an explicit `dateRange` end as "through that whole day" on every column, and no longer drops such a bound on `9999-12-31` whatever the column holds. The whole-day rule is applied once, by the shared `lowerFilterCondition` (`@objectstack/spec/data`), with the column's declared type, the reader the plugin already wires from the engine's registry (`sourceFieldMeta`): a declared `datetime` column keeps the whole day, and every other declared column is compared as written, as the engine compares it. The `/analytics/sql` echo renders the same lowering. - -**The native face now agrees with the engine.** Measured through `AnalyticsService.query` (what `POST /api/v1/analytics/query` relays) in the plugin's own composition, on SQLite and on PostgreSQL 16, over a `text` column `note` holding `'2026-07-27'`, `'2026-07-28'`, `'2026-07-28 late'`, `'n'` and no value: - -- `{ note: { $lte: '9999-12-31' } }` counted every row with a value (4). It now counts 3, the rows the engine's `find` returns: `'n'` sorts above `'9999-12-31'`. -- `{ note: { $lte: '2026-07-28' } }` counted 3, the `'2026-07-28 late'` row included. It now counts 2. -- `$between ['2026-07-28', '2026-07-28']` and a `dateRange` window of the same day counted 2; they now count 1. Their negation through `$not` gains the row the bound lost. - -On a declared `datetime` or `date` column every answer is unchanged, on both strategies. - -**A host with no typed reader** (a strategy context with no `declaredFieldType` hook, or an `AnalyticsService` built without `sourceFieldMeta`) reads every column type-blind, as ADR-0053 D-D1 item 7 prescribes for a seam that cannot read declarations: its native answers do not move. Pass `sourceFieldMeta` (the README shows how) to get the engine's answer on a non-temporal column. - -**The `/analytics/sql` echo.** A `dateRange` window on a declared `date` column now prints the inclusive `<=` the engine runs, where it printed `<` the next day; on a column the host names no type for, it prints the bound the ObjectQL strategy hands the engine, as written. A preset window that stops before its end (`today`, `this_month`, …) now prints `<` its end instant with that instant bound, where it printed `<=` with no value bound. The NULL guards print once where they printed two or three nested copies of the same guard; every row set is unchanged. - -**The draft preview now agrees with the engine too.** `queryDataset` with `previewDrafts` evaluates drafted seed rows in memory; it kept its own whole-day copy, read on every column. It now hands the evaluator the drafted object's declared types (`sourceFieldMeta`), and the shared lowering applies the rule with them: a declared `datetime` column keeps the whole day, any other declared column is compared as written, and a column the host names no type for is read type-blind (ADR-0053 D-D1 item 7). Measured through the plugin's own composition over the same rows, five of the preview's `note` cells moved, each onto the engine's answer: `$lte` a day 3 to 2, `$between` and a window of one day 2 to 1, a window to `9999-12-31` 3 to 2, and the `$not` gains the row. Its `$lte` and `$between` to `9999-12-31` already gave the engine's answer and are unchanged. Every `datetime` and `date` cell is unchanged. - -- A preview window is now the `{ $gte, $lte }` pair the ObjectQL strategy hands the engine, matched like the same bounds in a `where`. Its end used to be read with a `'~'` suffix ("that instant and its own sub-values"), a reading no other face gives. Measured on a `datetime` column over SQLite, a canonical end (`…T10:00:00.000Z`) answers as before and as the engine. An end spelled shorter than the stored value is compared as text, as the preview's `where` already compared it: an end of `…T10:00` or `…T10:00:00` now leaves out the row stored at exactly that instant (the engine keeps it), and leaves out the rows inside that minute or second (the engine leaves them out too; the old reading kept them). Write a window end in full (`2026-07-28T10:00:00.000Z`) to get the engine's rows on the preview. -- A window over rows that hold a `Date` (the BSON storage form a MongoDB-backed draft reads back) is compared as instants, like the preview's `where`; it was compared as the `Date`'s display text. -- A host that wires no `sourceFieldMeta` (or an object the registry does not hold yet) reads every column type-blind. On a `text` column holding a value that sorts above `'9999-12-31'` (`'n'`), a `$lte` or `$between` maximum of `9999-12-31` now keeps that row, as every other type-blind seam does; the deleted copy left it out. - -**Unchanged.** Every answer on a declared `datetime` or `date` column, on the native strategy, the ObjectQL strategy and the draft preview; every answer of the ObjectQL strategy; every answer of the read scope. diff --git a/.changeset/21418-operator-text-cut.md b/.changeset/21418-operator-text-cut.md deleted file mode 100644 index 800035e2059..00000000000 --- a/.changeset/21418-operator-text-cut.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/types': patch ---- - -fix(types): `operatorFacingErrorText` answers through the driver-fault redaction, so an operator-facing record carries no statement and no bound value - -Clause-②: no - -- **What changed.** `operatorFacingErrorText` passes every text it returns through `redactStatementFromMessage`, the one driver-fault redaction in this package. Text it reads off a raw-statement fault's `cause` is cut with `{ statementSent: true }`, which is the cut `@objectstack/driver-sql` applies to its own log line for the same fault. Every other text asks the shared leak predicate, as the engine's own log line does. -- **What an operator reads now.** The records this helper fills, in `os db clean` and in the metadata migrations and probes, keep the dialect's own diagnostic: the missing column, the failed constraint or the locked database. The value slots the redaction's dialect templates own are cut from it, and the redaction's marker stands where the statement was removed. The records no longer carry the statement or the values bound into it. -- **What does not change.** Text that is not a driver dump comes back exactly as before, empty text included. The thrown error is not touched: its `code`, `status`, class and `cause` reach every other reader as the driver composed them. The function's signature and the package's exports are unchanged. diff --git a/.changeset/21419-cube-measure-aggregate-field-type-refused.md b/.changeset/21419-cube-measure-aggregate-field-type-refused.md deleted file mode 100644 index d87f2a2d48c..00000000000 --- a/.changeset/21419-cube-measure-aggregate-field-type-refused.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -fix(lint)!: `os validate`, `os build` and `os lint` refuse an `analyticsCubes` measure whose aggregate the aggregate × field-type table refuses for its column, which the analytics door already refuses at query time - -Clause-②: no (narrowing) - - - -**BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail. A cube measure's `type` is its aggregate, and the analytics door judges every cube measure against `AGGREGATE_FIELD_TYPE_COMPATIBILITY` before any SQL is built: a pair the table refuses is answered `400 INVALID_FIELD`. The authoring check judged only a cube's `count_distinct` measures, so a cube `sum` over a `text` column, for one, passed every command and was refused on its first query. The `measure-aggregate-field-type-refused` id (gating, `error`) now judges every cube measure exactly as it judges a dataset measure. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. - -**What is refused.** On a cube whose `sql` names an object the stack defines: a `measures` entry of `type` `sum`, `avg`, `min` or `max` whose `sql` column is declared with a type outside that aggregate's row of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (`@objectstack/spec/data`). The four rows accept the numeric and boolean types, `min` and `max` the temporal types too, and `sum` does not accept `percent`; so a text, option, reference, file or structured-JSON column, among others, is refused under all four, and a `date`, `datetime` or `time` column under `sum` and `avg`. The column is the measure's `sql`: a column of the cube's object, or a relationship path read on the object its last hop reaches (the join the cube declares for that hop, else the lookup field's `reference`). - -**What an author sees now.** The finding names the cube, the measure, the column, the object that declares it and its type, the types the aggregate accepts and the aggregates the column's type accepts, and says the analytics door refuses the pair with `400 INVALID_FIELD`. It is located at `analyticsCubes[N].measures.KEY.type`, where `KEY` is the measure's key. A quantity that must be added up, averaged or ordered has to be stored as a numeric or temporal field and aggregated as one; `count` accepts every column. - -**Unchanged.** Every dataset finding and every cube dimension finding; a cube `count` measure over any column; a cube `count_distinct` measure, judged as before; a measure of an expression type (`number`, `string`, `boolean`); the row wildcard `'*'`; a measure whose column does not resolve or declares no type; a cube whose `sql` names no object this stack defines. The runtime metadata write door: no authoring rule is dispatched for an `analytics_cube` save. diff --git a/.changeset/21426-native-number-comparand.md b/.changeset/21426-native-number-comparand.md deleted file mode 100644 index 649e6e4b10f..00000000000 --- a/.changeset/21426-native-number-comparand.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -The analytics native-SQL path judges a comparand against a declared number field by the platform's number-comparand rule, the one the data engine's `where` already applies - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the analytics native-SQL face accepts. A query or dataset that compares a declared number field with a comparand the number-comparand rule refuses used to answer 200 with a count on the native face (a 500 on PostgreSQL for a non-numeric string). It now refuses `INVALID_FILTER` / 400 before any statement runs, which is what the engine-aggregate face already answered. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. - -- **What changed.** A comparand against a `number`, `currency`, `percent`, `rating`, `slider`, `progress` or `summary` column is judged by `numberComparandDoorVerdict` from `@objectstack/spec/data` before the native statement compiles. This covers the query's `where` (including the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter`. The rule runs in the same pass as the boolean rule. - - A numeric string (`'12'`, `'1e3'`) is bound as the number it names, which is what the engine binds. - - Anything else the rule refuses (a string with no numeric reading such as `'abc'`, `''` or `'+5'`, a boolean, or a list where one number belongs) is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. - - A relationship-path member is judged at the related object's declared column. -- **Before.** The native strategy bound the comparand as written. So `{ amount: 'abc' }` counted no rows on SQLite and answered a 500 on PostgreSQL, `{ amount: true }` bound `1` and answered 200, and `{ amount: { $lte: '9999-12-31' } }` counted every row. The engine-aggregate strategy refused all three with 400. -- **What you may notice.** An analytics query or dataset that compared a number field with a value outside the rule's accepted set now refuses instead of answering. Write a number, or a string of exactly that number's JSON spelling (`'12'`). -- **Unchanged.** A number, `null` (the null test), a `{ $field }` reference, a column that is not a number or a boolean, and a host that relays no declared field types (nothing is judged without one). diff --git a/.changeset/21434-migrate-json-exit-signal.md b/.changeset/21434-migrate-json-exit-signal.md deleted file mode 100644 index 07285fc9230..00000000000 --- a/.changeset/21434-migrate-json-exit-signal.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os migrate recorded-by`, `resume` and `account-issuer` print exactly one `--json` document, and a completed run exits 0 (#21434) - -Clause-②: no - -`os migrate recorded-by --apply --yes --json` converted the rows, printed its result, then printed a second document, `{"error":"EEXIT: 0","duration":…}`, and exited 1. A script that read the exit status took the completed run for a failure, and a parser that read stdout failed on the second document. The cause was the command's own `catch`: the `this.exit(…)` inside its `try` throws oclif's exit signal, and the `catch` reported the signal as an error. - -The same `catch` sat in three more commands: - -- **`os migrate resume --run --json`.** A run that was already concluded printed a second `{"error":"EEXIT: 0"}` and exited 1 instead of 0. A resumed run did the same. Every refusal inside the command (unknown run id, plan not loaded, confirmation required) printed a second `{"error":"EEXIT: 1"}` under its own document. -- **`os migrate account-issuer --json`.** A refused pre-flight printed a second `{"error":"EEXIT: 1"}` under its report. Without `--json`, it printed an extra `EEXIT: 1` error line. -- **`os migrate apply`** (text output). A `sys_account.issuer` pre-flight refusal printed an extra `EEXIT: 1` error line. - -Each command now prints one document and exits with the status it computes. A completed `recorded-by --apply` and an already-concluded or resumed `resume --run` exit 0. Refusals and failed runs still exit 1. A script that worked around the second document or the exit status 1 can drop that workaround. diff --git a/.changeset/21437-analytics-measure-names-no-field.md b/.changeset/21437-analytics-measure-names-no-field.md deleted file mode 100644 index 236c5f53c5d..00000000000 --- a/.changeset/21437-analytics-measure-names-no-field.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/service-analytics': minor ---- - -fix(service-analytics)!: a caller-named analytics measure whose inferred source names no field (`_sum`, `*`, `*_sum`, an empty spelling) is refused with `INVALID_FIELD` / 400 at the analytics door, naming the spelling sent, on both strategies, before any statement is built (#21437) - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept in `measures`, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, published type or error code changes. - -**The rule.** A `measures` entry the cube does not declare is inferred: the bare `count` counts rows (`COUNT(*)`), and any other spelling aggregates one of the object's own fields, named before an aggregation suffix (`_sum`, `_avg`, `_average`, `_min`, `_max`, `_count_distinct`) or, with no suffix, by the whole spelling. The bare `count` is now the only spelling that reads the row wildcard `'*'`. A spelling whose source is empty or is `'*'` names no field, and it is refused with `400 INVALID_FIELD` before anything is executed. The error names the spelling as it was sent (`member`, with `param: 'measures'` and `cube`); a `.` qualifier is kept in the name. - -**Before**, measured through `POST /api/v1/analytics/query` on SQLite, on the native-SQL and the ObjectQL strategy, on an ad-hoc cube and on an authored cube that does not declare the member: - -- `_sum`, `_avg`, `_average`, `_min`, `_max`, their `.`-qualified forms, `*`, `*_sum`, `*_avg` and the empty spelling `''` answered `500 DATABASE_ERROR`, after a statement reached the database (`SUM(*)`, `AVG(*)`, `SUM()`). -- `_count_distinct` and `*_count_distinct` answered `500 DATABASE_ERROR` on the native-SQL strategy (`COUNT(DISTINCT *)`). On the ObjectQL strategy the engine answered `400 INVALID_QUERY` after the aggregate was called. -- The qualifier alone (`.`) answered `403 PERMISSION_DENIED` from the member-shape gate. It now answers the same `400 INVALID_FIELD`, because it names no field either. - -**Now** each of those answers `400 INVALID_FIELD`, and no statement and no engine aggregate runs. `POST /api/v1/analytics/sql` refuses the same spellings instead of returning a statement that cannot run. - -**What to write instead.** Ask for `count` to count rows, or put the field's name before the suffix: the sum of `amount` is `amount_sum`. - -**Who is affected.** A caller that sent a measure spelling with nothing before the suffix, or the row wildcard itself. Every such request was already a 500. No example app, shipped dashboard, report, dataset, cube, doc or skill in this repository sends one. The console's analytics adapter composes a measure as the value field, an underscore and the aggregate function, so a widget whose value field is empty posts `_sum`. At the pinned `.objectui-sha` that adapter reads a 500 as an unknown failure and answers with its own client-side aggregation; it reads the 400 as a rejected request and surfaces it as an error. - -**Unchanged.** The bare `count`; a field-prefixed spelling such as `amount_sum`; the no-suffix spelling of a field (`amount`); a measure a cube declares, including one declared under a key such as `_sum`, which is the cube's own vocabulary and is never inferred; and the authored-position twin of this rule, the `@objectstack/spec` parse refusal of `'*'` outside a `count` on a cube or dataset measure (#21409). diff --git a/.changeset/21439-field-consumers-analytics-paths.md b/.changeset/21439-field-consumers-analytics-paths.md deleted file mode 100644 index f4d331929ef..00000000000 --- a/.changeset/21439-field-consumers-analytics-paths.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -`field-no-consumers` no longer calls a field "inert" when a dataset or cube member reads it through a relationship path (#21439). - -Clause-②: no - -`os validate`, `os build` and `os lint` warned "Verdict: inert — no site of any kind names it" for every field an analytics member reached through a path such as `account.revenue`, so an author following the warning would delete a column a measure reads. The four slots that name a column are a dataset dimension's and measure's `field` and a cube dimension's and measure's `sql`. Each one now credits every field its path reads: the lookup on the base object, each intermediate lookup, and the column on the object the last hop reaches. - -- **Hops resolve the way the analytics door resolves them.** A cube hop goes through the join the cube declares for it, else the lookup's `reference`. A dataset hop goes through the `reference` its compiler joins through, and only where the dataset's `include` declares the join. -- **A bare cube column is credited too.** Before, a cube member's `sql: 'amount'` credited nothing, because a cube names its object in its own `sql`. -- **A path the door refuses reads nothing.** Examples: a join the dataset's `include` does not declare, a hop that names no relationship, a column the last object does not have. Each field such a path names is now listed as a carrier site that a removal must clean (`carrier-only`), not as a reader. -- **A path the object graph cannot judge** credits the fields it does resolve. An example is a lookup to an object this stack does not define. - -Nothing new is refused, and the rule stays a warning. One warning can appear where there was none: a path the door refuses through a lookup named after its target object (`account.revenue`, with `account` a lookup to the object `account`). The old text scan credited its column as read. It is now reported `carrier-only`, beside the error the refused path already carries. diff --git a/.changeset/21441-objectql-echo-date-bucket.md b/.changeset/21441-objectql-echo-date-bucket.md deleted file mode 100644 index f42ef7cf08f..00000000000 --- a/.changeset/21441-objectql-echo-date-bucket.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/service-analytics": minor -"@objectstack/driver-sql": minor -"@objectstack/driver-turso": patch ---- - -fix(service-analytics): the ObjectQL face echoes a date-bucketed dimension in the bucket expression the driver itself groups by, so SQLite runs the statement it prints - -Clause-②: yes (widening) - -**Before**, the ObjectQL strategy printed every date-bucketed dimension as `date_trunc('', col)` in the `sql` it echoes and in the `POST /analytics/sql` body, on every dialect. The native strategy declines a granularity, so every bucketed query lands on this face. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` in the default composition: the rows were right. On SQLite the echo failed with `no such function: date_trunc` (month, quarter and week). On PostgreSQL 16.14 it ran but answered `2026-01-01T00:00:00.000Z` where the face answers `2026-01`. The driver groups by `strftime('%Y-%m', …)` on SQLite and `to_char((…)::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM')` on PostgreSQL. - -**Now** the echo prints the driver's own expression, so it runs on that dialect and answers the face's bucket keys. - -- **`@objectstack/driver-sql`**: `SqlDriver.dateBucketSql(objectName, field, granularity)` returns the expression `aggregate` groups by, rendered as SQL text: the existing `buildDateBucketExpr`, unchanged, with each identifier quoted by the dialect. It returns `null` for a granularity the dialect buckets in memory (`week` on SQLite). The MySQL arm (`date_format(convert_tz(…))`) is checked by code read only, because no MySQL server was available. -- **`@objectstack/service-analytics`**: the new optional `AnalyticsServiceConfig.dateBucketSql` hook carries the expression to the ObjectQL strategy. `AnalyticsServicePlugin` wires it from the driver that serves the object, as it wires `sqlDialect`. -- **`@objectstack/driver-turso`**: a comment that said `SqlDriver` buckets with `date_trunc` now names the SQLite `strftime` expression it emits. The inherited `dateBucketSql` answers on the remote face too: it renders the same SQLite expression with no connection, and libSQL runs it. - -**Unchanged.** The rows every face answers. The echo keeps `date_trunc(…)` where nothing answers: a host that wires no hook, a driver with no bucket expression (memory, MongoDB), a granularity the driver buckets in memory, and a query with a non-UTC `timezone`, which the engine buckets in memory on that zone's calendar. diff --git a/.changeset/21442-by-name-expanded-view.md b/.changeset/21442-by-name-expanded-view.md deleted file mode 100644 index 0683397020d..00000000000 --- a/.changeset/21442-by-name-expanded-view.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): a view a stored view container expands answers by name what the object door lists, on every kernel and for every container scope - -Clause-②: no - -- **What changed.** `GET /api/v1/meta/view?object=…` lists the views a stored view container expands, and the by-name read now answers each of those names with the same item. Before, `getMetaItem` expanded no container: it answered such a name only on an unscoped kernel and only for an environment-wide container, where the registry held a hydrated copy. On an environment-scoped kernel, and for an organization-scoped container on any kernel, it answered nothing. Where the name is one a package also ships, such as `.default` under a tenant's overlay of that package's container, it answered the packaged view while the list served the overlay's. -- **How.** The by-name read selects the stored containers in the caller's scope with the list read's own row selection, and expands them with the list read's own expansion. Nothing is persisted or registered, and a stored row of the name itself still answers first. -- **Layers, history and diff for such a name.** `getMetaItemLayered` reports the container's own stored row as `overlay`, with the scope it was read from as `overlayScope`, and the expanded view as `effective`. `historyMetaItem` and `diffMetaItem` answer exactly what they answer under the container's own name, and say so: every event's `ref.name` and the diff's `name` are the container's. No history is made up for a name that was never stored. -- **What does not change.** The container's own name still answers its stored row. The save door is unchanged, including a write by an expanded name. No response shape gains or loses a key. diff --git a/.changeset/21445-object-grid-typed-members.md b/.changeset/21445-object-grid-typed-members.md deleted file mode 100644 index f6b19aded8a..00000000000 --- a/.changeset/21445-object-grid-typed-members.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-grid` page block's props type the seven members the grid reads with a fixed shape, and the legacy `resizableColumns` spelling is retired in favour of `resizable` (#21445) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **Seven members of `ComponentPropsMap['object-grid']` are typed.** Each was `z.unknown()` (`bulkActionDefs` an array of it), although the console's `ObjectGrid` reads each with one shape. Any value passed, and the grid answered an off-shape one with a silent default: `rowHeight: 42` rendered as a compact grid, and an aggregation with an unknown function drew a zero nothing computed, or no number at all. Each member now takes the shape the grid reads: - - `rowHeight` is the list view's `RowHeightSchema`: `compact`, `short`, `medium`, `tall` or `extra_tall`. These are exactly the five values the grid admits. - - `rowColor` is the list view's `RowColorConfigSchema`, `{ field, colors }`. - - `navigation` is the list view's `NavigationConfigSchema`, the same carrier `object-kanban`, `object-calendar` and `object-timeline` take. - - `conditionalFormatting` is the list view's own member, `[{ condition, style }]`, with a CEL `condition` and a CSS `style` map. - - `bulkActionDefs` is an array of the list view's `BulkActionDefSchema`. - - `aggregations` is `[{ field, type }]`, with `type` drawn from the query AST's aggregation functions (`count`, `sum`, `avg`, `min`, `max`, `count_distinct`). No list-view schema declares this member, so the shape is the one the grid's grouping reads. - - `operations` is `{ create?, update?, delete?, export? }`, the four booleans a grid read point names. `read` and `import` are refused with the reason: no grid read point reads either. -- **`resizableColumns` is retired.** It was the legacy second spelling of `resizable`, read only when `resizable` was absent, so a grid authoring both silently ignored it. It is now a `retiredKey()` tombstone: writing it fails `tsc` (the input type is `never`) and fails the parse with a prescription naming `resizable`. Nothing in either repository wrote it. -- **`ObjectGridProps`** (and `ObjectGridPropsParsed`) carry those types instead of `unknown`, and `resizableColumns` is `never`. - -## FROM → TO - -| you wrote on an `object-grid` | write instead | -|:--|:--| -| `resizableColumns: false` | `resizable: false` — the same boolean | -| `resizableColumns: true` beside `resizable: false` | `resizable: false` — the grid has always followed `resizable` | -| `rowHeight: 42`, `rowHeight: 'comfortable'` | `rowHeight: 'medium'`, or another of `compact` / `short` / `tall` / `extra_tall` | -| `rowColor: 'red'` | `rowColor: { field: 'status', colors: { overdue: 'red' } }` | -| `navigation: 'drawer'` | `navigation: { mode: 'drawer' }` | -| `conditionalFormatting: [{ field: 'status', operator: 'equals', value: 'late', backgroundColor: '#fee2e2' }]` | `conditionalFormatting: [{ condition: "record.status == 'late'", style: { backgroundColor: '#fee2e2' } }]` | -| `aggregations: [{ field: 'amount', type: 'median' }]` | a function the grid computes: `count`, `sum`, `avg`, `min`, `max` or `count_distinct` | -| `operations: { create: true, read: true, import: false }` | `operations: { create: true }` — delete `read` and `import`; nothing reads them | - -The one-line fix: rename `resizableColumns` to `resizable`, and write each of the seven members in the shape the list view declares for the same key (`aggregations` as `[{ field, type }]`, `operations` as four booleans). `os migrate meta --from 17` lists the mechanical `resizableColumns` edits for existing sources. - -## The retirement kit - -- **Tombstone.** `resizableColumns` is a `retiredKey()` on `ObjectGridPropsSchema`; its authorable-surface line carries `[RETIRED]`. -- **Conversion.** `object-grid-resizable-columns-removed` (protocol 18, retired from the load path) follows the renderer's own precedence. It moves the value to `resizable` when `resizable` is absent, and deletes the key as a lossless strip when `resizable` holds a value. Its D3 record is the semantic entry `object-grid-resizable-columns-retired`, which carries the judgment for a grid that authored both keys with different values. -- **Registration.** `RETIRED_KEYS_BY_MAJOR[18]` carries `ui/ObjectGridProps:resizableColumns`. -- **The typed members** have the D3 entry `ui-object-grid-row-members-typed` and no conversion. Nothing on the load path refuses their shapes, and an off-shape value has no rewrite that keeps what the grid shows while honouring what the author wrote. - -## Who is affected, measured - -- **objectstack.** Measured on `origin/main` `53fd35e3e3`: zero `object-grid` blocks author any of the seven members or `resizableColumns` in the examples, `@objectstack/platform-objects`, the spec tests, the documentation and the published skills. The control: the same census finds the two showcase grids' `columns`. -- **objectui.** Measured at the `.objectui-sha` pin, over 76 `object-grid` property bags in its sources, tests and documentation (23 of them in parsed JSON documents). One documentation example, the repository README's data grid, authors `operations.read: true`, which this row now refuses. No other bag authors a refused shape. The control: the same census finds `columns` in 46 bags. -- **Deployed metadata** was not measured. diff --git a/.changeset/21448-list-at-scalar-operator.md b/.changeset/21448-list-at-scalar-operator.md deleted file mode 100644 index 128939c2832..00000000000 --- a/.changeset/21448-list-at-scalar-operator.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A list at a scalar operator (`{ amount: { $gt: [10, 99] } }`) is refused at the shared comparand-shape face, whatever the column type, instead of being narrowed to its first member - -Clause-②: no (narrowing) - - - -**BREAKING**: this narrows what the shared filter faces accept. FROM: a list at a scalar operator passed the comparand-shape face, and each consumer answered it alone. The analytics lowering bound the list's first member (`{ note: { $gt: ['a', 'z'] } }` answered 200 as `$gt 'a'` on both analytics faces, and the engine-aggregate face did the same on a number column), `driver-sql` refused it in its own words, and `driver-memory` answered one at a text operator. TO: `INVALID_FILTER` / 400 at the face, before any read, on every door that runs it, with one sentence naming the operator, the field, the list and where. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. - -- **What changed.** `assertListComparandShapes` (`@objectstack/spec/data`) refuses a list under every scalar operator other than `$eq` / `$ne`, which keep their own refusals: `$gt`, `$gte`, `$lt`, `$lte`, the text operators (`$contains`, `$notContains`, `$startsWith`, `$endsWith`, `$icontains`, `$like`, `$ilike`) and the flags (`$null`, `$exists`, `$empty`). This covers every depth, both filter spellings (object and `[field, op, value]`), and the empty list. - - The engine's `where`, per-aggregation `filter` and `having`, `parseFilterAST`, both analytics doors, the read-scope compiler and the RLS compiler all run the face, so all of them refuse it. - - The save door asks the same face. A dataset, measure, dashboard-widget or report filter that carries one is refused on save, located on the member. The HTTP routes that parse a filter in their body (`POST /api/v1/data/:object/query`, `/api/v1/analytics/query`, `/api/v1/analytics/dataset/query`) answer `VALIDATION_FAILED` / 400 there, as for every other face refusal. -- **What you may notice.** A filter that put a list under `$gt`, `$contains` or a flag now refuses instead of answering. Write one value; for "one of these values" use `$in` (authoring `in`), and for a range use `$between` (authoring `between`). A list at a flag reads in this sentence now, not the boolean-flag one. -- **Unchanged.** A list at `$in` / `$nin` / `$between`, `$in: []` / `$nin: []`, every single value (`null`, a `Date` and a `{ $field }` reference included), a list nested inside `$in`, and an operator outside the declared vocabulary, which keeps its own refusal. diff --git a/.changeset/21454-reader-context-evaluate-refusals.md b/.changeset/21454-reader-context-evaluate-refusals.md deleted file mode 100644 index 98a174fcae5..00000000000 --- a/.changeset/21454-reader-context-evaluate-refusals.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/runtime': minor ---- - -fix(runtime)!: the in-process reader contexts refuse the stored-metadata-body family's EVALUATE shapes and serve what a write returns, the way the generic data door does (#21454) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what an action or hook body's object API, an action handler's scoped API and an action handler's engine handle accept when they read the two stored-metadata tables. A read there that filters, sorts or groups on the stored body column or on a content-hash column, a read that names one of those columns in an explicit search-field list, and a `count` carrying such a filter, ran before this release and now answer the generic data door's `400 INVALID_FIELD` before the query runs. The route: filter, sort, group and search those tables by their scalar columns (the type, the name, the state and the like), and read the bodies with a plain list, which is served projected — the body as its type's read projection, the content hash in keyed form. A default search with no field list is not refused: it is narrowed to the columns the door serves. Every other column of the two tables, and every other object, is unchanged. It ships as `minor` under the launch-window convention for accept-set narrowings. - -- **`@objectstack/metadata-protocol`** now exports the generic data door's four evaluate-refusal predicates — `storedMetadataBodyGroupingRefusal`, `storedMetadataBodyPredicateRefusal`, `storedMetadataHashEvaluateRefusal` and `storedMetadataSearchRefusal` — so the `@objectstack/runtime` reader-context seam refuses the same shapes through the door's own predicates rather than a second copy. Additive: nothing that imported the package before is changed. -- **`@objectstack/runtime`** extends the stored-metadata reader-context seam (`ctx.api.object(...)` for action and hook bodies, a handler's `ctx.api`, and `ctx.engine.find`): a filter, sort, grouping or search that would evaluate the stored body or content hash of `sys_metadata` / `sys_metadata_history` is refused with the door's `INVALID_FIELD` / 400 before the query runs (a `count` with such a predicate included); a default `$search` is narrowed to the door's served field set rather than refused; and the row a write verb returns is served projected and keyed. The engine's own action verb (`ScopedRepo.execute`) is unreachable from a served body and is left untouched. diff --git a/.changeset/21454-reader-context-family-serve.md b/.changeset/21454-reader-context-family-serve.md deleted file mode 100644 index f99c1295aac..00000000000 --- a/.changeset/21454-reader-context-family-serve.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/runtime': patch -'@objectstack/metadata-protocol': minor ---- - -fix(runtime): a sandboxed body or an action handler that reads the stored-metadata tables is served what the generic data door serves (#21454) - -Clause-②: yes - -The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and a content hash computed over it. The generic data door serves such a row with the body as its type's read projection, with the stored credential material withheld, and the hash in keyed form. Three in-process reader contexts served the same rows as stored: - -- a sandboxed action or hook body that reads through `ctx.api.object(...)`, inside `ctx.api.transaction(...)` too; -- an action handler that reads through `ctx.engine.find(...)`; -- an action handler that reads through `ctx.api.object(...)`. - -An action body and an action handler run elevated, so the stored form reached whoever could invoke the action, a member included. - -**What changes.** A read of either table through any of these contexts now answers the data door's form: the projected body, and the content hash under the same key the data door uses. That key is the crypto provider's, or the process-scoped ephemeral key when no provider is registered. `find`, `findOne` and `aggregate` are served this way, and so is every context the scoped API derives: `sudo()`, `withRunAs(...)`, a `transaction(...)` callback's context, and the context `beginTransaction()` returns. A hook body that copies what it read into another record can now copy only the projected form. A projection that names the body column without the type column reads the type beside it and drops it again, as on the data door. - -**What does not change.** Every other object, every write and `count` behave as before. The platform's own readers of these tables still read the stored form, because the projection is applied at the reader contexts and not in the engine. - -`@objectstack/metadata-protocol` now exports the data door's stored-row serve, so these contexts consume it and keep no copy: `storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows`, `ephemeralStoredHashDigest` and the `StoredHashDigest` type. The exports are additive. - -The four functions `storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows` and `ephemeralStoredHashDigest`, and the type `StoredHashDigest`, are new public API of `@objectstack/metadata-protocol`, and `@objectstack/runtime` consumes them. diff --git a/.changeset/21455-approval-actor-lookups-hold-ids.md b/.changeset/21455-approval-actor-lookups-hold-ids.md deleted file mode 100644 index db2969d7989..00000000000 --- a/.changeset/21455-approval-actor-lookups-hold-ids.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -Every `sys_user` lookup the approvals plugin writes now holds a user id or nothing: the SLA and dead-run sweeps record no actor instead of a `system:` placeholder, notifications name only the person who acted, and `reassign_from` / `reassign_to` become slot-address text columns; stored placeholders are cleared at the next boot - -Clause-②: no - -Under ADR-0118 D1 a lookup to `sys_user` holds a user id or null, never a placeholder value. Four writers broke that, and a lookup holding a non-id drops the row from every join and report on it, silently. - -**This supersedes the "The SLA sweep keeps its reserved `system:sla` actor for now" sentence of the unreleased `21411-approval-actor-person` changeset.** Both ship in one release; from it, the sweep records no actor. - -- **Machine actors record no actor.** - - The SLA sweep's `escalate` row, and the `approve` / `reject` an `auto_approve` / `auto_reject` escalation then records, have `actor_id` empty. Before, both held `system:sla`. The `escalate` row's comment still names the configured action. - - The dead-run sweep's `recall` row has `actor_id` empty. Before, it held `system:dead-run`. Its comment still names the dead run and its status, and a submitter's own recall still records the submitter. -- **Notifications name only a person.** The actor the plugin hands to `sys_notification.actor_id` (and so to each `sys_inbox_message.actor_id`) is the user the action's context vouches for, or nothing. - - Before, a reassign, reminder, request for information, comment or send-back taken under a named position or email forwarded that address as the actor. - - Before, every SLA notification forwarded `system:sla`. It now forwards no actor, as the out-of-office notifications already did. -- **`reassign_from` / `reassign_to` are slot addresses.** A reassignment moves a pending-approver slot, so both columns hold the slot's address in its stored spelling: a user id, an email, or `position:`. They are now text columns (max 255 characters, like `acted_as`) instead of `sys_user` lookups, which matches what they already stored. - - Existing values need no rewrite, and an existing database keeps its columns as they are. On SQLite and PostgreSQL 16, booting the new declaration over a table created by the old one issues no DDL, keeps every stored value, and reports no schema drift for either column. A new database creates them as `text`, as it does `acted_as`. - - The action log still resolves `reassign_from_name` / `reassign_to_name` where an address names an account: a user id, or an email an account carries. A position address has no name. -- **Stored rows.** The boot-time repair that moves slot literals out of `actor_id` now also clears `system:sla` and `system:dead-run` from it, in the same pass and in the same idempotent way. A cleared sentinel gets no `acted_as`, because a sweep takes no slot. The boot log line reports the count as `sentinelsCleared`. -- **For a report or integration that read these values:** - - To find the SLA sweep's actions, read the `escalate` rows, and the decision that directly follows an `escalate` row whose comment names `auto_approve` or `auto_reject`. Do not test `actor_id` for `system:sla`. - - To find a dead-run release, read the `recall` row whose comment names the run. Do not test `actor_id` for `system:dead-run`. - - Read `reassign_from` / `reassign_to` as slot addresses. Do not expand them as `sys_user` references. diff --git a/.changeset/21458-spec-approval-action-acted-as.md b/.changeset/21458-spec-approval-action-acted-as.md deleted file mode 100644 index 5424509520c..00000000000 --- a/.changeset/21458-spec-approval-action-acted-as.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): `ApprovalActionRow` declares `acted_as`, the pending-approver slot an approval action was taken as, beside the person in `actor_id` - -Clause-②: yes - -**What it declares.** One optional string member on `ApprovalActionRow` in `@objectstack/spec/contracts`, the row type of an approval request's action log (`IApprovalService.listActions`, served at `GET /api/v1/approvals/requests/:id/actions` and typed by the client SDK): - -- `acted_as?: string` is the slot the action was admitted under, in the slot's stored spelling as it stood in the request's `pending_approvers`: a `position:` address (or `role:`, the deprecated pre-rename spelling), an email, or a user id. -- It is never a person. The person who acted is `actor_id`, which under ADR-0118 D1 holds a `sys_user` id or nothing. A slot addressed by a user id carries that id in `acted_as` as the slot's address, which makes no claim about who acted. -- Absent means the action was not admitted through a slot (a submitter's own action, a system action, or an admin override, which `via_override` marks), or the row was written before the slot was recorded. So absent alone never proves that no slot was involved. - -**What moves for consumers.** Nothing in this package writes the member, and every existing export and member is unchanged: a row without `acted_as` conforms exactly as before. The approvals service is its producer, and that package's own changeset states when `listActions` starts returning it. Until then every row omits it, which is the member's declared absent case. A client that renders the action log can show `acted_as` beside the actor's name as the capacity the actor acted in. diff --git a/.changeset/21459-page-requires-compiled-kinds.md b/.changeset/21459-page-requires-compiled-kinds.md deleted file mode 100644 index 0f6d5e7c3bf..00000000000 --- a/.changeset/21459-page-requires-compiled-kinds.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A page's `requires` is accepted only on the kinds whose source is compiled at save: `html` and its deprecated alias `jsx`. On a `react`, `full` or `slotted` page, and on a page that omits `kind` (which is `full`), it is refused at parse. - -Clause-②: yes (narrowing) - - - -**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. - -**Why.** `requires` is the list of plugin namespaces a page's source uses (ADR-0080 §5). It is derived from the source at save, and its describe has always said "omit it". On an html page, on a server that has the deployment's SDUI component manifest, the metadata save door compiles the source, stores the namespaces it uses as `requires`, and refuses a written list that disagrees. A `react` source is executed at render and never compiled at save, and `full` and `slotted` pages have no source. So on those three kinds nothing derived the key, the Studio page editor dropped it on every save, and its one reader was a load-time warning. `PageSchema` still accepted it there and never told the author it did nothing. The maintainer ruled that the key is accepted only on the compiled kinds. - -**What is refused.** `requires` on a page whose `kind` is `react`, `full` or `slotted`, or a page with no `kind`, at the `requires` path. An empty list is refused too, because the key is what is refused, not its contents. The issue's `code` is `custom`, and its message names the key, the page's kind and the compiled kinds. That covers `definePage()`, `PageSchema`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `pages.N.requires`), `os validate`, which runs the same stack parse, and the metadata save door (`422 INVALID_METADATA`). - -**What stays accepted.** `requires` on an `html` or `jsx` page, byte for byte. The save door still derives it, stores it, and refuses a written list that disagrees. Every page that omits `requires` parses as before, on every kind. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `requires: [...]` on a `kind: 'react'` page | nothing: delete the key. Nothing derived or enforced it | -| `requires: [...]` on a `kind: 'full'` or `kind: 'slotted'` page, or on a page with no `kind` | nothing: delete the key | -| `requires: [...]` on a `kind: 'html'` or `kind: 'jsx'` page | unchanged. The platform derives it from the source at save, so omitting it is still the intended authoring | - -**The one-line fix: delete `requires` from every page whose `kind` is not `html` or `jsx`.** `os migrate meta --from 17` lists the mechanical edits for existing sources. Stored pages and built artifacts are converted when they are read. - -**Who is affected, measured.** No page body authors `requires` on a `react`, `full` or `slotted` page in this repository at `c98a72d69e` (`examples/**`, `packages/apps/**`, `content/docs/**`, `skills/**`, tests and fixtures). Every `requires:` there is the stack-level capability list or an html page in a save-door test. The same holds in cloud (`c5a4c9e6cb`), hotcrm (`5ae524916d`) and objectui (`8366accd13`), per the ruling's census. Deployed metadata was not measured. - -### The retirement kit - -- **The refusal.** `checkPageRequiresKind`, an exported object-level check attached to `PageSchema` beside `checkPageSourceCompleteness` (`@objectstack/spec/ui`), with `COMPILED_PAGE_KINDS` (`['html', 'jsx']`) as its vocabulary. A downstream mirror that derives its schema from `PageSchema.shape` re-attaches it with `.superRefine(checkPageRequiresKind)`. There is no tombstone and no `RETIRED_KEYS_BY_MAJOR` row, because the key stays live on html pages. -- **The conversion.** `page-requires-non-compiled-kind-removed` (protocol 18) deletes the key from `react`, `full`, `slotted` and kind-less pages. It is a lossless delete: on those kinds the list never had an effect. It is retired from the load path, so authored sources are refused at parse, while stored rows, built artifacts and `os migrate meta` replay it. Its D3 record is the semantic entry `page-requires-non-compiled-kind-refused`. -- **The ledgers.** The `requires` describe, its liveness row (`liveness/page.json`) and its form-reconciliation row now say the key exists only on html and jsx pages. diff --git a/.changeset/21464-component-props-form-custom-fields-sections-typed.md b/.changeset/21464-component-props-form-custom-fields-sections-typed.md deleted file mode 100644 index b7778a880a2..00000000000 --- a/.changeset/21464-component-props-form-custom-fields-sections-typed.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-form` page block's `customFields` takes a closed runtime form field, and the `sections` of `object-form` and `object-master-detail-form` take a page-block section shape, instead of any value (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — two accept-set narrowings on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`object-form` `customFields` is a list of closed runtime form fields.** It was `z.unknown()`. Each member is the field the form merges over the fields it generates from the object's metadata and draws as written, and the spec now declares it: `name` (its identity), `label`, `description`, `type`, `inputType`, `widget`, `required`, `disabled`, `readonly`, `hidden`, `placeholder`, `options`, `validation`, `dependsOn`, `visibleWhen`, `readonlyWhen`, `requiredWhen`, `colSpan`, `span`, `group`, and the metadata a field widget reads off it — `multiple`, `rows`, `accept`, `dimensions`, `reference`, `min`, `max`, `minLength`, `maxLength`, `pattern`, `returnType`, `summaryOperations`, `columns`. Members this package already declares take that declaration by reference (the object field's own, the evaluated predicates); `label`, `description` and `placeholder` are plain strings. An `options` entry is the runtime option the form's option controls draw, closed: `label`, `value`, `description` (a lookup searches it), `visibleWhen` (the cascade offers the option only while it holds). Its `value` is a string, a number or a boolean, kept as written — an inline field binds no object column, so a stored field's lowercase-identifier rule does not apply, and `{ label: 'Box', value: 'Box' }` parses. -- **The `sections` of `object-form` and `object-master-detail-form` are one page-block section shape.** They were `z.array(z.unknown())`. A section takes the form view's section keys — `name`, `label`, `description`, `collapsible`, `collapsed`, `visibleWhen`, `columns`, `pane`, `group`, `fields` — and the form view's group-reference rule; each `fields` entry is a field name, the form view's `{ field, … }` entry, or an inline runtime form field (the `customFields` member). The stored form view's `FormSectionSchema` is unchanged. -- **Canonical spellings only.** A page block's `properties` is never parsed on the way to the form, so a form view's parse-time folds do not run there: a section `visibleOn` and a string `columns` reached the form raw and were dropped. Both are refused with the canonical spelling, and so is a `{ field }` entry's or an inline field's `visibleOn`. -- **Refused with what to write instead:** an inline field's legacy `condition`, its `defaultValue` (which seeds nothing), `id`, a `fields` member claim, the `grid` widget's eight snake_case keys (`min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label`, `sort_field` — they come in once the widget reads a camelCase spelling), a boolean `validation.required`, a `validation.pattern` / `validate` rule, an option's `color`, `default`, `disabled` or `icon` (no form option control reads them), and a locale map where the form draws a plain string. -- **`ObjectFormProps`, `ObjectMasterDetailFormProps`** and their parsed types carry the field and section types on these members instead of `unknown`; the shapes themselves are module-private. A bare CEL `visibleWhen` parses to its `{ dialect, source }` envelope, as on every evaluated slot, so the `object-form` row's input and parsed types now differ and it gains the one new export, the type `ObjectFormPropsParsed` (ADR-0122), as `ObjectMasterDetailFormPropsParsed` already is. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `customFields: [{ name: 'b', visibleOn: "record.a != ''" }]` | `customFields: [{ name: 'b', visibleWhen: "record.a != ''" }]` | -| `customFields: [{ name: 'b', condition: { field: 'a', equals: 'x' } }]` | `customFields: [{ name: 'b', visibleWhen: "record.a == 'x'" }]` (`notEquals` is `!=`, `in: [ … ]` is `record.a in [ … ]`) | -| `customFields: [{ name: 'memo', defaultValue: 'X' }]` | `customFields: [{ name: 'memo' }], initialValues: { memo: 'X' }` | -| `customFields: [{ name: 'a', validation: { required: true } }]` | `customFields: [{ name: 'a', required: true }]` (a string `validation.required` is the message) | -| `customFields: [{ name: 'a', validation: { pattern: { value, message } } }]` | `customFields: [{ name: 'a', pattern: '^[A-Z]+$' }]` | -| `customFields: [{ name: 'items', type: 'grid', min_rows: 1 }]` | `customFields: [{ name: 'items', type: 'grid', columns: [ … ] }]` — the widget's defaults until it reads a camelCase key | -| `customFields: [{ name: 'tier', type: 'select', options: [{ label: 'Gold', value: 'gold', default: true }] }]` | `customFields: [{ name: 'tier', type: 'select', options: [{ label: 'Gold', value: 'gold' }] }], initialValues: { tier: 'gold' }` | -| `options: [{ label: 'Gold', value: 'gold', color: '#d4af37' }]` on an inline field | `options: [{ label: 'Gold', value: 'gold' }]` — a colour belongs on the object field's own option | -| `sections: [{ fields: ['a'], visibleOn: 'record.b == 1' }]` | `sections: [{ fields: ['a'], visibleWhen: 'record.b == 1' }]` | -| `sections: [{ fields: ['a'], columns: '2' }]` | `sections: [{ fields: ['a'], columns: 2 }]` | -| `sections: [{ fields: [{ field: 'a', visibleOn: '…' }] }]` | `sections: [{ fields: [{ field: 'a', visibleWhen: '…' }] }]` | -| `sections: [{ label: { en: 'Basics' }, fields: ['a'] }]` | `sections: [{ name: 'basics', label: 'Basics', fields: ['a'] }]` — the heading translates through `objects.._sections.basics.label` | - -The one-line fix: write each inline field in camelCase with the members the form draws and each section in its canonical spelling. No conversion is registered: nothing on the load path refuses either shape, and the census below found no working value to respell — the D3 entries `ui-object-form-custom-fields-typed` and `ui-object-form-sections-typed` carry that judgment. - -## Who is affected, measured - -A writer is a value written on the block: a page-component node (an object literal naming `object-form` or `object-master-detail-form`, flat or in its `properties` bag, or a literal annotated as one), a direct parse through the row, the block's React component inside `schema={{…}}`, the argument a local helper passes in that position at every same-file call site, or — the second pass — any object literal carrying `customFields` or `sections` in a file that names a form block. Values resolve through same-file constants and spreads, and through a `.map` over a constant list — the first run of this census read such a list as non-static and so never parsed the object manager's options; that miss is why an inline option's `value` is now a runtime value. Every static value was parsed through this branch's rows, and each value with a non-static part, and each refusal, was read by hand. - -- **objectstack** at `316be321ef`, this branch's merge base: three `object-form` `sections` writers (the showcase's new-project wizard, and one test each in `lint` and `spec`), field names only — all parse. No `customFields` writer. The other `sections` the second pass finds are form views and `record:details` sections, which these rows do not judge. -- **objectui** at the `.objectui-sha` pin `2e818d0b51ec` and at `main` `fd060f076` (every cited reader file is byte-identical between the two; `main` adds three test values, which parse): **`customFields`** — 31 values parse (four block literals; the designer's object manager, whose `icon` and `group` options `{ label: 'Box', value: 'Box' }`, `{ label: 'Custom Objects', value: 'Custom Objects' }`, … parse as runtime option values — typed as the form view's option, a stored field's lowercase identifier, both were refused; and 26 helper and embeddable-form arguments) and two are refused, both probes: a type-level test's `visibleOn` (never drawn) and the fixture pinning that an inline `defaultValue` seeds nothing; the 11 fully non-static values are run-time hand-offs and helper parameters, read by hand. **`sections`** — 102 `object-form` values with a static part parse (the field designer's inline fields and the plugin-form README's inline-field wizard among them), and so do four `object-master-detail-form` values. Two are refused, both probes of shapes objectui's own renderer test says this door refuses at parse: a section declaring neither `fields` nor `group`, and a group-owned `label` / `collapsible` beside `group`. The 26 fully non-static values are run-time hand-offs and helper parameters, read by hand: they use declared keys only, but for objectui's probe that a retired `className` / `gridClassName` reaches nothing. The second pass's other refused section values are `record:details`, detail-view or object-view form-slot sections, which these rows do not judge. -- **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no writer of either member. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-form-family-typed.md b/.changeset/21464-component-props-form-family-typed.md deleted file mode 100644 index 24ff21831fa..00000000000 --- a/.changeset/21464-component-props-form-family-typed.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: four members of an `object-form` page block take the shape the form reads instead of any value — `contentLayout`, `submitBehavior`, `navigateOnSuccess` and `mobile` (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **Four members are typed.** `ComponentPropsMap['object-form']` declared `contentLayout`, `submitBehavior`, `navigateOnSuccess` and `mobile` as `z.unknown()`, although the form reads each with one shape. Any value passed, and an off-shape one was answered with a silent default: a `submitBehavior` whose `kind` the form does not know showed the thank-you panel; a misspelled `contentLayout` stacked the modal's sections; a `navigateOnSuccess` that is not a string failed the submit after the record had been written; a misspelled `mobile` member was ignored. -- **`submitBehavior` is the form view's own block, by reference** — `{ kind: 'thank-you', title?, message? }`, `{ kind: 'redirect', url, delayMs? }`, `{ kind: 'continue' }` or `{ kind: 'next-record' }` — with the same rule on a `redirect` `url` a form view carries: a relative path, interpolating declared record fields as `{{record.field_name}}`. -- **The measured shape, where no form view declares the member:** `contentLayout` is `'simple'` or `'tabbed'`; `navigateOnSuccess` is a relative path string (`{id}` / `{recordId}` interpolate the saved record's id); `mobile` is `{ stickyActions?, stepper?, stepperMinFields?, stepperFieldsPerStep?, fullscreenLongText? }`, with `stepper` `true`, `false` or `'auto'` and the two counts positive integers. -- **`ObjectFormProps`** carries these types on the four members instead of `unknown`. -- **The form's `fields` and `sections`, and the master-detail form's `fields` and `sections`, are not narrowed** and still accept any value. The form draws a top-level `fields` entry written as `{ name }` by that name, and it draws an inline runtime field (`{ name, type, … }`) written inside a section's `fields` as it stands — two shapes the typed members (field-name strings; the form view's section, whose field entry is keyed by `field`) would refuse. Each is held until that read is ruled. The master-detail form hands both members to its form unchanged, so they are held with the form's. -- **`customFields` is not narrowed either.** Its entries are the console's runtime form field (keyed by `name`), which the spec has not declared; it is typed once the spec declares it. - -## FROM → TO - -| you wrote on an `object-form` | write instead | -|:--|:--| -| `submitBehavior: 'thank-you'` | `submitBehavior: { kind: 'thank-you' }` | -| `submitBehavior: { kind: 'toast' }` (any `kind` outside the four) | one of `thank-you`, `redirect`, `continue`, `next-record` | -| `submitBehavior: { kind: 'thank-you', heading: 'Done' }` | `{ kind: 'thank-you', title: 'Done' }` | -| `submitBehavior: { kind: 'redirect', url: 'https://app.example.com/done' }` | a relative path: `url: '/done'` | -| `contentLayout: 'tabs'` | `contentLayout: 'tabbed'` | -| `navigateOnSuccess: { url: '/orders/{id}' }` | `navigateOnSuccess: '/orders/{id}'`, or `submitBehavior: { kind: 'redirect', url: '/orders/{{record.id}}' }` | -| `mobile: { stepper: 'yes' }` | `mobile: { stepper: true }`, or `'auto'` for phone-width viewports only | -| `mobile: { stepperFieldsPerStep: 0 }` | delete the key (one field a step is the default), or a positive integer | - -The one-line fix: write each member as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the form shows today and honours what the author wrote; the D3 entry `ui-object-form-members-typed` carries that judgment. - -## Who is affected, measured - -A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is read through same-file constants and local helpers. The control is `objectName` on the same nodes. - -- **objectstack** at `e909aa0a23`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 16 `object-form` nodes (the control on 13). Three values among the four members: the showcase's new-project wizard `submitBehavior` (a thank-you panel) and two copies of it in the lint and spec tests. All three parse. -- **objectui** at the `.objectui-sha` pin `89cad75d55`: 539 `object-form` nodes (the control on 522). Across the four members there are 73 values: 60 are static, and 56 of them parse. The 4 that do not are test fixtures of a protocol-relative redirect (`//example.com/thanks`), each asserting that the form refuses it and navigates nowhere. Of the 13 values that are not static, 9 are relative redirects that parse by inspection, and 4 are redirect fixtures the form refuses (three same-origin absolute URLs and one protocol-relative one). No refused value is one the form draws. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-kanban-conditional-formatting-typed.md b/.changeset/21464-component-props-kanban-conditional-formatting-typed.md deleted file mode 100644 index 6a8dc2df7b5..00000000000 --- a/.changeset/21464-component-props-kanban-conditional-formatting-typed.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-kanban` page block's `conditionalFormatting` takes the list view's own `[{ condition, style }]` rules instead of any value (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`object-kanban` `conditionalFormatting` is the list view's own member, by reference, as on `object-grid`.** It was `z.unknown()`, held while objectui's kanban also authored a native `{ field, operator, value, backgroundColor }` rule the list view refuses. objectui has since made the list view's `{ condition, style }` rule the member's only authoring dialect, and the board evaluates it with the evaluator the grid's rows use. So `42`, a bare string, a single rule outside a list or a rule with no `style` — values that passed and painted no card — are refused, and so are a blank `condition`, a non-string `style` value, the native rule, an `expression` rule and a colour written beside `condition` or `style`. -- **`ObjectKanbanProps`** carries the list view's rule type on `conditionalFormatting` instead of `unknown`. The member's string `condition` parses to the `{ dialect: 'cel', source }` envelope, exactly as it does on a list view and on `object-grid`. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `conditionalFormatting: [{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }]` | `conditionalFormatting: [{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }]` (`not_equals` is `!=`, `contains` is `.contains(…)`, `in` is `record.FIELD in [ … ]`) | -| `conditionalFormatting: [{ condition: "record.priority == 'high'", backgroundColor: '#fee2e2' }]` | `[{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }]` — every colour goes in `style` | -| `conditionalFormatting: [{ expression: "record.priority == 'high'", style: { color: 'red' } }]` | `[{ condition: "record.priority == 'high'", style: { color: 'red' } }]` | -| `conditionalFormatting: { condition, style }` (one rule, no list) | `conditionalFormatting: [{ condition, style }]` | -| `conditionalFormatting: [{ condition: '', style }]` | delete the rule — a blank condition matches no card | - -The one-line fix: write each rule as `{ condition, style }`, a CEL `condition` over the card's `record.*` and a CSS `style` map, the rule a list view declares. No conversion is registered: nothing on the load path refuses the shape, and the census below found no working rule to respell — the D3 entry `ui-object-kanban-conditional-formatting-typed` carries that judgment. - -## Who is affected, measured - -A writer is a value written on the block: a page-component node (an object literal naming `object-kanban`, flat or in its `properties` bag, or a literal asserted as one), a direct parse through the row, the block's React component inside `schema={{…}}`, or the argument of a local test helper that mounts one. Values resolve through same-file constants and spreads, and parameters at every same-file call site. Each static value was parsed through the list view's member; a second pass parsed every rule-shaped object within 400 characters after a `conditionalFormatting` token, in any syntax, and each remaining hit was read by hand. - -- **objectstack** at `16d241a6af`, every tracked file: one writer, this package's own test that the key survived the `quickAdd` retirement, `[{ field: 'priority', value: 'high' }]` — no `operator`, so the board's evaluator built no predicate from it and painted nothing. Respelled to a `{ condition, style }` rule in the same change. -- **objectui** at the `.objectui-sha` pin `ab1879721595` and at `main` `2e818d0b51` (the readers are byte-identical between the two), every value a test fixture: nine `{ condition, style }` writers on the block (three through the board test's mount helper, one asserted node, one declared-keys row parsed through this very row, one live-member row, the dialect test's control, and the wire-slot test's string and envelope conditions), all parse. The ten refused values are refusal probes. Nine are refused by objectui's own faces too: the native rule, the flat colour rule, a colour beside `style` (three keys), an undeclared `label` and three malformed conditions. The tenth is objectui's probe that its mirror still admits a blank `condition`, which the board answers with no paint. Eight more rules mount the runtime `KanbanBoard` directly rather than the block, and the view-face relays carry a list view's rules; all of them parse. -- **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no `conditionalFormatting` at all (controls: `kanban` in 48 and 35 files). -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-list-family-typed.md b/.changeset/21464-component-props-list-family-typed.md deleted file mode 100644 index 8edeb47bdc3..00000000000 --- a/.changeset/21464-component-props-list-family-typed.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: eight list members of an `object-grid`, `object-kanban` or `object-calendar` page block take the shape the block reads instead of any value — the grid's `fields`, `selection`, `selectable`, `rowActions`, `bulkActions` and `batchActions`, the kanban's `columns` and the calendar's `calendar` (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **Eight members are typed.** `ComponentPropsMap['object-grid']`, `['object-kanban']` and `['object-calendar']` declared these members as `z.unknown()` (an array of it for the lists), although each renderer reads them with one shape. Any value passed, and an off-shape one was dropped or substituted with no report: an object entry in `fields` named no field; a `{ name }` entry in `bulkActions` was skipped; a kanban lane list mixing objects and strings drew a blank lane; a calendar block with no `startDateField` placed no event. -- **The list view's own members, by reference**, where a list view declares one: the grid's `selection` (`{ type }`, with `none`, `single` or `multiple`), `rowActions` and `bulkActions` (action-name strings), and the calendar's `calendar` (`{ startDateField, endDateField?, titleField?, colorField?, allDayField? }`). `batchActions`, the second spelling of `bulkActions` that the grid reads first, takes `bulkActions`'s def. Neither spelling is retired here. -- **The measured shape**, where no list view declares the member: the grid's `fields` (field-name strings), the grid's `selectable` (`true`, `false`, `'single'` or `'multiple'`), and the kanban's `columns` (all lanes `{ id, title, cards?, limit?, className?, collapsed? }`, or all bare value strings). A lane `id` and `title` are strings, a static card carries a string `id` and `title` beside its row's own values, and `limit` is a positive integer. -- **`ObjectGridProps`, `ObjectKanbanProps` and `ObjectCalendarProps`** (and their `…Parsed` twins) carry these types on the eight members instead of `unknown`. -- **The grid's `columns` is not narrowed** and still accepts any value. The list view's column entry is its by-reference shape, and the grid's draw path reads exactly that, but the grid's group headers also draw the labels from an authored column's `options` (the column whose `field` is the grouping field, ahead of the field's own options). The list view's column entry declares no `options`, so typing `columns` now would refuse a value the grid draws. It is held until that read is ruled. -- **The enumeration pin** loses eight lines and keeps the grid's `columns` as held for that ruling. One `z.unknown()` member is added and recorded: the rest of a static kanban card (its row's own values, beside the typed `id` and `title`). - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `object-grid` `fields: [{ field: 'name', width: 240 }]` | `fields: ['name']`, or the entry on `columns` | -| `object-grid` `selection: 'multiple'` | `selection: { type: 'multiple' }` | -| `object-grid` `selectable: 'none'` | `selectable: false`, or `selection: { type: 'none' }` | -| `object-grid` `bulkActions: [{ name: 'approve' }]` (also `batchActions`, `rowActions`) | `bulkActions: ['approve']`, or the full def on `bulkActionDefs` | -| `object-kanban` `columns: [{ id: 'done', title: 'Done' }, 'todo']` | one spelling per list: `columns: [{ id: 'done', title: 'Done' }, { id: 'todo', title: 'To Do' }]` | -| `object-kanban` a lane `color: 'red'` | `className: 'border-t-2 border-red-500'` | -| `object-kanban` a lane `{ id: 1, title: 'One' }` | `{ id: '1', title: 'One' }` | -| `object-calendar` `calendar: { dateField: 'kickoff', endField: 'wrapup' }` | `calendar: { startDateField: 'kickoff', endDateField: 'wrapup' }` | - -The one-line fix: write each member as the list view declares it, or as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entry `ui-object-grid-kanban-calendar-list-members-typed` carries that judgment. - -## Who is affected, measured - -A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is read through same-file constants, and the control is `objectName` on the same nodes. - -- **objectstack** at `49161683fb`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 57 `object-grid`, 30 `object-kanban` and 5 `object-calendar` nodes (the control on 47 / 27 / 4 of them). The one authored value among the eight members is a kanban `columns` (lanes, in the protocol docs), and it parses. No node authors another of the eight. -- **objectui** at the `.objectui-sha` pin `89cad75d55`: 689 `object-grid`, 240 `object-kanban` and 160 `object-calendar` nodes (the control on 293 / 108 / 98). Across the eight members, 241 values are static, and 233 of them parse. Each of the 8 that do not is a test fixture whose value the renderer drops, skips or refuses: 2 object entries in `bulkActions` (the renderer skips them, and the tests assert the skip), 3 object entries in `fields` that copy the hand-off the list view makes to the grid at run time (not an authored page), a lane `color` (retired in the console; the test marks it an undeclared member), and 2 uses of the calendar's retired `dateField` / `endField` aliases (the test asserts their refusal). No refused value is one the renderer draws. 24 values are not static (helper parameters, `.map` results and the run-time hand-offs); none of them is an authored page. The grid's `columns` (310 static values) is held because 2 of them author a column `options` the grid draws in its group headers, a fixture written to pin that behaviour. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-metric-family-typed.md b/.changeset/21464-component-props-metric-family-typed.md deleted file mode 100644 index 2b6f7d7f660..00000000000 --- a/.changeset/21464-component-props-metric-family-typed.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-metric` page block's `aggregate` and `trend` take the shape the tile reads instead of any value (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **Two members are typed.** `ComponentPropsMap['object-metric']` declared `aggregate` and `trend` as `z.unknown()`, although the tile reads each with one shape. Any value passed, and an off-shape one was answered in silence: `aggregate: 'count'` or a function the engine does not have asked the server for a measure it does not have, so the tile showed an error or, on the client-side fallback, a sum it was not asked for; `groupby` for `groupBy` drew one ungrouped number; a `trend` with no `value` painted a lone `%`, and a misspelled member or direction was not drawn. -- **`aggregate` takes its vocabulary by reference** — `{ field?, function, groupBy? }`: `function` is the query engine's own `AggregationFunction` (`count`, `sum`, `avg`, `min`, `max` or `count_distinct` — the six the tile forwards to the data source), with a `field` for every function but `count`; `groupBy` is the chart aggregate's own `ChartGroupBySchema`, a field name or a `{ field, dateGranularity?, alias? }` date-bucket node, and here it is optional, because a metric paints one number over every row. The chart's aggregate is not taken whole: it requires `groupBy`, and its five functions leave out the `count_distinct` the tile draws. -- **`trend` takes the badge's measured shape**, `{ value, label?, direction? }`: `value` a number (painted as a percentage), `label` a string or an inline locale map, `direction` `up`, `down` or `neutral`. -- **`ObjectMetricProps`** carries these types on the two members instead of `unknown`. -- **`drillDown` and `compareTo` are not narrowed** and still accept any value. Each by-reference candidate disagrees with what the tile reads: the chart's drill-down declares a `filter` the tile never reads and refuses the `report` the tile draws as a report body; the dashboard widget's comparison declares a `dimension` that this path never reads. Each is typed once that fork is ruled. - -## FROM → TO - -| you wrote on an `object-metric` | write instead | -|:--|:--| -| `aggregate: 'count'` | `aggregate: { function: 'count' }` | -| `aggregate: { function: 'sum' }` | name the field: `aggregate: { field: 'amount', function: 'sum' }` | -| `aggregate: { field: 'amount', function: 'median' }` (any function outside the six) | one of `count`, `sum`, `avg`, `min`, `max`, `count_distinct` | -| `aggregate: { field: 'amount', function: 'sum', groupby: 'stage' }` | `groupBy: 'stage'` | -| `aggregate: { function: 'count', dateGranularity: 'month' }` | `aggregate: { function: 'count', groupBy: { field: 'closed_at', dateGranularity: 'month' } }` | -| `trend: 'up'` | `trend: { value: 12, direction: 'up' }` | -| `trend: { value: '12%' }` | `trend: { value: 12 }` (the badge adds the `%`) | -| `trend: { value: 12, direction: 'rising' }` | `direction: 'up'` | - -The one-line fix: write each member as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the tile shows today and honours what the author wrote; the D3 entry `ui-object-metric-aggregate-trend-typed` carries that judgment. - -## Who is affected, measured - -A writer is a value written on an `object-metric`: a page-component node (an object literal naming the type, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one. Values resolve through same-file constants; helper arguments and `it.each` rows were resolved by hand. Each value was parsed through the built row. - -- **objectstack** at `b610eabf72`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 22 `aggregate` values, all `{ field, function }` with `count` or `sum` — the showcase's thirteen KPI tiles (`index.ts`, `my-work.page.ts` and the `kpi()` helper in `command-center.page.ts`), two in the layout-DSL protocol page and six copies in the spec and lint tests. 21 parse. The 22nd is the `kind: 'html'` example in `skills/objectstack-ui/rules/pages.md`, `aggregate="count"`, which this row does not judge (the html tier is compiled against the component manifest, not parsed through `ComponentPropsMap`). No `trend` is authored. -- **objectui** at the `.objectui-sha` pin `89cad75d55`: 66 `aggregate` values (64 static) and 12 `trend` values (all static). 63 and 11 parse. The two refused values are test fixtures probing that the tile does not draw them: an array `groupBy` the data adapter refuses at the producer (`objectMetricStructuredGroupBy-8613`), and a `trend` carrying `percent` and `caption`, which the test asserts the badge never draws (`objectMetricTrendMembers-8071`). The two values that are not static are the dashboard relays' run-time hand-offs (`DashboardRenderer`, `DashboardGridLayout`). objectui's unit-rule pin mounts a `count_distinct` tile, which parses. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-metric-rest-and-grid-columns-typed.md b/.changeset/21464-component-props-metric-rest-and-grid-columns-typed.md deleted file mode 100644 index ed5e49bee34..00000000000 --- a/.changeset/21464-component-props-metric-rest-and-grid-columns-typed.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-metric` page block's `drillDown` and `compareTo`, and an `object-grid` page block's `columns`, take the shape each block reads instead of any value (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`object-metric` `compareTo` takes the tile's read, `{ kind }`.** It was `z.unknown()`, although the tile reads `kind` alone: a bare `'previousYear'` or a kind outside the two compared against the previous period, and a `dimension` was carried and never read. `kind` is the dashboard widget comparison's own vocabulary by reference (`previousPeriod`, `previousYear`). `dimension` is refused by name, with the prescription: this inline tile shifts the date macros in its own `filter` and never reads a dataset time dimension, so state the window on the tile's `filter`. -- **`object-metric` `drillDown` takes the tile's read.** It was `z.unknown()`: a drill `filter`, a `mode` or a misspelled member passed and was ignored. Its five list members — `enabled`, `title`, `target` (`drawer`, `dialog`, `navigate`), `columns` (field names) and `maxRows` (a positive whole number) — are the chart drill-down's own by reference. `filter` and `mode` are refused by name: a metric tile has no click event for a drill filter to resolve against (the drilled list is scoped by the metric's own `filter`), and no row for `mode` to open as a record. The chart's drill-down shape is not taken whole, because it declares `filter`. -- **The drill-down's `report` is not narrowed** and still accepts any value. The tile draws a dataset-bound report through the shared drill drawer, but no spec drill shape declares a `report` member yet; it is typed once the spec declares the drill report. -- **`object-grid` `columns` takes the list view's own `columns`**: all field-name strings, or all column entries `{ field, label?, width?, align?, hidden?, sortable?, resizable?, wrap?, type?, pinned?, summary?, prefix?, link?, action? }`. It was an array of `z.unknown()`, held at the second stage because the grid's group headers drew a column's `options`, which the column entry does not declare. The renderer has since retired that read (the group-header labels come from the object field's `options` only), so the hold is lifted. A column keyed `accessorKey` / `header` or `name`, a column with no `field`, a list mixing strings and entries, or a column key the entry does not declare (`editable`, `options`, `reference`, or a footer number hint such as `currency` or `precision`) is refused. -- **`ObjectMetricProps` and `ObjectGridProps`** carry these types on the three members instead of `unknown`. A parsed column's `prefix.type` now carries the list view's `'text'` default. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `object-metric` `compareTo: 'previousYear'` | `compareTo: { kind: 'previousYear' }` | -| `object-metric` `compareTo: { kind: 'previousYear', dimension: 'close_date' }` | `compareTo: { kind: 'previousYear' }`, with the window stated on the tile's own `filter` (date macros such as `{current_quarter_start}`) | -| `object-metric` `compareTo: { kind: 'previousQuarter' }` | `previousPeriod` (the equal-length window before the one the filter resolves to) or `previousYear` | -| `object-metric` `drillDown: { filter: { stage: 'won' } }` | delete it, and scope the metric with its own `filter` (the drilled list follows it) | -| `object-metric` `drillDown: { mode: 'record' }` | delete it — a metric always lists the records behind its number | -| `object-metric` `drillDown: { limit: 50 }` | `drillDown: { maxRows: 50 }` | -| `object-grid` `columns: [{ accessorKey: 'amount', header: 'Amount' }]` | `columns: [{ field: 'amount', label: 'Amount' }]` | -| `object-grid` `columns: [{ name: 'salary' }]` | `columns: [{ field: 'salary' }]` | -| `object-grid` `columns: ['name', { field: 'amount', width: 120 }]` | all entries: `[{ field: 'name' }, { field: 'amount', width: 120 }]` | -| `object-grid` a column `editable`, `options`, `reference`, `currency` or `precision` | delete the key: inline editing is the grid's own `editable`, and option labels, relational metadata and number formats are the object field's | - -The one-line fix: write each member as the table above shows. No conversion is registered, because a refused value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entries `ui-object-metric-compare-to-typed`, `ui-object-metric-drill-down-typed` and `ui-object-grid-columns-typed` carry that judgment. - -## Who is affected, measured - -A writer is a value written on the block: a page-component node (an object literal naming the type, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one (positional helper parameters resolved at every call site). Values resolve through same-file constants. Each static value was parsed through the row. - -- **objectstack** at `6ec54f00ba`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/`, `apps/` and `docs/`: 5 `object-grid` `columns` values, all field-name strings (the showcase's `my-work.page.ts` and `command-center.page.ts` grids, and three test copies), all parse. No `object-metric` `drillDown` or `compareTo` is authored. -- **objectui** at the `.objectui-sha` pin `ab1879721595`, every one a test fixture or a run-time hand-off: - - `compareTo`: 5 values, 4 parse. The refused one is the test that asserts a `dimension` never touches the query (`ObjectMetricWidget.compareTo.test.tsx`). - - `drillDown`: 26 values, 25 static; 23 parse. The two refused are the compile-time refusal probes for `filter` and `mode` (`ObjectMetricWidget.drillDownRefusal-9002.test.tsx`). The one that is not static carries a report probe, which parses, since `report` stays open. - - `object-grid` `columns`: 362 values, 353 static (147 distinct); 312 parse. Each of the 41 refused is a test fixture whose refused key or entry the grid does not draw: 17 columns keyed `accessorKey` / `header` and 5 keyed `name` (the column-spelling diagnostic, identity and field-security tests); 14 columns carrying `editable: false` and 1 carrying `reference`, keys no read takes off an authored column; 2 carrying `options`, the tests asserting that the group headers no longer read them; 1 column with no `field`; and 1 numeric `columns` refused by objectui's own mirror. The 9 values that are not static are 3 run-time hand-offs (the object view and two designer grids) and 6 test lists built from `{ field, label, type }` entries, which parse. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-navigation-typed.md b/.changeset/21464-component-props-navigation-typed.md deleted file mode 100644 index 1b4b151e4f6..00000000000 --- a/.changeset/21464-component-props-navigation-typed.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: `navigation` on an `object-map`, `object-gantt` or `object-tree` page block takes the list view's navigation block instead of any value, and every remaining `z.unknown()` member of `ComponentPropsMap` is enumerated with its recorded reason (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`navigation` is typed on three rows.** `ComponentPropsMap['object-map']`, `['object-gantt']` and `['object-tree']` declared `navigation` as `z.unknown()`, although each renderer hands it to the console's shared navigation hook, which reads `navigation.mode` and falls back to `page` when it finds none. Any value passed, and an off-shape one was answered with a silent default: `navigation: 42` and a bare mode string such as `'drawer'` both opened the record page, whatever they named. Each row now takes the list view's `NavigationConfigSchema` by reference, the same block `object-grid`, `object-kanban`, `object-calendar` and `object-timeline` already take: `{ mode?, size?, openNewTab?, preventNavigation? }`, with `mode` one of `page`, `drawer`, `modal`, `split`, `popover`, `new_window` or `none`. -- **`ObjectMapProps`, `ObjectGanttProps` and `ObjectTreeProps`** (and their `…Parsed` twins) carry `NavigationConfig` on `navigation` instead of `unknown`. -- **No other member changes.** Every other `z.unknown()` member across `ComponentPropsMap` (107 of them) is now listed, with its recorded reason, by a test that fails on a new one until it carries one. The reasons are composition slots, the action blocks' runner-forwarded members, record rows and field values, members of schemas another file owns, a value shown as-is, a deliberately open bag, and a row no renderer draws. The list also holds 28 members a renderer reads with a fixed shape. 27 of them are typed in later changes, and one, `object-kanban`'s `conditionalFormatting`, waits for a ruling, because the console's own kanban fixtures author two rule dialects the list view's schema refuses. - -## FROM → TO - -| you wrote on an `object-map` / `object-gantt` / `object-tree` | write instead | -|:--|:--| -| `navigation: 'drawer'` | `navigation: { mode: 'drawer' }` | -| `navigation: { mode: 'tab' }` | a mode the hook knows: `page`, `drawer`, `modal`, `split`, `popover`, `new_window` or `none` | -| `navigation: { mode: 'drawer', target: '_blank' }` | `navigation: { mode: 'new_window' }`, or `openNewTab: true` beside a `page` mode | - -The one-line fix: write `navigation` as the block a list view declares, `{ mode, size?, openNewTab?, preventNavigation? }`. No conversion is registered, because an off-shape value has no rewrite that both keeps what the block shows today (the record page) and honours what the author wrote; the D3 entry `ui-object-map-gantt-tree-navigation-typed` carries that judgment. - -## Who is affected, measured - -- **objectstack.** Measured on this branch after merging `origin/main` `100c394f6f`, over the 5 files per row that name `object-map` / `object-gantt` / `object-tree` in the examples, `packages/apps`, `@objectstack/platform-objects`, the plugins and services, the spec sources, the documentation and the published skills: no block of the three authors `navigation`. The one file that co-mentions a row and a `navigation:` key writes app navigation arrays, not this member. The control: the same census finds `objectName` in 4 of the 5 files per row. -- **objectui.** Measured at the `.objectui-sha` pin, over the 88 / 98 / 59 files that name `object-map` / `object-gantt` / `object-tree` (the control: `objectName` in 55 / 70 / 40 of them): every authored `navigation` is `{ mode }` with one of the seven modes, some with `size: 'lg'` or `openNewTab`, and each of those parses on all three rows. The non-object values are probes that expect a refusal: `navigation: 'anything'` in objectui's mirror tests, which assert that both faces answer alike, and a `navigation: 'drawer'` under a `@ts-expect-error`. Neither authors anything. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-objectui-held-typed.md b/.changeset/21464-component-props-objectui-held-typed.md deleted file mode 100644 index f82477d8b8e..00000000000 --- a/.changeset/21464-component-props-objectui-held-typed.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-gantt` page block's `markers`, an `object-timeline` page block's `mapping`, and the top-level `fields` of the `object-form` and `object-master-detail-form` page blocks take the shape each block reads instead of any value (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`object-gantt` `markers` takes `{ date, label?, color? }` entries.** Its entries were `z.unknown()`, because the marker contract lived only in objectui: a marker with no `date`, a numeric `date` or a misspelled member passed, and the chart drew no line, or drew it with no label and in the default colour. The spec now declares objectui's own authoring declaration of a marker — `date` an ISO date or date-time string, `label` the text drawn against the line, `color` any CSS colour — closed, and the row takes it. A marker `title`, `text` or `name` is pointed at `label`, and a `colour` at `color`. -- **`object-timeline` `mapping` takes `{ title?, date?, description?, variant? }`**, each a field name. It was `z.unknown()`, for the same reason: a bare field name, a non-string binding or a misspelled member (`titleField` inside `mapping`) passed, and the rail drew the default field. The spec now declares objectui's own declaration of the binding record, closed. `titleField`, `dateField` / `startDateField`, `descriptionField` and `variantField` written inside `mapping` are pointed at the member they meant. -- **`object-form` and `object-master-detail-form` `fields` take field names.** The top-level list was an array of `z.unknown()`, held while the form drew a `{ name }` entry its page-builder guide taught, with a `label`, `type` and `required` it silently dropped. objectui has since retired that entry from every authoring face (the form still draws a stored one by its name), so both rows take field-name strings, objectui's own declaration of the member. A `{ name: 'email' }` entry is refused with `write 'email'` and where a per-form override goes; a `{ field: 'email' }` entry — the `sections[].fields` vocabulary, which the form skips at the top level — is refused with the same name and that pointer. -- **Not narrowed, and still accepting any value:** the `object-metric` drill-down's `report`, `object-form` `customFields`, both forms' `sections`, `object-timeline` `items` and the members of `action:group` / `action:menu`. Each contract still lives in objectui and has more than one viable spec shape that no ruling decides yet; each is typed once one is chosen. -- **`ObjectGanttProps`, `ObjectTimelineProps`, `ObjectFormProps` and `ObjectMasterDetailFormProps`** carry these types on the four members instead of `unknown`. No new member carries a default, so each parsed value is the authored one. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `object-gantt` `markers: [{ date: 5 }]` | `markers: [{ date: '2026-07-01' }]` — an ISO date or date-time string | -| `object-gantt` `markers: [{ label: 'Freeze' }]` | give it a `date`: `[{ date: '2026-07-01', label: 'Freeze' }]` | -| `object-gantt` `markers: [{ date: '2026-07-01', title: 'Freeze', colour: 'red' }]` | `[{ date: '2026-07-01', label: 'Freeze', color: 'red' }]` | -| `object-timeline` `mapping: 'subject'` | `mapping: { title: 'subject' }` — name the member the field binds | -| `object-timeline` `mapping: { titleField: 'subject', variantField: 'status' }` | `mapping: { title: 'subject', variant: 'status' }` | -| `object-form` `fields: [{ name: 'email', label: 'Email', required: true }]` | `fields: ['email']`, with the label and `required` on the object field or on a `sections[].fields` entry | -| `object-form` `fields: [{ field: 'email' }]` | `fields: ['email']`, or move the entry into a section's `fields` | -| `object-master-detail-form` `fields: [{ name: 'note' }, 'status']` | `fields: ['note', 'status']` | - -The one-line fix: write each member as the table above shows. No conversion is registered: a misspelled marker or mapping member has no rewrite that says which member the author meant, and a form already draws a stored `{ name }` entry by its name, while an override written beside it has nowhere to go but a section — the D3 entries `ui-object-gantt-markers-typed`, `ui-object-timeline-mapping-typed` and `ui-object-form-fields-names-typed` carry that judgment. - -## Who is affected, measured - -A writer is a value written on the block: a page-component node (an object literal naming the type, flat or in its `properties` bag, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one (positional helper parameters resolved at every call site). Values resolve through same-file constants. Each static value was parsed through the row; a text search for each member key beside the block's name found the writers the walk does not reach, and each was read by hand. - -- **objectstack** at `7d0781482d`, over `examples/`, `packages/`, `content/`, `skills/`, `apps/` and `docs/`: no `markers` and no `object-form` `fields`; one `mapping` (this package's own navigation test, `{ title, variant }`) and four `object-master-detail-form` `fields` (the showcase's project workspace, the objectui layout DSL page, and two test copies), all field names. All parse. -- **objectui** at the `.objectui-sha` pin `ab1879721595` and at `main` `94985a92ba` (every read point identical between the two), every value a test fixture, a document or a run-time hand-off: - - `markers`: 9 values, 8 parse. The refused one is objectui's own compile-time probe that a numeric `date` is refused (`gantt-declared-keys.test.ts`). Five more mount `GanttView`, the runtime chart, directly rather than the block, and are not writers of this member. - - `mapping`: 9 values (the timeline inputs test and the absent-date-axis refusal test), all parse. - - `fields`, both forms: 73 values at `main` — 56 parse, 11 are run-time hand-offs that are not static, and the 6 refused are fixtures probing the read: three `{ field }` entries asserting the form skips them with a warning, a `{ name }` entry asserting objectui's own mirror refuses it, and two `{ name }` entries asserting a stored one still draws. At the pin a seventh is refused: the page-builder guide's `{ name, label, type, required }` example, respelled to names on objectui `main`. (Fourteen more matches are object definitions or permission maps whose own `fields` key the walk read as the block's, and are not writers.) -- **hotcrm** at `4054ec2680` and **cloud** at `b2d7a7f6f8`: no writer of any of the four members. -- **Deployed metadata** was not measured. diff --git a/.changeset/21464-component-props-report-items-action-members-typed.md b/.changeset/21464-component-props-report-items-action-members-typed.md deleted file mode 100644 index edea6cb7230..00000000000 --- a/.changeset/21464-component-props-report-items-action-members-typed.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-metric` drill-down's `report` takes a report definition, an `object-timeline`'s `items` take the entry kind its `variant` selects, and each `action:group` / `action:menu` member takes a closed inline action, instead of any value (#21464) - -Clause-②: yes (narrowing) - - - -**BREAKING** — three accept-set narrowings on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**`@objectstack/spec`** - -- **`object-metric` `drillDown.report` is a report definition — `ReportSchema`, by reference.** It was `z.unknown()`. The tile hands it to the shared drill drawer, which draws a dataset-bound report (with the metric's filter joined into the report's own `runtimeFilter`) and lists the records for any other value. A joined report already refuses a block that binds no `dataset`, so every report the member admits is one the drawer draws; a report with no `dataset`, a bare report name, a `{ name }` reference or the retired `objectName` / `columns` form is refused. The drawer still draws a few incomplete reports the member refuses (no `name` / `label`, a non-joined report with no `values`, a joined one with a container `dataset` or with only some blocks bound) — no measured writer authors one. -- **`object-timeline` `items` takes the entry kind the block's `variant` selects.** It was `z.array(z.unknown())`. On `vertical` (the default) or `horizontal` an entry is a feed entry `{ time?, title, description?, variant?, icon?, content?, className? }`; on `gantt` it is a gantt row `{ label, items? }` whose bars are `{ title?, startDate?, endDate?, variant? }`, each date a string or epoch milliseconds — objectui's two ruled element kinds, closed. A row refinement pairs each entry with its kind: a feed entry with no `title`, a gantt row with no `label`, and a key of the other kind are refused at the entry, by path. `variant` is one of `default`, `success`, `warning`, `danger`, `info`. A feed entry's `content` (child components) is not judged yet. The keys the record-bound rail composes onto its entries (`color`, `startDate`, `endDate`, `group`, `meta`) are refused on an authored entry with what to write instead. -- **Each `action:group` / `action:menu` member is a closed inline action.** It was an open record. A member takes `action:button`'s keys with its executor spelled `type` (a member is an action entry): `name`, `label`, `icon`, `type`, `variant`, `visible`, `disabled`, `tags`, `params`, `description`, `target`, `openIn`, `method`, `bodyExtra`, `bodyShape`, `operation`, `patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, `locations`, `toast`, `resultDialog`, `onSuccess`, `objectName` — and `size` on an `action:group` member, whose inline button reads it (an `action:menu` item reads none). `tags` takes `separator-before`, the one tag the containers draw. Refused, each with what to write instead: `actionType` (write `type`), `endpoint` / `url` / `path` / `href` (write `target`), `enabled` (write `disabled`, inverted), `autoTrigger`, `outcomeMessages` (write `successMessage`), a member `className`, a member `properties` bag, `undoable` and `recordIdField`. `outcomeMessages` stays undeclared on all four action blocks (`action:button`, `action:icon`, `action:group`, `action:menu`). -- **`ObjectMetricProps`, `ObjectTimelineProps`, `ActionGroupProps`, `ActionMenuProps`** and their parsed types carry these shapes instead of `unknown`; the member and entry shapes are module-private. `ObjectMetricPropsParsed` now also differs from the authored type on `drillDown.report`, whose `ReportSchema` defaults (`type`, `drilldown`) materialize on parse. No export is added or removed. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `drillDown: { report: 'pipeline' }` or `{ report: { name: 'pipeline' } }` | `drillDown: { report: { name: 'pipeline', label: 'Pipeline', dataset: 'deals_ds', values: ['amount_sum'] } }` | -| `drillDown: { report: { name, label, objectName: 'deal', columns: [ … ] } }` | the dataset-bound report: `{ name, label, dataset, rows, values }` | -| `drillDown: { report: { …, type: 'joined', blocks: [{ name: 'notes' }] } }` | bind every block: `blocks: [{ name: 'notes', dataset: 'notes_ds', values: [ … ] }]` | -| `items: [{ date: '2026-01-15', title: 'Kickoff' }]` | `items: [{ time: '2026-01-15', title: 'Kickoff' }]` | -| `items: [{ title: 'Kickoff', color: 'green' }]` | `items: [{ title: 'Kickoff', variant: 'success' }]` | -| `items: [{ label: 'Backend', items: [ … ] }]` with no `variant` | `variant: 'gantt', items: [{ label: 'Backend', items: [ … ] }]` | -| `variant: 'gantt', items: [{ title: 'Kickoff' }]` | `variant: 'gantt', items: [{ label: 'Kickoff', items: [{ startDate, endDate }] }]`, or drop `variant: 'gantt'` | -| `actions: [{ name: 'go', actionType: 'url', target: '/x' }]` | `actions: [{ name: 'go', type: 'url', target: '/x' }]` | -| `actions: [{ name: 'save', type: 'api', endpoint: '/api/save' }]` | `actions: [{ name: 'save', type: 'api', target: '/api/save' }]` | -| `actions: [{ name: 'del', outcomeMessages: { archived: 'Archived' } }]` | `actions: [{ name: 'del', successMessage: 'Archived' }]` | -| `actions: [{ name: 'del', className: 'text-red-600' }]` | `actions: [{ name: 'del', variant: 'destructive' }]` | -| `actions: [{ name: 'run', enabled: "record.status == 'open'" }]` | `actions: [{ name: 'run', disabled: "record.status != 'open'" }]` | -| `actions: [{ name: 'edit', properties: { params: { … } } }]` on `action:group` / `action:menu` | `bodyExtra` for a `type: 'api'` request body, or the action as its own `action:button` node with a `params` object | -| `action:menu` `actions: [{ name: 'a', size: 'sm' }]` | `actions: [{ name: 'a' }]` — the menu's own `size` sizes the trigger | - -The one-line fix: write a drill report as the report definition, each timeline entry as the kind the block's `variant` draws, and each container member with `action:button`'s keys and `type` as its executor. No conversion is registered: nothing on the load path refuses these shapes, and the census below found no working value to respell — the D3 entries `ui-object-metric-drill-down-report-typed`, `ui-object-timeline-items-typed` and `ui-action-group-menu-members-typed` carry that judgment. - -## Who is affected, measured - -A writer is a value written on the block: a page-component node (an object literal naming the block, flat or in its `properties` bag, or with its `type` arriving through a spread constant), a literal annotated as one, a direct parse through the row, the block's React component (`schema={…}`, or its props), and the argument a local helper passes in that position at every same-file call site. Values resolve through same-file constants and spreads, a `.map` over a constant list, templates and same-file helper calls (`member('alpha', { size })`). Every static value was parsed through this branch's rows; each value with a non-static part, and each refusal, was read by hand. - -- **objectstack** at `1289925c0a`, this branch's merge base: one drill report (the metric pin's own), one timeline `items` (a spec test, a feed entry) and three `action:group` / `action:menu` members (a spec test) — all parse. No example, doc or skill writes any of the three. -- **objectui** at the `.objectui-sha` pin `2e818d0b51ec` and at `main` `2abec3a96` (every cited reader byte-identical between the two, and the same census at both): **drill report** — the drawn report drill (`objectMetricDrillDownMembers-8071.test.tsx:281`) parses; refused are only the probes of what the drawer does NOT draw (that file's two `it.each` values, and the `@object-ui/types` drill-mirror tests' `{ name }` / incomplete-report refusal probes). **Timeline `items`** — 18 values: 15 parse (feed entries and gantt rows); the 3 refused are the render-time gantt date diagnostic's own probes (an array, `false` and `null` bar date). **Members** — `action:group` 40 values and `action:menu` 19, all in tests but for one run-time hand-off: every static member parses except the probes of the very reads the ruling refuses — the member pin's `className` (`action-group-menu-inputs-11168.test.tsx:249`), the `outcomeMessages` forward tests, the `properties.params` static-value tests, and the host's `autoTrigger` flag in the overflow / forward tests. The hand-off is `action:bar`'s overflow menu (`action-bar.tsx:287`), which hands the bar's own members — a host's registered actions — to `action:menu` at run time, never through the component-props gate. -- **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no writer of any of the three (hotcrm's four `object-metric` tiles declare no drill-down). -- **Deployed metadata** was not measured. diff --git a/.changeset/21468-walled-public-form-withdrawal.md b/.changeset/21468-walled-public-form-withdrawal.md deleted file mode 100644 index 6c75c77fe62..00000000000 --- a/.changeset/21468-walled-public-form-withdrawal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -Withdrawing or publishing a public form on a walled tenancy posture (degraded or not) is now refused loudly at authoring, with `403 NOT_OVERRIDABLE`, when the save is organization-scoped and the anonymous form doors cannot honour it. The message names the remedy: save the change env-wide, which every anonymous door honours. Drafts and draft promotion are refused alike. Other organization-scoped edits, env-wide saves and single-posture deployments are unchanged. diff --git a/.changeset/21470-metadata-protocol-every-write-door-item-name.md b/.changeset/21470-metadata-protocol-every-write-door-item-name.md deleted file mode 100644 index d74fbc5771c..00000000000 --- a/.changeset/21470-metadata-protocol-every-write-door-item-name.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -Every runtime door that writes a metadata row refuses a body whose own `name` disagrees with the row's name, for every metadata type - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime write doors, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the view-container half of this refusal takes in the same release. - -**What was accepted before.** The runtime stores a metadata row under the name the request names and registers its body under the body's own `name`. These doors accepted a body whose `name` was not the row's, so the row answered under a name nobody saved it under, and under none by its own: - -- `saveMetaItem`, which `PUT /api/v1/meta/:type/:name` and the dispatcher's metadata save both call, for every type but a view container (a dashboard saved as `dash_a` with `name: 'dash_b'` registered as `dash_b`; a record view saved as `crm_lead.mine` with `name: 'crm_lead.other'` registered as `crm_lead.other`); -- `rollbackMetaItem` and `revertCommit`, which wrote such a stored history version back as the active row without passing `saveMetaItem`; -- `publishMetaItem` and `publishPackageDrafts`, which promoted such a stored draft the same way. - -**What is refused now.** Each of those bodies, with `VALIDATION_ERROR` / 400, before anything is stored or registered, through the judge the view-container refusal already used (`savedItemNameRefusal`, `@objectstack/metadata/view-container-name`). `rollbackMetaItem` and `publishMetaItem` throw it. `revertCommit` reports the item in `failed[]` with `code: 'VALIDATION_ERROR'`. `publishPackageDrafts` aborts the batch on it, as it does on any refused draft: nothing in the batch is published. A body with no `name` is accepted as before. A `name` the body carries is judged whatever its value; a `translation` saved with `name: ''`, which its schema accepts, is now refused instead of being registered under the empty string. A view at the save door is the exception: a missing or empty view `name` is still stamped with the save name. A `field` written through the `OS_METADATA_WRITABLE` operator hatch is accepted only without a body `name`: its row is named `object.field`, which the column `name` cannot spell, and registered it answered under the column name alone. Where the type's schema already refused such a body (an empty or non-string `name` on most types, any `name` on a `seed`, whose schema declares none), the answer is now this refusal (`VALIDATION_ERROR` / 400) instead of the schema's `INVALID_METADATA` / 422; nothing is stored either way. - -**The fix.** Set the body's `name` to the name you save it under, or save the item under the body's own `name`; for a view or a `field`, dropping `name` works too. To bring back a version or a draft that carries another `name`, save the item again with that fix, and publish that save if it is a draft. diff --git a/.changeset/21470-metadata-write-door-item-name-judge.md b/.changeset/21470-metadata-write-door-item-name-judge.md deleted file mode 100644 index de4b36333ea..00000000000 --- a/.changeset/21470-metadata-write-door-item-name-judge.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata': minor ---- - -The runtime write doors' `name` judge covers every metadata type: `savedItemNameRefusal` replaces `savedViewContainerNameRefusal` on `@objectstack/metadata/view-container-name` - -Clause-②: yes - -- `savedItemNameRefusal(type, item, saveName, door)` is the one entry the runtime write doors of `@objectstack/metadata-protocol` call. It returns a `VALIDATION_ERROR` / 400 refusal when a body of any type carries its own `name` and that `name` differs from the name the row is written under, and `undefined` otherwise. `door` is `'save'`, `'restore'` or `'publish'`. A body with no `name` passes. A `name` the body does carry is judged whatever its value (`''`, `null` and non-strings included), with one exception: a `view` at the `'save'` door, which stamps a missing name there, is judged only on a non-empty string `name`. -- It replaces `savedViewContainerNameRefusal(container, saveName)`, which judged view containers only. That export was added to this subpath in this same release cycle and never shipped in a published version, so no published export is removed. -- The words name the type and give a remedy that works for it: "drop `name`, or set it to KEY" for a view, whose missing name the save door stamps; "drop `name`" for a `field`, whose row is named `object.field`, which its dot-free column `name` cannot spell; "set `name` to KEY, or save the item under NAME" for every other type; and at the restore and publish doors, whose caller cannot edit the stored body in place, the save that fixes it. For a view container at the save door the message is byte for byte the one `savedViewContainerNameRefusal` returned. -- `viewContainerNameRefusal` (the source registrars' entry), its words and the `ViewContainerNameRefusal` type are unchanged. diff --git a/.changeset/21471-one-shot-never-mints-key.md b/.changeset/21471-one-shot-never-mints-key.md deleted file mode 100644 index 270aa706298..00000000000 --- a/.changeset/21471-one-shot-never-mints-key.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os secret orphans`, `os storage orphans` and `os migrate files-to-references` no longer create a data key file in the key home. A one-shot command never mints key material (#21471) - -Clause-②: no - -Each of these commands composes the settings service. Given no crypto provider, the service builds its own default one. In a development posture with no `OS_SECRET_KEY`, no `OS_DEV_CRYPTO_KEY` and no key file, that default writes a new key file into the key home. So a report that promises to write nothing left key material behind, and the next development-posture process on that host adopted the minted key. A minted key opens nothing that is stored, so the run gained nothing from it. - -- **What these commands hand the settings service now.** They pass the provider `os secret rewrap` already passed: the one over a data key that already exists, resolved the way every host resolves it, in the strict posture and with the auto-key opt-in withheld, so it never mints. With no key, the service gets a provider that refuses every call and says why. A stored setting that cannot be opened reads as it did with a freshly minted key: empty, with a warning. -- **One composition.** The settings service is composed in one place in `@objectstack/cli` (`utils/one-shot-settings.ts`), shared by `secret orphans`, `secret rewrap` and the storage arm of the data-migration plugins. `os serve` still takes the service's default: persisting a key in a development posture so restarts reuse it is that host's documented behaviour. -- **Visible difference.** On a host whose key lives only in the key file, these commands now print the strict posture's one-line note on stderr ("using the persisted key at …"), as `os secret rewrap` already did. stdout and `--json` output are unchanged. diff --git a/.changeset/21476-public-form-intake-advisory.md b/.changeset/21476-public-form-intake-advisory.md deleted file mode 100644 index 2358e3d91af..00000000000 --- a/.changeset/21476-public-form-intake-advisory.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/metadata-core': minor -'@objectstack/metadata-protocol': patch -'@objectstack/rest': patch ---- - -Public forms on a walled tenancy posture: saving or publishing a view whose public form cannot take anonymous intake now tells the author why, on the response. - -Clause-②: yes (widening) - -On a walled posture (`group` or `isolated` in force), an open public form whose object is walled by an organization column cannot take an anonymous submission: the submission carries no organization, and an insert without one into a walled object is refused. The two anonymous form endpoints already answer such a form as a withdrawn one (`404 FORM_NOT_FOUND`), and the administrator's read of the view (`GET /meta/view/:name`) already states why in `_diagnostics.warnings`. - -- **`@objectstack/metadata-protocol`**: saving the view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and a package's batch publish) now answers success with one `warning` advisory per such form, under `advisories`, with rule `public-form-intake-unavailable`. It is located at the form's `sharing` (for example `views[0].formViews.contact.sharing`), its `message` is the same text the administrator's read states, and its `hint` is the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. The write is never refused. The advisory reads the posture in force from the `tenancy` service, which is what the anonymous endpoints read: a single-posture deployment, a deployment whose walled posture is degraded to `single`, a deployment with no tenancy service, and a form bound to a tenancy-disabled object get no advisory, and a draft save is not judged. The publish refusal for an unstamped platform schedule flow still reads the requested posture, as before. -- **`@objectstack/metadata-core`**: the intake-availability rule moved here from `@objectstack/rest` and is exported, so the anonymous endpoints, the administrator's read and the publish advisory read one answer: `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` (`null` when the form can take intake, otherwise the object, the posture and the wall column; it judges the object's effective schema, with the injected `organization_id`), `anonymousFormIntakePosture(tenancy)` (the posture in force, as a tenancy service reports it), `anonymousFormIntakeUnavailableMessage` and `anonymousFormIntakeUnavailableRemedy` (the reason and its remedy), `anonymousFormSharingPath` and `anonymousFormObjectName`, and the type `AnonymousFormIntakeUnavailable`. -- **`@objectstack/rest`**: the anonymous form endpoints and the administrator's read import that rule instead of holding their own copy. Their answers are unchanged. diff --git a/.changeset/21476-walled-public-form-intake-unavailable.md b/.changeset/21476-walled-public-form-intake-unavailable.md deleted file mode 100644 index b973699b4ef..00000000000 --- a/.changeset/21476-walled-public-form-intake-unavailable.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/rest': patch ---- - -Public forms on a walled tenancy posture: a form whose object is walled by an organization column is no longer offered to anonymous visitors. An anonymous submission carries no organization, and on a walled posture an insert into such an object without one is refused, so the form used to render and then answer `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` on every submit. Both anonymous form endpoints (`GET /forms/:slug` and `POST /forms/:slug/submit`) now answer it exactly as they answer a withdrawn form (`404 FORM_NOT_FOUND`), so an anonymous caller learns nothing about the deployment's tenancy. The administrator's read of the form (`GET /meta/view/:name`) states why in `_diagnostics.warnings`, located at the form's `sharing`, with the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. Forms bound to tenancy-disabled objects, and single-posture deployments, are unchanged. - -Clause-②: no diff --git a/.changeset/21485-date-bucket-calendar-day.md b/.changeset/21485-date-bucket-calendar-day.md deleted file mode 100644 index 9091a4d45ff..00000000000 --- a/.changeset/21485-date-bucket-calendar-day.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/driver-sql': patch ---- - -A `Field.date` grouped by `day`, `week`, `month`, `quarter` or `year` buckets as its own calendar day on PostgreSQL and MySQL, whatever zone the server or the session is in (#21485). - -Clause-②: no - -- **What was wrong.** The PostgreSQL bucket cast every column to `timestamptz` and the MySQL bucket passed every column through `convert_tz`. A `date` has no instant, so both invented midnight in the session's zone, and on a session east of UTC the conversion to UTC read the previous day. On PostgreSQL with the server at `Asia/Shanghai`, `2026-06-01` grouped into month `2026-05`, and `2026-01-01` into year `2025`. MySQL did the same once the session zone was `+08:00`; the driver pins its own sessions to UTC, so there it took a host `pool.afterCreate` that sets the session zone. -- **What it does now.** A declared `Field.date` buckets its calendar day with no zone conversion. A `Field.datetime`, and a column with no declaration, keep the UTC-instant expression, byte for byte. SQLite already bucketed a `date` as its calendar day and is unchanged. -- **Where it shows.** `aggregate()` with a `dateGranularity` group, and the expression `SqlDriver.dateBucketSql()` renders for the analytics SQL echo, which reads the same expression. diff --git a/.changeset/21486-warm-boot-seed-claim.md b/.changeset/21486-warm-boot-seed-claim.md deleted file mode 100644 index 1e9f6d0c0a7..00000000000 --- a/.changeset/21486-warm-boot-seed-claim.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/plugin-security": patch ---- - -The seed-ownership claim now runs whenever a seed settles, on every boot, not only on the boot that promotes the first platform admin. - -Clause-②: no - -- **Before:** a later boot whose seed replay inserted rows into a database that already had a platform admin left those rows `owner_id` NULL for good. An in-budget seed settles before `kernel:ready`, and the bootstrap that runs there finds the existing admin (`already_have_admin`) and promotes nobody, so neither path reached the claim. A `readScope: 'own'` grant never saw those rows. -- **Now:** when a seed settles (`app:seeded`) before this boot's bootstrap has named a claim target, the handler resolves the target itself: the existing platform admin, by the bootstrap's own `already_have_admin` rule. The claim then hands the replayed rows to that admin. The handler subscribes in `init()`, so a seed that settles before this plugin's `start()` is heard too. That happens on any composition that registers the app first. -- Unchanged: the claim's predicates (`owner_id` NULL or `usr_system`), its object filter and the first-boot promotion path. A row someone else owns is never touched. Under a walled tenancy posture no claim runs, as before. -- Log lines: the claim report reads `handed N seeded record(s) to platform admin USER_ID`, where it used to say `first admin`. Its provisional and failure lines now say when the claim actually runs next: the next seed settle, on this boot or a later one, or the next platform-admin promotion. `os meta resync` is not such a run. diff --git a/.changeset/21489-job-bodies-install-local.md b/.changeset/21489-job-bodies-install-local.md deleted file mode 100644 index cf178b5a775..00000000000 --- a/.changeset/21489-job-bodies-install-local.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/runtime': minor -'@objectstack/cloud-connection': minor -'@objectstack/cli': minor -'@objectstack/spec': minor ---- - -fix(runtime,cloud-connection)!: a job's sandboxed `body` is scheduled on every door that brings an artifact in, and install-local refuses an enabled job with no `body` (#21489) - -Clause-②: yes (narrowing) - - - -**BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package that declares an **enabled job with no `body`**. Such a job names its code only through `handler` — a `defineStack({ functions })` entry, which travels in the artifact's runtime module and never in the package JSON this door installs — so it used to install with a 200 and never run, hot or after a restart, with nothing saying so. - -- **Job bodies run.** A job's sandboxed `body` (`JobSchema.body`, the hook body shape) is now scheduled on every door that brings an artifact in: the boot (`os start --artifact`, a `defineStack` config) and install-local, on install and on every rehydrate after a restart. One binder does it for all of them. With both `body` and `handler` declared, the `body` wins. The body runs in the QuickJS sandbox with `ctx.api` (as system: a job has no caller), `ctx.log` and `ctx.crypto` behind its declared `capabilities`. The job's `timeoutMs` is its one time limit; with none, a job body gets a 5000 ms CPU budget. A body may return `{ outcome: 'degraded', reason }` to report a run that did not do its work. -- **A package's jobs stop with it.** Re-scheduling a package's jobs replaces its set: a reinstall whose new version drops, disables or can no longer run a job cancels that job, and a version with no jobs cancels them all. Uninstalling a package cancels its scheduled jobs through a new uninstall cleanup, `runtime.package-jobs`, on the protocol's uninstall-cleanup registry, so install-local's `DELETE` and the protocol's package uninstall both stop them and report it in `cleanups`. Another package's jobs are never touched. -- **The refusal.** The install answers `422` with `VALIDATION_ERROR`, names each refused job and the function its `handler` declares, and installs nothing: nothing is registered, persisted or scheduled. A disabled job (`enabled: false`) is not judged. A package installed by an earlier version keeps rehydrating; its handler-only job is reported at `warn` and does not run. -- **CLI.** `os package install` prints a refusal's code beside its status (`Install failed (422 VALIDATION_ERROR): …`), for every refusal alike. -- **Spec.** The shipped liveness ledger records `job.body` (`language`, `source`, `capabilities`, `memoryMb`) as live, so `os validate` / `os build` no longer warn that a job's `body` is planned and not read yet. `body.timeoutMs` stays refused on a job. `JobSchema.body`'s description and the `defineJob` example no longer say to keep a `handler` until the runtime runs job bodies. -- **Unchanged:** a `handler` job on a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) still runs its `functions` entry; a package without jobs installs exactly as before. - -The route for a refused package: give each enabled job a `body` (sandboxed JS that reaches data through `ctx.api`), or boot the artifact with `os start --artifact`, which loads its runtime module. It ships as `minor` under the launch-window convention for accept-set narrowings. diff --git a/.changeset/21490-install-local-uninstall-cleanups.md b/.changeset/21490-install-local-uninstall-cleanups.md deleted file mode 100644 index d6af060c807..00000000000 --- a/.changeset/21490-install-local-uninstall-cleanups.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/cloud-connection': patch -'@objectstack/metadata-protocol': minor ---- - -fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups, so the package's permission sets and their grants go with it - -Clause-②: yes - -`DELETE /api/v1/marketplace/install-local/:manifestId` removed the package's ledger entry and nothing else. After a restart the package's objects were gone, but its `managed_by: package` rows in `sys_permission_set`, and every grant of them, survived the uninstall. That broke ADR-0090's "No ghost grants" promise on this door. - -The door now runs the uninstall cleanups that domain plugins register with the protocol (`registerUninstallCleanup`) once the ledger entry is gone. It uses the same registry and the same runner as the protocol's own uninstall, so `plugin-security`'s `security.package-permissions` cleanup removes the package's sets with their position and user bindings, and any cleanup registered later fires here too. The cleanups run with the package's manifest id and no organization, because an install-local package is installed for the whole runtime. - -The response carries each outcome as `data.cleanups`, the way the protocol's uninstall reports them. A failed cleanup is reported there and named in the operator log with its remedy (install the package again, then uninstall it again). When the protocol cannot run the cleanups, the response says so as one failed `protocol.runUninstallCleanups` outcome. An uninstall that does not happen (a refused caller, an id this door never installed, a ledger write that fails) revokes nothing. - -`@objectstack/metadata-protocol`: `ObjectStackProtocolImplementation` gains `runUninstallCleanups({ packageId, organizationId?, actor? })`, the one runner of the uninstall-cleanup registry. It runs every registered cleanup for the package and answers one `UninstallCleanupOutcome` per cleanup. It never throws: a failed cleanup is an outcome, and a thrown fault's driver text goes to the operator log, not into the outcome. `deletePackage` now calls it as its last step in place of its own loop, and its `cleanups` are unchanged. The only visible difference there is the log tag of a failed cleanup's warning, now `[protocol.runUninstallCleanups]` instead of `[protocol.deletePackage]`. - -`@objectstack/cloud-connection` now declares its dependency on `@objectstack/metadata-protocol`, which it already received through `@objectstack/runtime`, for the cleanup outcome types. diff --git a/.changeset/21492-retire-memory-boot-store.md b/.changeset/21492-retire-memory-boot-store.md deleted file mode 100644 index a818c9c53bf..00000000000 --- a/.changeset/21492-retire-memory-boot-store.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor -'@objectstack/cli': minor ---- - -fix(spec,runtime,cli)!: the in-memory (mingo) engine is no longer a boot store — every boot door refuses it and names SQLite instead (#21492, #21572) - -Clause-②: yes (narrowing) - - - -**BREAKING**: the in-memory (mingo) engine can no longer be selected as the store a server, a migration or an embedded stack boots on. It refuses every tenant-scoped read by design, so a boot on it signed a user in and then answered `503` to every data request; there was nothing working to keep. The retirement is made at the declaration: `@objectstack/spec`'s driver table withdrew `memory`, `mingo` and `in-memory` from its selection face (they stay on the config-contract face beside `inmemory`), and every boot door refuses the engine with one sentence that names the replacement. - -- **`@objectstack/spec`** — `DATABASE_DRIVER_SELECTION_ALIASES` no longer lists `memory`, `mingo` or `in-memory`; `DATABASE_DRIVER_SELECTION_IDS` no longer lists `memory`; `resolveDatabaseDriverId` answers `undefined` for all four spellings. `resolveDriverId`, `DRIVER_ID_ALIASES`, `BUILTIN_DRIVER_IDS` and the `memory` config contract are unchanged. -- **`@objectstack/cli`** — `--database-driver memory` is refused while the flags parse (`os dev`, `os start`); `OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory` is refused before `os dev` or `os start` prints its Database row; `os serve`'s legacy path refuses the spellings and the `memory://` / `mingo://` schemes as a fatal boot error. The help no longer offers `memory://`. -- **`@objectstack/runtime`** — `createStandaloneStack`, `createDefaultHostConfig` and `resolveStandaloneDatabase` (every ordinary `os dev` / `os start` / `os serve` boot and every `os migrate` subcommand) refuse the spellings, the `memory://` and `mingo://` schemes, and a project whose default datasource is declared with `driver: 'memory'`. `resolveProjectDatabaseUrl` refuses a retired driver selection ahead of every rung, and its `ProjectDatabaseUrlSource` type no longer has the `'memory-driver'` member. `ResolvedStandaloneDatabase.driver` never names `memory`. Two exports are added for hosts that refuse the engine themselves: `namesRetiredMemoryEngine` and `retiredMemoryEngineMessage`. -- **Unchanged:** the `@objectstack/driver-memory` package; a declared non-default datasource with `driver: 'memory'` and a directly constructed `InMemoryDriver`, both still built; SQLite's dev step-down, whose last rung is still this driver. - -Migration — one flag change: - -- FROM `os dev --database-driver memory` (or `OS_DATABASE_DRIVER=memory`) TO `os dev --fresh` for a throwaway database deleted on exit. -- FROM `OS_DATABASE_URL=memory://…` / `--database memory://…` / `databaseUrl: 'memory://…'` TO `:memory:` (SQLite's own in-memory database), e.g. `OS_DATABASE_URL=:memory:`. -- FROM a default datasource declared `{ driver: 'memory' }` TO a SQLite one, e.g. `{ driver: 'sqlite', config: { filename: ':memory:' } }`. - -No shipped example selects the engine. It ships as `minor` under the launch-window convention for accept-set narrowings. diff --git a/.changeset/21496-text-face-exit-signal.md b/.changeset/21496-text-face-exit-signal.md deleted file mode 100644 index ee176556273..00000000000 --- a/.changeset/21496-text-face-exit-signal.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os package install`, `os package publish` and `os plugin sign` print one error line per refusal (#21496) - -Clause-②: no - -`os package install ./does-not-exist.json` printed `✗ Cannot read artifact: ENOENT …` and then a second line, `✗ EEXIT: 1`. The exit status, 1, was right. The extra line came from the command's own `catch`: the `this.exit(1)` inside its `try` throws oclif's exit signal, and the `catch` reported the signal as an error. - -The same `catch` sat in two more commands: - -- **`os package publish`.** Every refusal it makes printed the extra `✗ EEXIT: 1` line. Examples are an unreadable artifact, an invalid manifest id, no cloud login, a failed package registration and a failed version publish. An `--icon-file` whose image type it cannot infer printed three error lines: the refusal, then `✗ Cannot read --icon-file '…': EEXIT: 1`, then `✗ EEXIT: 1`. -- **`os plugin sign`.** A signature that failed its self-verification printed `✗ Self-verification error: EEXIT: 1` under the refusal. - -Each refusal is now one error line, and every exit status is unchanged. A script that filtered out the `EEXIT` line can drop that filter. diff --git a/.changeset/21498-cli-compose-migration-recovery.md b/.changeset/21498-cli-compose-migration-recovery.md deleted file mode 100644 index 00b4ba4d3f5..00000000000 --- a/.changeset/21498-cli-compose-migration-recovery.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os migrate resume --run --yes` can resume an interrupted `os migrate recorded-by` run, and `os serve` reports interrupted migration runs at boot (#21498) - -Clause-②: no - -`MigrationRecoveryPlugin` owns two things: the `migration-plans` registry, where a journal-backed migration's code is looked up, and the boot scan that reports runs which started and never finished. No CLI boot composed it. So `os migrate resume` found no plan for any run. It refused with "no loaded package registers" the plan, even though the plan's package was loaded in that process. And no `os serve`, `os start` or `os dev` boot ever scanned the migration journal. - -- **The `os migrate` data commands** (`recorded-by`, `resume`, `value-shapes`, `summary-nulls`, `files-to-references`, `meta --stored`, `audit-metadata-bodies`, `os storage orphans`) now boot with the plugin. A run interrupted before any of its chunks committed now resumes to completion. A command booted over an interrupted run also warns about that run on stderr first. -- **Every `os serve` boot** (and so `os start` and `os dev`, which spawn it) composes the plugin beside `PlatformObjectsPlugin`, which registers the journal the scan reads. An interrupted run is reported once at boot, with the `os migrate resume --run ` command that resumes it. Nothing is resumed automatically. A database with no interrupted run prints nothing. A config that composes its own `new MigrationRecoveryPlugin()` keeps that instance. -- **A run that had committed a chunk, or that was started with a non-default `--chunk-size`,** reaches the runner too. The runner fix that lets it resume is in the `@objectstack/core` entry for #21528. diff --git a/.changeset/21498-metadata-protocol-registers-recorded-by-plan.md b/.changeset/21498-metadata-protocol-registers-recorded-by-plan.md deleted file mode 100644 index 5c399bcf8ca..00000000000 --- a/.changeset/21498-metadata-protocol-registers-recorded-by-plan.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -The metadata protocol registers its journal-backed migration plan, `metadata.recorded-by-sentinel-to-null`, with the kernel's `migration-plans` registry (#21498) - -Clause-②: no - -A migration journal records a run's plan hash, not the plan's code. To resume a run, the package that owns the plan has to register it. This package owns the `recorded_by` sentinel-to-NULL plan, and until now it never registered it. So any process that composed the registry still reported the run as unresumable. - -The protocol assembly (`assembleMetadataProtocol`, which `ObjectQLPlugin` and `MetadataProtocolPlugin` both run) now registers the plan at `kernel:ready`. It does so only when a `migration-plans` service is composed. That runs before `MigrationRecoveryPlugin`'s boot scan, so the scan reports the run as resumable. A kernel with no registry is unchanged. Registering a plan runs nothing: only `os migrate resume` acts on it. diff --git a/.changeset/21499-verify-never-mints-key.md b/.changeset/21499-verify-never-mints-key.md deleted file mode 100644 index 1098289b248..00000000000 --- a/.changeset/21499-verify-never-mints-key.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/verify": patch ---- - -`bootStack` (and so `os verify`) no longer creates a data key file in the key home, and no longer seals its fixtures under a key the host already holds (#21499) - -Clause-②: no - -The harness composed the settings service with no crypto provider and bound the engine to a default `LocalCryptoProvider`. `bootStack` forces a development posture. In that posture, with no `OS_SECRET_KEY`, no `OS_DEV_CRYPTO_KEY` and no key file, both providers wrote a new key file into the key home. So `os verify`, a one-shot command over an in-memory database, left key material behind, and the next development-posture process on that host adopted it. On a host that already had a key, the harness sealed its throwaway fixtures under that real key. - -- **What the harness uses now.** One `LocalCryptoProvider` over a random key held in this process's memory only. It never reads `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the key file, and it never writes anywhere. The settings service and the engine get the same instance, so `secret` fields and encrypted settings still seal and open on a host with no key at all. -- **One key per process, not per boot.** Two `bootStack` calls over one `databaseFile` in the same process (the harness's restart) still open each other's secrets. -- **Unchanged.** `BootOptions` and the rest of the public API, and `os verify`'s stdout and `--json` report. The one stderr line announcing the minted key file is gone. diff --git a/.changeset/21500-spec-view-container-default-form.md b/.changeset/21500-spec-view-container-default-form.md deleted file mode 100644 index f3a2c17dd37..00000000000 --- a/.changeset/21500-spec-view-container-default-form.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -A view container's `form` is its default form: it is never collapsed into a named form, and no named form is promoted to default - -Clause-②: no - -`ViewSchema` declares `form` the container's default form and `formViews` additional named forms. `expandViewContainer` / `expandViewContainerWithDiagnostics`, which every view registrar shares, now serves exactly that. Behaviour changes for authors: - -- **A container with no `form` no longer serves its first named form as the default create/edit form.** Before, the first `formViews` entry was flagged `isDefault`, whatever it was: in the CRM example that was the anonymous Web-to-Lead form. Now no form item is flagged, and each named form is served only where it is asked for by name (a form action's `target`, `addRecord.formView`, a public `sharing.publicLink`). If you relied on the old promotion, move the intended create/edit form into `form`: `formViews: { edit: { … } }` becomes `form: { … }`, and a reference to `.edit` becomes `.form`. -- **A named form no longer replaces `form`.** Before, `form` was dropped when any named form shared its `type`, `label` and `columns`, even with different sections, and the first named form became the default. Now `form` is always served as `.form`, flagged `isDefault`, and is the only form item flagged. A named form whose body equals `form` stays its own named item. -- **The default `list` collapses only into a named list that restates its whole body.** A `listViews` entry that repeats `list` key for key, the list's own `name` aside (the "default == `listViews.all`" pattern), still folds into that one named item. A named list that shares `list`'s `type`, `label` and `columns` but differs in anything else (a filter, a sort) is now its own view, and `list` is served beside it as `.default`, the default list. Before, such a named list took the default's place, and the default list's own body was not served. - -The CRM and showcase examples move their create/edit form into `form`. The showcase task's `showcase_log_time` and `showcase_new_task` actions now target `showcase_task.form`. The public Web-to-Lead and contact-us forms stay named. - -⛔ No schema, parse, export or accept-set change. diff --git a/.changeset/21501-artifact-flag-precedence.md b/.changeset/21501-artifact-flag-precedence.md deleted file mode 100644 index f7f1d266d23..00000000000 --- a/.changeset/21501-artifact-flag-precedence.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -`os dev -a PATH` and `os start --artifact PATH` now serve the artifact they name, also from a directory that holds an `objectstack.config.ts` (#21501). - -Clause-②: no - -- **One precedence, written once.** The order is `--artifact` > `OS_ARTIFACT_URL` > `OS_ARTIFACT_PATH` > `/dist/objectstack.json` > `/dist/objectstack.json` (`os start` only) > a cwd `objectstack.config.ts`, except that a cwd config joins the boot when the resolved artifact is its own compiled output. It is the order the `os start` reference already published. `os start` and `os dev` both resolve through one module, and the `serve` child they spawn boots exactly their answer. -- **Beside a config.** The child used to read the supervisor's answer only when the working directory held no config. So `os dev -a X` and `os start --artifact X` printed `Artifact: X` and served the config's `dist/objectstack.json`, or the config itself. A named artifact now boots alone, exactly as it boots from a directory with no config. The config takes part only when the artifact is its own compiled output: `/dist/objectstack.json`, or the path the command compiled it to. A bare `os dev`, a bare `os start` in a project, and `os start --artifact ./dist/objectstack.json` take that path, and are unchanged. A host config (its `plugins` hold code) boots its own module there, because its compiled output cannot carry that code. -- **`OS_ARTIFACT_PATH` beside a config** follows the same rule: `OS_ARTIFACT_PATH=Y os start` serves `Y` without loading the config. Under `os start --artifact ./dist/objectstack.json` the flag now also wins over an exported `OS_ARTIFACT_PATH` inside the config boot. -- **`os dev` under a local `OS_ARTIFACT_PATH`** compiles the cwd config into that path, so the file there is the config's own compiled output. The config takes part in the boot that serves it, and a host config compiled there keeps its plugins. -- **`os dev` gains the `OS_ARTIFACT_URL` rung.** `--artifact` outranks it. Before, the reference stayed in the child's environment and won. Without the flag the reference drives the boot, as under `os start`. The `Artifact:` row names it (redacted), and nothing is compiled into, watched for or judged stale against it. -- **Banner rows.** `os start` and `os dev` print `Config:` only when the config takes part in the boot. The child says it is not loading a config that sits beside a named artifact, instead of `No objectstack.config.ts found`. -- **The ready banner names what loaded.** On a config boot, a non-host config whose app was served from its compiled artifact gets `Artifact: dist/objectstack.json` in the ready banner, and a host config keeps `Config: objectstack.config.ts`. No ready-banner row names a file the boot did not load. - -Upgrading: a project that ran `os dev -a`, `os start --artifact` or `OS_ARTIFACT_PATH` beside its config, and relied on that config being loaded, should drop the override or point it at `./dist/objectstack.json`. diff --git a/.changeset/21505-read-scope-temporal-coercion.md b/.changeset/21505-read-scope-temporal-coercion.md deleted file mode 100644 index 1dd56307b33..00000000000 --- a/.changeset/21505-read-scope-temporal-coercion.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -fix(service-analytics)!: the analytics read scope and the draft preview compare a temporal comparand in the column's storage form, as the engine does (ADR-0053 D-A1 / D-A2) (#21505) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this changes the rows two analytics faces select for a value comparison on a declared temporal column, in both directions, onto the rows `engine.find` selects for the same filter: on some filters fewer rows than before, on others more. The faces are the row-level read scope compiled into the native statement, and the draft preview (`queryDataset` with `previewDrafts`). It ships as `minor` under the launch-window convention for answer changes. No export is removed, no accepted input is refused and no error code changes. - -**The read scope.** `compileScopedFilterToSql` takes two new optional members in its options, `coerceTemporalFilterValue(field, value)` and `coerceTemporalFilterColumn(field, columnSql)`. Together they are the driver's `temporalFilterValue` / `temporalFilterColumnSql` pair, bound to the object the scope reads. After the shared lowering, every value comparison binds its comparand through the first and reads its column through the second: equality, `$ne`, the four orderings, `$in`, `$nin` and `$between`. Null tests, `$empty` and the text operators read the column as stored. An absent member is identity: the comparand and the column stay as written, which is what a host that passes neither got before. `NativeSQLStrategy` (the read scope merged into the native statement) and the `ObjectQLStrategy` echo (`/analytics/sql`) pass the context's pair, which `AnalyticsServicePlugin` wires to the driver. Before, the comparand was bound as written and the database read it by its own rules, on SQLite and on PostgreSQL whatever the server's time zone. - -**The draft preview.** It has no driver, so each value comparison on a column the host declares `datetime`, `date` or `time` now puts both sides in the storage form `@objectstack/core`'s `temporalStorageForm` gives: the comparand, and the drafted row's value, as `driver-memory` reads them. Before, it compared the two spellings as text. A column the host names no type for is compared as written, as before. - -A `date` column answered the engine's rows on both faces before and still does when both sides are spelled as days. No `@objectstack/spec` contract changes and no dependency edge is added. A host that calls `compileScopedFilterToSql` directly gets the coercion by passing the pair from its driver. diff --git a/.changeset/21509-verify-select-multiple.md b/.changeset/21509-verify-select-multiple.md deleted file mode 100644 index 73a6befb8f2..00000000000 --- a/.changeset/21509-verify-select-multiple.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/verify": patch ---- - -`os verify` writes each derived sample in the shape the engine stores it: a `select` declared `multiple: true` is written as a list and compared as a set - -Clause-②: no - -- The CRUD round-trip derivation now asks `@objectstack/spec`'s `isMultiValueField` whether a field is multi-valued, the same predicate the engine stores by. Before, the `select` / `radio` sample was one scalar option code compared `equal` whatever the field declared, so a multi-valued `select` read back as a one-element list and was reported as a fidelity gap the engine does not have. The shipped `examples/app-todo` (`todo_task.tags`) failed `os verify` with exit 1 on exactly that, and now passes. -- A single-valued `select` or `radio` keeps its scalar sample and its `equal` comparison. `multiselect` and `checkboxes` are unchanged. -- A relational field's `multiple` is answered by the same predicate. A `lookup` declared `multiple: true` still receives a list of ids. A `master_detail` or `tree` field carrying `multiple: true` now receives one id, which is how the engine stores those types. The spec already refuses `multiple` on those types at parse, so only an unparsed config could reach this. -- No export, type or accept-set change. diff --git a/.changeset/21510-list-read-stored-row-wins.md b/.changeset/21510-list-read-stored-row-wins.md deleted file mode 100644 index e3a2e047aa2..00000000000 --- a/.changeset/21510-list-read-stored-row-wins.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): the object door lists a stored view row under its own name even where a stored view container expands that name, as the by-name read already answers - -Clause-②: no - -- **What changed.** `GET /api/v1/meta/view?object=…` (the object door) no longer lets a stored view container's expansion replace a stored row of the same name. A view item (a row carrying `viewKind`) saved under a name the container also expands, such as `.default` beside a stored overlay of that object's container, is now what the object door lists under that name. Before, the object door listed the container's expansion there while the by-name read (`GET /api/v1/meta/view/NAME`) answered the stored row. Both doors now answer the row. -- **The rule.** A row stored under exactly a name is the override for that name (ADR-0005 keys an overlay by its own name). An expansion fills only the names that have no row of their own. The list read and the by-name read decide this with one test, over the rows each selects for the same caller, so a row stored for one organization does not hide the expansion from any other caller. -- **A container stored under one of its own expanded names.** That row is the name's own row as well, so its expansion no longer fills the name. The object door never lists a container, so it now lists nothing under that name. Before, it listed the container's expansion there. The by-name read answers the stored container, as before. -- **What does not change.** Every name a container expands that has no stored row of its own is still listed, and on both doors it still replaces a packaged view of the same name. The by-name read answers as before. The save door is unchanged. No response shape gains or loses a key. diff --git a/.changeset/21511-expansion-tenant-marker.md b/.changeset/21511-expansion-tenant-marker.md deleted file mode 100644 index ea70b615ea5..00000000000 --- a/.changeset/21511-expansion-tenant-marker.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -An expanded view of a stored view container is reported as tenant-authored on an unscoped kernel, as it already was on an environment-scoped one: not resettable, and with no `code` layer - -Clause-②: no - -On an unscoped (control-plane) kernel, registry hydration registers each view a stored environment-wide container expands, under that view's own name. The container was registered with the tenant-authorship marker (`_provenance: 'org'`), and its expansions were not. An expansion of a container bound to a package therefore carried that package's id and no marker, and the registry's artifact lookup took it for a view the package ships. For such a name `getMetaItem` (`GET /api/v1/meta/view/NAME`) answered `resettable: true`, and `getMetaItemLayered` (`/layers`) answered the stored container's expansion as the `code` layer. The `code` layer was also wrong for an expansion of a package-less container. An environment-scoped kernel registers nothing, and answered `resettable: false` and `code: null`. - -Each registered expansion now carries its container's marker, applied before the expansion's own artifact envelope, in the same order the container gets it. Where the container's own package ships a view of that name, that artifact's envelope still wins (ADR-0010 §3.3). Both kernels now give the same answer for every expanded name. Studio's reset affordance and its code-versus-overlay diff are drawn from these two values. - -The save door is unchanged: it accepts a write by an expanded name on both kernels, as before, and the stored row then answers that name. diff --git a/.changeset/21515-job-body.md b/.changeset/21515-job-body.md deleted file mode 100644 index d56c29c664e..00000000000 --- a/.changeset/21515-job-body.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A job can carry a sandboxed `body`, the same JavaScript body hooks and script actions carry, so its work travels with the metadata; `handler` is deprecated beside it (#21515). - -Clause-②: yes (widening) - -- **`JobSchema.body`** is `ScriptBodySchema` by reference: `{ language: 'js', source, capabilities, memoryMb }`, strict as on hooks. It runs in the QuickJS sandbox with no module scope, reaches data only through `ctx.api` under its declared `capabilities`, and logs through `ctx.log`. The in-process `JobHandlerContext` members (`ql`, `logger`, `bundle`) do not exist there. -- **`handler` is optional and DEPRECATED, "prefer `body`".** When both are present `body` wins, as for hooks. A job that declares neither is refused at parse, located at `body`, with a message naming both keys. The rule is published in the JSON Schema too (`anyOf` of one `required` per key), not only enforced by the parse. -- **Only the L2 body.** An expression (L1) body is refused on a job at `body.language`, and the message says why: an expression performs no I/O, so its only effect would be a returned value, and a job runs for its effects. The message lives on `ScriptBodySchema.language` and fires only where that shape is used on its own; hook and action bodies are unchanged. -- **One time limit.** A body job's limit is the job's own `timeoutMs`: one attempt is one sandbox run, bounded by that value. `body.timeoutMs` (capped at 30 s on hooks and actions) is refused on a job, with the prescription to move the value to `timeoutMs`. The job-level key has no cap, so long-running work states its limit there or splits into bounded runs. The `timeoutMs` describe is the one place this is stated. -- **Not yet run by the runtime.** Scheduling a job's `body` is a separate change. Until it lands a job runs through `handler`, and a body-only job is skipped at boot with a warning. The liveness ledger grades `job.body` `planned`, so `os validate`, `os lint` and `os build` warn wherever a job sets a `body`. `objectstack build` does not mint a job body from the function a `handler` names; write it as data. - -Nothing that parsed before is refused now: every existing job declares `handler`, and none declares `body`. diff --git a/.changeset/21516-core-object-not-found-error.md b/.changeset/21516-core-object-not-found-error.md deleted file mode 100644 index dc23723fe47..00000000000 --- a/.changeset/21516-core-object-not-found-error.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/core': minor ---- - -New export `objectNotFoundError(object)`: the one `OBJECT_NOT_FOUND` envelope the data door and the engine's in-process verbs refuse an unresolved object name with - -Clause-②: yes - -`@objectstack/core` exports `objectNotFoundError(object: string): Error`. The error it returns carries `code: 'OBJECT_NOT_FOUND'`, `status: 404`, the requested name on `object`, and the message `Object '' not found`. It lives here beside `recordNotFoundError`, and for the same reason: the engine cannot import `@objectstack/metadata-protocol`, where the data door first wrote this envelope (ADR-0076 D2). The data door's object-existence gate and `@objectstack/objectql`'s resolver both build their refusal from it, so the two answer one name space with one envelope. Additive: nothing that existed before changes. diff --git a/.changeset/21516-metadata-protocol-refusal-readers.md b/.changeset/21516-metadata-protocol-refusal-readers.md deleted file mode 100644 index 580917dc9f8..00000000000 --- a/.changeset/21516-metadata-protocol-refusal-readers.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -The data door's object-existence gate builds its `OBJECT_NOT_FOUND` from the shared factory, and two best-effort readers treat the engine's refusal of their own object as the not-provisioned case - -Clause-②: no - -- `assertObjectRegistered` (the data door's object-existence gate) now throws `objectNotFoundError(object)` from `@objectstack/core`. The code, the status, the `object` field and the message are unchanged, byte for byte. -- `SeedLoaderService.resolveSoleOrganizationId` and the history counters `SysMetadataRepository` reads (`version`, `event_seq`) already answered a missing table of their own object as "nothing here yet". `@objectstack/objectql` now refuses an object name its registry does not hold with `OBJECT_NOT_FOUND` instead of reaching the driver, so each reader also answers that refusal as the same absence when the error's own `object` is the object it read. A refusal naming another object, and every other read failure, still propagate. With a registered object nothing changes. diff --git a/.changeset/21516-objectql-unresolved-name-refusal.md b/.changeset/21516-objectql-unresolved-name-refusal.md deleted file mode 100644 index 3b276c6c775..00000000000 --- a/.changeset/21516-objectql-unresolved-name-refusal.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/objectql': minor ---- - -An in-process engine verb refuses an object name the registry does not resolve, with the data door's own `OBJECT_NOT_FOUND`, instead of handing it to the driver as a raw table name - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing of the engine's in-process verbs, shipped as `minor` under the repo's launch-window convention for breaking changes. - -**What was accepted before.** `find`, `findOne`, `count`, `aggregate`, `insert` (and `insertMany`), `update`, `delete` and `validate` resolved their target through the schema registry and, for a name the registry did not resolve, handed the name to the driver as a raw table name. A caller in the process (a sandboxed action or hook body's `ctx.api`, an action handler, host code) could therefore read or write a table by a name the generic data door refuses with `404 OBJECT_NOT_FOUND`, and every in-process guard keyed by a registered object name could be stepped around by naming the target another way. - -**What is refused now.** Such a name is refused with the data door's own envelope (`OBJECT_NOT_FOUND`, `status: 404`, the name on `object`, built by `objectNotFoundError` from `@objectstack/core`) before any hook, middleware or driver runs. A registered name resolves exactly as before. `judgeFilter` still judges the filter for a name the registry does not hold, because it reads nothing and reaches no driver; execution refuses that object before admission. - -**Inside the engine.** The single-tenant organization probe asks the registry first: an install that registers no organization object is the lean case it always was, with no organization to derive, and the write proceeds unstamped without a driver read. - -**The fix.** Register the object (in the stack, with `registry.registerObject`, or through a plugin manifest) before addressing it through the engine. Host code that must reach storage without a registry entry addresses the driver itself (`datasource(name)`, `getDriverForObject(name)`), a path a sandboxed body cannot reach. diff --git a/.changeset/21516-spec-judge-filter-docblock.md b/.changeset/21516-spec-judge-filter-docblock.md deleted file mode 100644 index 305df89e9e4..00000000000 --- a/.changeset/21516-spec-judge-filter-docblock.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The `IObjectQLEngine.judgeFilter` docblock states that execution refuses an object the registry does not know before admission - -Clause-②: no - -The comment ships in the package's type declarations (`dist/*.d.ts`); `src/contracts/objectql-engine.ts` itself is not in `files[]`. It used to say that, for an object the registry does not know, the schema-free doors still judge "as at execution". Execution now refuses such an object before admission (`OBJECT_NOT_FOUND`, 404), so the comment says that answer is about the object, not the filter, and is not this member's verdict. ⛔ No schema, parse, export or accept-set change. diff --git a/.changeset/21517-approval-reassign-slot-address-tsdoc.md b/.changeset/21517-approval-reassign-slot-address-tsdoc.md deleted file mode 100644 index 2e2d24bafd1..00000000000 --- a/.changeset/21517-approval-reassign-slot-address-tsdoc.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The `ApprovalActionRow` documentation now says what `reassign_from` and `reassign_to` hold. It said both were users. They hold a slot address in its stored spelling: a user id, an email, or a position address such as `position:legal`. A reassignment moves a slot, not necessarily a person, and the person who made the move is `actor_id`. The `reassign_from_name` and `reassign_to_name` documentation now says when a name resolves: only for a user id, or for an email an account carries. A position address never resolves, so a consumer renders the address when the name is absent. - -Clause-②: no - -Documentation only. No schema, export or type changes. diff --git a/.changeset/21519-flow-read-node-family-serve.md b/.changeset/21519-flow-read-node-family-serve.md deleted file mode 100644 index c77a6c8fb40..00000000000 --- a/.changeset/21519-flow-read-node-family-serve.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(service-automation): a flow's `get_record` node that reads the stored-metadata tables is served what the generic data door serves (#21519) - -Clause-②: no - -The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and a content hash computed over it. The generic data door serves such a row with the body as its type's read projection, with the stored credential material withheld, and the hash in keyed form. A flow's `get_record` node read the same rows and served them as stored, under either run identity (`runAs: 'system'` and `runAs: 'user'`). What it read went into the run's declared output, which the flow's caller is handed back, and into any record the flow wrote from it. - -**What changes.** When the node reads either table, its answer now takes the data door's form, for one row (`findOne`, no `limit`) and for a row list (`find`, `limit` above 1). The body is projected, and the content hash is keyed under the same key the data door uses. That key is the crypto provider's, read from the data engine when the node runs, or the process-scoped ephemeral key when no provider is registered. So the hash a flow is served equals the hash the data door serves for the same row. A record the flow writes from what it read can therefore carry only the projected body and the keyed hash. A `fields` projection that names the body column without the type column reads the type beside it and drops it again, as on the data door. - -**What does not change.** Every other object is read exactly as before. The node's other config keys (`filter`, `limit`, `outputVariable`) and the write nodes behave as before. The node consumes the data door's own functions from `@objectstack/metadata-protocol` (`storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows`, `ephemeralStoredHashDigest`) and keeps no copy of them. `@objectstack/service-automation` now depends on `@objectstack/metadata-protocol`. A composition that runs flows on `ObjectQLPlugin`, as `os dev` and `os serve` do, already loaded that package. diff --git a/.changeset/21520-body-family-boundary.md b/.changeset/21520-body-family-boundary.md deleted file mode 100644 index 227743874d9..00000000000 --- a/.changeset/21520-body-family-boundary.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/runtime': minor ---- - -fix(runtime)!: an app-authored body may not bind a hook to, or write, the stored-metadata tables (#21520) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what an app-authored body may do with the two stored-metadata tables, `sys_metadata` and `sys_metadata_history`. For an app-authored body, the metadata protocol is now their only writer: a change to metadata goes through the metadata API, where it is validated and its provenance is recorded. - -- **Binding.** A hook with a sandboxed `body` whose `object` names either table, alone or in a list, is no longer bound. The refusal is made at registration, at the one point every body hook becomes a handler, so it holds on every door a hook binds by: a code bundle or boot artifact, an installed artifact, and a hook authored at runtime through the metadata door. It carries `PERMISSION_DENIED` / 403, names the metadata API, and is recorded against the hook in the bind log at `error` (thrown under strict binding). A wildcard (`'*'`) body hook still binds; its body is not run for either table's events, and the bind says so once at `info`. -- **Writing.** A sandboxed action or hook body's write of either table through `ctx.api` — every write verb, inside a transaction or not, with or without elevation — answers `PERMISSION_DENIED` / 403 before the write runs, so nothing lands and the answer does not depend on what the write names. -- **Unchanged:** a body's reads of the two tables (still served as the generic data door serves them); host code that registers its own action handlers or hooks; the platform's own hooks, which are code and still fire on the metadata door's save; and every other object. - -The route: change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) rather than from a body, and bind hooks to the objects an app owns. No shipped example binds a body hook to either table or writes one from a body. It ships as `minor` under the launch-window convention for accept-set narrowings. diff --git a/.changeset/21523-init-refusal-printed-once.md b/.changeset/21523-init-refusal-printed-once.md deleted file mode 100644 index 486a361d26c..00000000000 --- a/.changeset/21523-init-refusal-printed-once.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os init` prints its dependency-install and scaffold-validation refusals once (#21523) - -Clause-②: no - -`os init demo -p npm` with an unreachable package registry printed `✗ Project scaffolded, but dependency installation failed.`, then a second `✗ Dependency installation failed`, then oclif's `Error: Dependency installation failed`, and exited 2. The second `✗` line came from the command's outer `catch`: the `this.error(…)` inside its `try` throws oclif's exit signal, and the `catch` reported it again. A scaffold that failed its own validation got a second `✗ Scaffold validation failed` line under its refusal the same way. - -The `catch` now lets the signal through. Each refusal prints its `✗` line once, followed by oclif's `Error:` line as before, and the exit status is still 2. diff --git a/.changeset/21524-plugin-signature-ed25519-key-type.md b/.changeset/21524-plugin-signature-ed25519-key-type.md deleted file mode 100644 index 439403a8f12..00000000000 --- a/.changeset/21524-plugin-signature-ed25519-key-type.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/core': minor ---- - -fix(core)!: the plugin artifact signature contract refuses any key that is not Ed25519, so its `ed25519` label now holds (#21524) - -**BREAKING**: `signPayload` and `verifyPayload` (the plugin artifact signature contract in `@objectstack/core`) now refuse a key whose type is not Ed25519. Until now they accepted any asymmetric key. node's `sign(null, …)` and `verify(null, …)` follow the key they are handed, so an RSA, EC or Ed448 key signed under the `ed25519:KEYID:SIG` label and verified against its own public half. `os plugin sign --key` with an RSA private key exited 0, printed `Plugin signed`, and wrote an `ed25519:`-labelled sidecar over an RSA signature. - -What is refused now: - -- **`signPayload`** throws when the private key is not Ed25519. The error names the key type found (`rsa`, `ec`, `ed448`, and `secret` for a symmetric key). -- **`verifyPayload`** throws when the verifying key's type is not the algorithm the signature's label names. The label is checked against the key, not trusted, and the only label the contract parses is `ed25519`. The error names the key type found. -- **`verifyPublisherSignature`, `verifyPlatformSignature` and `verifyPluginArtifact`** verify through `verifyPayload`. So a publisher key registry entry or a platform key that is not Ed25519 makes them throw, or reject, with that same error. It is not folded into a `false` or an `ok: false` result, because a wrong key is the verifier's own configuration, not a verdict on the artifact. -- **`os plugin sign`** prints one `✗ Signing failed: signPayload: …` line naming the key type, exits 1, and writes no sidecar. - -Each refusal is a plain `Error`, the error style the module already used. - -**The fix:** sign with an Ed25519 key, generated with `openssl genpkey -algorithm ed25519` or `generateEd25519KeyPair()`. Configure Ed25519 public keys for the publisher key registry and the platform key. A signature made earlier with a non-Ed25519 key cannot be verified any more. Sign the artifact again with an Ed25519 key. - -**Unchanged:** an Ed25519 key signs and verifies exactly as before, with the same deterministic signature bytes. That holds for a PEM string, a `KeyObject`, and the PEM buffer, DER and JWK inputs node also accepts. A malformed signature string, a signature that does not verify, and a key that cannot be read still answer `false`. The signature string format and every export are unchanged. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21528-core-resume-started-over-plan.md b/.changeset/21528-core-resume-started-over-plan.md deleted file mode 100644 index 0a237e4b5e7..00000000000 --- a/.changeset/21528-core-resume-started-over-plan.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/core': patch ---- - -fix(core): a resumed migration run is compared against the chunk plan it started over, so `os migrate resume` completes an interrupted `recorded-by` run that had committed a chunk or was started with a non-default `--chunk-size` (#21528) - -Clause-②: no - -`runMigrationJournal` recomputed a resumed run's chunk plan from the rows `load()` returned at resume time, at the plan's current chunk size, and refused `PLAN_CHANGED` when that plan's hash differed from the one `run_started` recorded. Two kinds of interrupted run could differ. A plan whose `load()` selects only the work still to do, which `recorded-by`'s plan does, returns fewer rows once a chunk has committed. And the plan handed back for a resume carries its own chunk size, not the one the run was started with. So `os migrate resume` listed such a run as `resumable: true`, and `os migrate resume --run --yes` then refused it. - -A resume now reads the chunk plan back from the journal's `run_started` record: - -- **Identity.** The plan's id and step names are hashed with the recorded chunk boundaries and compared with the recorded hash. A plan whose id or steps changed is still refused `PLAN_CHANGED`. The run resumes at the chunk size it started with. -- **Rows.** Each step's rows are bound to that chunk plan. If `load()` returns every row the run started over, each chunk's rows are where the journal put them, as before. If it returns exactly the rows of the chunks not yet committed, those rows go, in order, to those chunks. Any other row count is refused `PLAN_CHANGED`, and the message names the step. -- **Unwind.** If a chunk fails after a resume that bound its rows the second way, the runner compensates the chunks this process committed, newest first. It then stops at the newest chunk an earlier process committed and journals `run_failed`, because `load()` no longer returns that chunk's rows. It does not compensate other rows in their place, and the run ends `failed`. diff --git a/.changeset/21529-absent-database-empty-work.md b/.changeset/21529-absent-database-empty-work.md deleted file mode 100644 index b371dc2d127..00000000000 --- a/.changeset/21529-absent-database-empty-work.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@objectstack/cli": patch -"@objectstack/runtime": patch ---- - -`os migrate resume`, `os migrate recorded-by` and `os migrate value-shapes` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 with "The database refused to run this query" (#21529) - -Clause-②: no - -Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing. On a never-booted database (or a `--database-url` that points at one), every default run failed: - -- `os migrate resume` exited 1, naming `sys_migration_journal`; -- `os migrate recorded-by` exited 1, naming `sys_metadata_history`; -- `os migrate value-shapes` reported every scanned object as unreadable, kept the gate closed and exited 1, over data that does not exist. - -Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so: - -- `os migrate resume` lists no interrupted runs (`{"interrupted": [], "count": 0}`), exit 0; -- `os migrate recorded-by` reports `pending: 0`, nothing to convert, exit 0; -- `os migrate value-shapes` completes a clean scan of zero records, exit 0, and names the objects it did not read because they have no table yet (on stderr under `--json`). - -Human mode says the table is not there yet, instead of implying the command looked through one. `--json` documents have the same shape as on a booted database with nothing to do. The write modes (`--run`, `--apply`) are unchanged: they boot with the schema sync, so their tables exist before they read. - -`MigrationRecoveryPlugin` (`@objectstack/runtime`), which every one of these boots composes, scans the migration journal at boot. On such a database it logged "Migration journal scan failed; interrupted migrations (if any) were NOT detected" on every run. It now treats a missing journal table as "no runs" and says nothing. It recognises that case only with the shared `isMissingTableError` predicate, asked about `sys_migration_journal` itself. Any other failure of the scan still warns. - -There is nothing to migrate. diff --git a/.changeset/21532-mcp-server-info-version-default.md b/.changeset/21532-mcp-server-info-version-default.md deleted file mode 100644 index 5babccd242b..00000000000 --- a/.changeset/21532-mcp-server-info-version-default.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/mcp': patch ---- - -The MCP server's `serverInfo.version` is the package version unless you set one, as `MCPServerPluginOptions.version` always documented ("Defaults to package version") (#21532). - -Clause-②: no - -- Before, `MCPServerPlugin` and `MCPServerRuntime` each defaulted to the literal `1.0.0`, so every deployment built without the option, `os serve`'s auto-registration included, answered `initialize` with `serverInfo.version` `1.0.0` whatever the installed `@objectstack/mcp` was. Both defaults now read the version from the package's own `package.json`, ESM and CJS alike. -- An explicit `version` option (`MCPServerPluginOptions.version`, `MCPServerRuntimeConfig.version`) is still answered as given. -- `new MCPServerPlugin().version`, the kernel plugin's own version, is the package version too, where it was `1.0.0`. Its declared type is now `string | undefined`: if the manifest cannot be read (a bundle with no `package.json` beside it), `serverInfo.version` says `unknown` and the plugin's own `version` is left unset, which both kernels accept, instead of a placeholder they would refuse. -- Pass `version` yourself to keep reporting a fixed string. diff --git a/.changeset/21542-refusal-renders-once.md b/.changeset/21542-refusal-renders-once.md deleted file mode 100644 index 3eb38729483..00000000000 --- a/.changeset/21542-refusal-renders-once.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -fix(cli): `os init` and `os compile` render each refusal once, not once on stdout and again as oclif's `Error:` block on stderr (#21542) - -Clause-②: no - -`os init demo -t bogus` printed `✗ Unknown template: bogus` on stdout, then the same sentence as oclif's `Error:` block on stderr, and exited 2. Ten refusals did it: the five `os init` makes before it writes anything (an unknown template, a project name that is not valid, a target directory that is not empty, a current directory whose name is not a valid project name, an `objectstack.config.ts` that already exists), its scaffold self-test and dependency install, its catch-all, and `os compile`'s runtime-bundle refusal and catch-all (`os build` inherits both). Each printed its own `✗` line and then handed the sentence to `this.error`, which has oclif's entry point render it again. - -Each now prints its `✗` line and the hint under it once, and ends in `this.exit(2)`: the status `this.error` raised, with nothing rendered by the entry point. Stdout carries the same lines as before; stderr no longer repeats them. Exit statuses are unchanged: 2 for all ten. - -A script that read the sentence from stderr, from the `Error:` block, now finds it on stdout, on the `✗` line, which is where the full wording and the hint always were. diff --git a/.changeset/21544-door-narrowing-export.md b/.changeset/21544-door-narrowing-export.md deleted file mode 100644 index 353318eef41..00000000000 --- a/.changeset/21544-door-narrowing-export.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor -'@objectstack/runtime': patch ---- - -fix(metadata-protocol)!: the generic data door refuses a stored-metadata filter that reads the body or a content hash through a cross-field comparand or below its depth backstop, and exports its one filter-field collector and one search narrowing for the reader-context seam (#21544) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what the generic data door (`GET /api/v1/data/:object`, `POST /api/v1/data/:object/query` and the in-process `findData`) accepts when it reads `sys_metadata` or `sys_metadata_history`. Two filter shapes read the stored body column or a content-hash column (`checksum`, `previous_checksum`, or the history table's `change_note`) without the family's refusal ever seeing them, and both ran before this release: - -- a cross-field comparand naming one of those columns — `{ "name": { "$ne": { "$field": "metadata" } } }`, in `where` or in an aggregation's `filter`, under `$not` included. The SQL drivers evaluate it row by row, so row presence disclosed the column's value; -- a filter on one of those columns nested more than 32 combinators deep, which the door's field collector stopped reading at. A body `$contains` of a stored credential answered the row and a wrong guess answered none. - -Both now answer the door's `400 INVALID_FIELD`, naming the column, before the query runs — the answer the same filter already gets when it names the column directly. The route: filter those tables by their scalar columns (the type, the name, the state and the like), compare scalar columns with each other, and read the bodies with a plain list, which is served projected. Every other column of the two tables, and every other object, is unchanged; a dotted key into one of those columns was, and stays, refused by the door's dotted-path rule. It ships as `minor` under the launch-window convention for accept-set narrowings. - -- **`@objectstack/metadata-protocol`** exports two module functions the generic data door now calls itself: - - `collectStoredMetadataFilterFields(object, query)` — the family's one filter-field collector: every column a read query's filters read (`where`, the engine's `filter` alias and each aggregation filter): each key's head and each cross-field `{ $field }` comparand, at any depth. `[]` outside the family. - - `narrowStoredMetadataSearch(object, query, schema, wireSpelling?)` — the family's one default-search narrowing: an explicit search-field list naming the body or a hash column is refused, a default search is narrowed to the searchable set without them (returned for the caller to run as `searchFields`), and a set that narrows to nothing is refused. The `StoredMetadataSearchSchema` type it reads is exported beside it. -- **`@objectstack/runtime`**: the stored-metadata reader-context seam (`ctx.api.object(...)` for action and hook bodies, a handler's `ctx.api`, and `ctx.engine.find`) calls those two functions instead of its own copy of the narrowing and `@objectstack/plugin-security`'s condition walk, so the seam and the door answer every family filter and search identically. A `count` through the seam now runs the query the guard returns. The seam's accept set is unchanged: every shape it refused before it still refuses, now through the door's collector. diff --git a/.changeset/21552-absent-database-family-closeout.md b/.changeset/21552-absent-database-family-closeout.md deleted file mode 100644 index 482b2196980..00000000000 --- a/.changeset/21552-absent-database-family-closeout.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os migrate account-issuer`, `os migrate audit-metadata-bodies`, `os migrate meta --stored`, `os secret orphans`, `os secret rewrap` and `os storage orphans` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 on a refused read (#21552) - -Clause-②: no - -Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing, and the database refused the read. On a never-booted database (or a `--database-url` that points at one) every default run exited 1: - -- `os migrate account-issuer` refused, naming `sys_account`; -- `os migrate audit-metadata-bodies` counted `failures: 3` for `sys_audit_log`, `sys_activity` and `sys_metadata_audit`; -- `os migrate meta --stored` refused, naming `sys_metadata`; -- `os secret orphans` and `os secret rewrap` answered `"error": "scan_failed"`, naming `sys_secret`; -- `os storage orphans` refused, naming `sys_file`. - -Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so: - -- `os migrate account-issuer` reports no account and no collision (`ok: true`), exit 0; -- `os migrate audit-metadata-bodies` reports nothing to rewrite, with `failures: 0`, exit 0; -- `os migrate meta --stored` reports no stored metadata to examine (`scanned: 0`, `clean: true`), exit 0; -- `os secret orphans` and `os secret rewrap` report no secret to act on, with every holder family enumerated rather than a gap, exit 0; -- `os storage orphans` reports no stranded file, exit 0. - -Each names the tables it did not read: on stdout in human mode, on stderr under `--json`, where stdout stays one document. `os migrate account-issuer` is the one that recognises the refusal instead of asking the boot: its boot composes no auth plugin, so `sys_account` is never listed as a table to create. It recognises only the missing-table refusal for `sys_account`, with the shared `isMissingTableError` predicate. - -A table that exists but lacks a column, and any other read that is refused, is still read and still refuses with exit 1. The write modes (`--apply`, `--delete`) are unchanged: they boot with the schema sync, so their tables exist before they read. - -There is nothing to migrate. diff --git a/.changeset/21558-container-own-expansion-name.md b/.changeset/21558-container-own-expansion-name.md deleted file mode 100644 index 0769b6e90b7..00000000000 --- a/.changeset/21558-container-own-expansion-name.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -The runtime save door refuses a view container saved under a name its own expansion produces - -Clause-②: yes (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's `name` refusals shipped with. - -**What was accepted before.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, accepted an aggregated view container (`list` / `form` / `listViews` / `formViews`) saved under one of the names its own expansion produces: for example `{ name: 'crm_lead.default', object: 'crm_lead', list: { … } }` saved as `crm_lead.default`, the name its bare `list` expands to. That row is the name's own stored row, and an expansion fills only names that have no row of their own (the object door adopts that rule in this same release), so the container's expansion never filled it. The object door (`GET /api/v1/meta/view?object=…`), which never lists a container, listed nothing under the name, and the by-name read answered the raw container. No door answered a view item for the name, and nothing said why. - -**What is refused now.** That save, with `VALIDATION_ERROR` / 400, before anything is stored or registered, in draft and in publish mode. Whether a name is one the container's own expansion produces is decided by the same expansion the read doors run, so every member kind (a bare or named `list`, `listViews`, `form`, `formViews`) and the expander's de-duplicated names (`…_2`) are judged where the readers place them. A container with no `name` is judged under the save name the door stamps on it. A container on another package's object expands under its own name, which is never the name it is saved under, so it is not refused. - -**What still saves.** A container under its object's name, which expands as before. A view item (a body carrying `viewKind`) under an expanded name, the sanctioned override for that name. The read doors are unchanged. A row stored in this shape before this change keeps its bytes and is served as before; `migrate meta --stored` and package duplication, which re-save stored rows through this door, now report such a row as failed with this refusal instead of re-saving it. - -**The fix.** Save the container under its object's name (`crm_lead`), or save a view item (`name`, `object`, `viewKind`, `config`) under the expanded name (`crm_lead.default`). diff --git a/.changeset/21565-hook-body-stored-metadata-target-refused.md b/.changeset/21565-hook-body-stored-metadata-target-refused.md deleted file mode 100644 index fd85c3c507c..00000000000 --- a/.changeset/21565-hook-body-stored-metadata-target-refused.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A hook whose `body` targets a table of stored metadata, `sys_metadata` or `sys_metadata_history`, is refused at parse, with the runtime's prescription: change metadata through the metadata API. - -Clause-②: yes (narrowing) - - - -**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. - -**Why.** An app-authored body may not touch the two stored-metadata tables: for a body, the metadata protocol is their only writer, where a change is validated and its provenance is recorded. The runtime already enforces that where a body hook becomes a handler: such a hook is refused at registration and never runs. But `HookSchema` still accepted it, so the metadata save door answered 200 for a hook that would never fire, and the author learned otherwise only from a server log. - -**What is refused.** A hook carrying a `body`, in any form, whose `object` names `sys_metadata` or `sys_metadata_history`, as the string or as any member of the list. One such member refuses the whole hook, as the runtime does. The issue's `code` is `custom`, at `object` (or `object.N` for a list member), and its message names the table and ends with the runtime's prescription. The membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `HookSchema`, `defineHook()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `hooks.N.object`), `os validate`, which runs the same stack parse, an artifact's parse, and the metadata save door (`422 INVALID_METADATA`). - -**What stays accepted, byte for byte.** A hook with no `body` on those tables (a code `handler`, which is how the platform writes its own hooks), a wildcard (`object: '*'`) hook with a `body` (it names neither table: the runtime binds it and never runs its body for those tables' events), and every hook on any other object. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| a hook with a `body` and `object: 'sys_metadata'` or `object: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the hook | -| a hook with a `body` whose `object` list includes either table | drop those tables from the list; change metadata through the metadata API instead | -| a hook with a `body` on `'*'` or on any other object | unchanged | - -**The one-line fix: delete the hook, or remove `sys_metadata` and `sys_metadata_history` from its `object`, and make the change through the metadata API.** The runtime never ran such a hook, so removing it changes nothing an app does. - -**Who is affected, measured.** No authored hook targets either table in this repository's `packages/**` and `examples/**` at `44072fc2b9` (317 hook-shaped declarations, 24 of them outside tests; the only hits are the runtime's own tests of its registration refusal) or in hotcrm at `94668373f2` (44 declarations, 40 outside tests, no hit). Deployed metadata was not measured. A stored hook row of this shape still loads, now with a `[metadata_spec_invalid]` warning and a `_diagnostics` badge, and is still never bound. - -### The kit - -- **The refusal.** An object-level check attached to `HookSchema` with `.superRefine(...)`. A schema derived from `HookSchema` by overriding a key must use `.safeExtend()`, which keeps the check; zod refuses `.extend()` over a refined object. The artifact-stage hook in `@objectstack/spec` now derives that way. -- **The ledger.** The D3 semantic entry `hook-body-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused hook carries no intent a rewrite could keep. diff --git a/.changeset/21571-unprojected-read-declared-fields.md b/.changeset/21571-unprojected-read-declared-fields.md deleted file mode 100644 index 25d289817a2..00000000000 --- a/.changeset/21571-unprojected-read-declared-fields.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@objectstack/objectql': minor ---- - -fix(objectql)!: a read with no projection serves the object's declared fields and the platform's system columns, never a column no metadata declares (#21571) - -**BREAKING (narrowing)** — what a released read door serves shrinks. A column that -no metadata declares, typically a field retired in an upgrade whose column additive -schema sync leaves in the table until `os migrate apply --allow-destructive`, is no -longer returned by any read through the engine. - -| read | before | now | -| --- | --- | --- | -| `POST /api/v1/data/:object/query` or `GET /api/v1/data/:object` with no `fields` | every column of the table, retired ones and their values included | the declared fields, the registry's system columns, `id`, `created_at`, `updated_at` | -| `GET /api/v1/data/:object/:id`, export, search hits, the RPC dispatcher, `expand`ed records | the same whole row | the same declared set | -| `engine.find` / `engine.findOne` in process (hooks, flows, plugins), no `fields` | the whole row | the declared set | -| an explicit `fields` naming a declared field whose column does not exist yet (driver-sql retries `select('*')`) | the whole row, retired columns included | the declared set | -| `POST /api/v1/data/:object/:id/clone` of a record whose table carries a retired column | refused `INVALID_FIELD` (the copy carried the retired column into the insert) | cloned | - -**Unchanged:** naming a retired column in `fields` still answers `400 INVALID_FIELD` -on the data door. Declared fields keep their treatment: `internal: true` omission, -credential masking, formula evaluation and the hidden `__search` strip run as -before, and the registry-injected tenant, owner and audit columns are still served. -No driver changed: the engine shapes the rows any driver returns, so the answer is -the same on every driver and every door. Writes, and the rows a write returns, are -not changed by this release. - -**If you still read a retired column's values** (for example a one-time conversion -that copies the old columns into their replacement field): run that conversion -BEFORE upgrading to this release, while the old field is still declared, or, once -it lands, read the unmapped columns through the operator-only `os migrate` read -(objectstack#21573). There is no flag that re-opens undeclared columns on a runtime -door. An in-process reader that needs a column must declare it as a field. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21573-migrate-unmapped-columns.md b/.changeset/21573-migrate-unmapped-columns.md deleted file mode 100644 index e3ed45d9994..00000000000 --- a/.changeset/21573-migrate-unmapped-columns.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -feat(cli): `os migrate unmapped-columns --object NAME` reads the values of a retired field's columns, keyed by record id, for a conversion before `os migrate apply --allow-destructive` drops them (#21573) - -Clause-②: yes (widening) - -- **What it reads.** The columns `os migrate plan` reports as `unmapped_column` for one object's table: a column that is still in the table and that no metadata declares, typically one a retired field left behind. The column set is the plan's own findings, from the same differ on the same read-only boot, so the command never reads a column the plan does not report. Each record is emitted as `{ id, values }`. `--json` prints one document, `{ database, object, table, columns, count, records, duration }`. The text face lists the columns and each record's values. -- **Why it exists.** A read or a write through the engine now serves an object's declared fields only, and naming an undeclared column is refused. An app that moves a retired field's values into the field that replaced it reads them once with this command, writes them with its own script, and then drops the columns with `os migrate apply --allow-destructive`. That is the route the read and write narrowing in `@objectstack/objectql` names for this case. -- **Operator-only and read-only.** It runs under the database credentials you pass (`--database-url`, else `OS_DATABASE_URL`, else the project database), and it reads every organization's rows. No REST route, API flag or per-request option serves these values, and the runtime doors are unchanged. It boots the way `os migrate plan` does: no schema DDL, no seed data, and no database file created. -- **Values as stored.** An unmapped column has no declared type, so each value is emitted as the database client returns it, with no field-type decoding; a PostgreSQL `timestamp` arrives as a date and is emitted as its ISO 8601 text. A value JSON cannot carry as stored (binary bytes, a `bigint`, or a non-finite number) is refused in both faces with exit 1, naming the column and the record id, and no record is emitted: read that column with the database's own client. No column the platform creates for a field type answers with one of these, on SQLite or on PostgreSQL. -- **Answers.** An object with no unmapped column, or with no table yet: empty work, exit 0. No SQL driver: `os migrate plan`'s own `no_sql_driver` answer, exit 0. An undeclared object name: `OBJECT_NOT_FOUND`, exit 1. An object the plan does not diff (federated, or bound to another datasource): refused, exit 1. A read that cannot be complete, such as one stopped by `--max-records`: refused, exit 1, and no partial set is emitted. A value JSON cannot carry as stored: refused, exit 1, as above. -- `MigrateUnmappedColumnsCommand` is exported from `@objectstack/cli` beside the other `os migrate` commands. - -Nothing that ran before changes. This is a new command. diff --git a/.changeset/21576-install-local-uninstall-withdraws-registration.md b/.changeset/21576-install-local-uninstall-withdraws-registration.md deleted file mode 100644 index 4342a9a9c8c..00000000000 --- a/.changeset/21576-install-local-uninstall-withdraws-registration.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/cloud-connection": patch ---- - -An install-local uninstall (`DELETE /api/v1/marketplace/install-local/:manifestId`) now withdraws the package from the running kernel - -Clause-②: no - -- The DELETE used to remove the ledger entry and run the uninstall cleanups, but it left the package registered in the running kernel until the next restart. So another package's hot install re-ran the declared-permission seeding over the uninstalled package too. Its permission set came back as a package-managed row, and that row survived the restart as an orphan that an administrator could grant. -- After the ledger entry is removed, the door now calls `SchemaRegistry.uninstallPackage`, the same verb the protocol's own uninstall uses, on the same registry. It does this before the cleanups run. The package's objects answer 404 straight away, not only after a restart, and no reader of the registered packages counts it again. A reinstall of the same package in the same process registers it again. -- If the registry refuses the withdrawal, for example because another package extends an object this package owns, the uninstall still succeeds and the cleanups still run. The refusal is reported as a failed `registry.uninstallPackage` entry in `cleanups`. The operator log carries the cause and the remedy. -- The response `note` no longer says the kernel cannot unregister a package in place. The request and response keys are unchanged. diff --git a/.changeset/21585-hook-refusal-install-local.md b/.changeset/21585-hook-refusal-install-local.md deleted file mode 100644 index c26ac77acb6..00000000000 --- a/.changeset/21585-hook-refusal-install-local.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/runtime': minor -'@objectstack/cloud-connection': minor ---- - -fix(runtime,cloud-connection)!: install-local refuses a hook with no `body` and a job `body` that does not bind, and withholds such a hook on rehydrate (#21585) - -Clause-②: yes (narrowing) - - - -**BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses two more kinds of package it used to install with a 200: - -- **A hook with no `body`.** A hook in the deprecated function-name `handler` form names code that travels only in an artifact's runtime module, never in the package JSON this door installs. Such a hook used to install and then either never fire or bind by name to a function the package does not ship. Every hook is judged, since a hook has no on/off switch. A hook that carries both a `body` and a `handler` installs as before: its `body` wins. -- **An enabled job whose `body` does not bind.** The door used to judge only that a job `body` was present. It now judges that the body binds, by the declaration's own parse of `JobSchema.body`, the same parse the scheduler binds by. So a job whose `body` is an expression (L1) body, or carries `body.timeoutMs`, is refused instead of installed and never scheduled. - -- **The refusal.** The install answers `422` with `VALIDATION_ERROR`, the answer the door already gives an enabled job with no `body`. One answer names everything the door cannot run: each hook and the function its `handler` names, each job and its handler, and each refused job `body` with the key the declaration refuses. Nothing is installed: nothing is registered, persisted, bound or scheduled. `os package install` exits non-zero and prints the code beside the status. -- **Rehydrate.** A package installed by an earlier version keeps rehydrating after a restart. Its body hooks bind as before. A hook of it with no `body` is reported at `warn` by name and is **not bound**: this door carries no runtime module, so the hook's `handler` can never name the package's own code. Its job with no runnable `body` is reported and not run, as before. -- **Runtime.** The binder exports the two judgements the door reads: `collectHooksWithoutBody`, and `collectJobsWithoutBody`, which also names a job whose `body` does not bind. `bindAppArtifactHandlers` takes `withholdHooksWithoutBody`, which a door that carries no runtime module sets, and reports the hooks it withheld as `withheldHooks`. -- **Unchanged:** a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) binds an app's handler hooks to its own functions exactly as before. Hooks authored through the metadata API are unchanged too. A package whose hooks carry a `body` and whose enabled jobs carry a valid `body` installs exactly as before. - -The route for a refused package: give each hook a `body` (sandboxed JS, the form actions and jobs use), and correct each job `body` to the declared shape. That shape is a sandboxed JS body whose time limit is the job's own `timeoutMs`, and `os validate` reports the same refusal. Alternatively, boot the artifact with `os start --artifact`, which loads its runtime module. This ships as `minor`, under the launch-window convention for narrowings of an accept set. diff --git a/.changeset/21589-detail-entry-sort-field-retired.md b/.changeset/21589-detail-entry-sort-field-retired.md deleted file mode 100644 index aa70334828f..00000000000 --- a/.changeset/21589-detail-entry-sort-field-retired.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `object-master-detail-form` detail entry's `sortField` is retired — the console derives the line-position field from the child object and reads no authored value (#21589) - -Clause-②: no (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. - -`ComponentPropsMap['object-master-detail-form'].details[].sortField` named the child field the line grid stamps with each line's position on drag-reorder. The console stopped reading it: the field it stamps is derived from the child object, and the pinned console crossed that change while the spec still declared the key. So an authored `sortField` went through `os validate` clean and was dropped, and a drag-reorder stamped the derived field, or none (ADR-0049 enforce-or-remove). - -### FROM → TO - -| before | what to write instead | -| --- | --- | -| `details: [{ childObject: 'crm_invoice_line', sortField: 'line_no' }]` | delete `sortField`. The grid stamps the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`. | -| `sortField` naming a field outside that list | give the child object one of those fields; the line order is kept there. | -| an entry that names `relationshipField` and at least one column and gives every column a `type` | unchanged: the renderer keeps that entry exactly as authored, loads no child schema for it, and stamps no line position, before and after the upgrade alike. | - -**The one-line fix: delete `sortField` from every `object-master-detail-form` detail entry.** `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. - -**What an author now sees.** Writing the key fails `tsc` (its input type is the retired-key mark), and `os validate`, `os build` and `os lint` report it as a `component-props-invalid` warning carrying the prescription at `properties.details.N.sortField`. A page that carries it still saves and loads: a page component's `properties` is not parsed on the metadata save or load path. - -### The retirement kit - -- **Tombstone.** `sortField` is a `retiredKey()` on the strict detail entry. Its prescription prints the derived field names from their one declaration, a module reached by relative import only (`data/inline-grid-sort-fields.ts`), which the derived inline-grid columns read too. -- **D2 conversion `object-master-detail-form-detail-sort-field-removed`** (step 18, retired from the load path): a lossless delete of `sortField` from every `properties.details[]` entry of an `object-master-detail-form`, scoped by component type and by position. Stored `sys_metadata` pages and built artifacts replay it, one notice per entry. -- **D3 entry `object-master-detail-form-detail-sort-field-retired`** carries the judgment the delete cannot make: whether the child object declares the field the line order is kept in. -- **`RETIRED_KEYS_BY_MAJOR[18]`** registers the nested key `ui/ObjectMasterDetailFormProps:details.sortField`. -- **`record:line_items`' answer to `sortField`** no longer sends the author to the detail entry: no block takes an authored `sortField` any more. -- **No deprecation window**: the writer census is zero. - -**Measured producers: none.** On origin/main 9a4182a752, no `object-master-detail-form` detail entry in `examples/`, `apps/`, `packages/`, `skills/` or `content/docs/` writes `sortField`, against the sibling detail-entry key `addLabel` on the showcase project workspace's entry as the control, through the same instrument. At the objectui pin `89cad75d5570` the only detail entries that write it are probes asserting that nothing reads it. Deployed metadata NOT MEASURED. diff --git a/.changeset/21594-body-family-read-refusal.md b/.changeset/21594-body-family-read-refusal.md deleted file mode 100644 index a7f7311982d..00000000000 --- a/.changeset/21594-body-family-read-refusal.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/runtime': minor ---- - -fix(runtime)!: an app-authored body may not read the stored-metadata tables either; it reaches them through the metadata API only (#21594) - -Clause-②: yes (narrowing) - - - -**BREAKING**: this narrows what an app-authored body may do with the two stored-metadata tables, `sys_metadata` and `sys_metadata_history`. With the binding and write refusals already in this release, an app-authored body now reaches them through the metadata API only. - -- **Reading.** A sandboxed hook, action or job body's read of either table through `ctx.api` answers `PERMISSION_DENIED` / 403 before the read runs. That covers `find`, `findOne`, `count` and `aggregate`, inside a transaction or not, with or without elevation, and whatever filter, sort, grouping, search or projection the read carries. A body is served nothing of these tables, neither the stored row nor a projection of it, and the answer does not depend on what the read asks. A hook body that reads them fails the write that fired it. -- **Subject record.** An action body is not handed a row of either table as its subject record either. The `/actions` door loads an action's subject row before it dispatches. When that row is from either table, the call answers the same `PERMISSION_DENIED` / 403 before the body runs, instead of handing the body the row as `ctx.record`. That covers an action declared on either table (through a bundle, an installed package or the metadata door) and an object-less action addressed under one. A call that carries no record hands the body nothing and runs as before. -- **The route.** A body that read either table through `ctx.api.object(...)` was served the body projected and the content hash keyed; read metadata through the metadata API instead: `GET /api/v1/meta/:type/:name` for a definition, and `GET /api/v1/meta/:type/:name/history` for its versions. The refusal names that route. No shipped example reads either table from a body. -- **This supersedes, for bodies, two earlier entries of this release:** the served read of these tables, and the evaluate-shape refusals (`INVALID_FIELD` / 400) and default-search narrowing of that read. For a body, all of these now give way to this refusal. It also supersedes the binding-and-write entry's note that a body's reads are unchanged. -- **Unchanged:** host code that registers its own action handlers (its `ctx.api`, `ctx.engine.find` and subject record are still served as the generic data door serves these tables, with the door's evaluate refusals); the binding and write refusals; the platform's own readers; the generic data door and the metadata API; and every other object. - -It ships as `minor` under the launch-window convention for accept-set narrowings. diff --git a/.changeset/21595-sqlite-week-bucket.md b/.changeset/21595-sqlite-week-bucket.md deleted file mode 100644 index d3fcfb5b6ce..00000000000 --- a/.changeset/21595-sqlite-week-bucket.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-sql': patch -'@objectstack/service-analytics': patch ---- - -On SQLite, a `week` date bucket is grouped in SQL, and the analytics SQL echo never prints a bucket statement that SQLite refuses (#21595). - -Clause-②: no - -- **What was wrong.** `driver-sql` grouped `day`, `month`, `quarter` and `year` in SQL on SQLite, but not `week`. Its `supports.queryDateGranularity` said `week: false`, so the engine bucketed weeks in memory, and the ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` echoed the bucket as `date_trunc('week', col)`. SQLite has no `date_trunc`, so that echo could not run. A non-UTC `timezone` on SQLite gave the same echo for every granularity. -- **What it does now.** - - SQLite advertises all five granularities. `week` buckets as `YYYY-Www`, the ISO 8601 week that the PostgreSQL and MySQL arms answer. The expression does not use `strftime('%V')`, which needs SQLite 3.46: `@libsql/client` 0.18.0 bundles SQLite 3.45.1, where `%V` answers NULL. It runs on better-sqlite3, on libSQL (`driver-turso`) and on sql.js (`driver-sqlite-wasm`). A `Field.date` still buckets as its own calendar day. - - The echo prints that expression for a `week` bucket on SQLite, and the statement runs. - - With a non-UTC `timezone` on SQLite, `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. `POST /api/v1/analytics/query` still answers the rows, and its answer carries no `sql`. The engine buckets on that zone's calendar in memory, and SQLite has no time-zone database, so no SQLite statement produces those keys. -- **Where it shows.** `aggregate()` with a `week` group on SQLite, `SqlDriver.dateBucketSql()`, and the analytics SQL echo. A query sent with `timezone: 'UTC'`, or with no `timezone`, still echoes the driver's own expression. diff --git a/.changeset/21597-lifecycle-registry-first-guard.md b/.changeset/21597-lifecycle-registry-first-guard.md deleted file mode 100644 index 3a85dd937e5..00000000000 --- a/.changeset/21597-lifecycle-registry-first-guard.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -`LifecycleService` asks the registry whether `sys_organization` is registered before its governance tenant scan reads it, so a composition that registers no `sys_organization` sweeps single-tenant again instead of aborting every sweep - -Clause-②: no - -- The engine's in-process verbs refuse an object name the registry does not resolve (`OBJECT_NOT_FOUND`, 404) before any driver is asked. Take a composition with a settings service and lifecycle-declared objects but no `sys_organization` object. Its tenant scan got that refusal instead of a missing table, so every sweep aborted before applying any policy. The scan now asks `engine.registry.getObject('sys_organization')` first, the same shape `ObjectQL.probeInstallOrganizations` takes. An unregistered object answers "no tenant overrides", and the sweep runs one global pass on each declared window. -- A registered `sys_organization` is read as before. A missing table is still the one benign driver cause. Every other failure still aborts the sweep and is reported through `report.errors`, an `OBJECT_NOT_FOUND` from that read included. -- `LifecycleEngineLike['registry']` now declares the optional `getObject?(name)` member the scan reads. A registry without it cannot be asked, and the scan then reads exactly as before. No new export and no change to the sweep report's shape. diff --git a/.changeset/21602-package-scoped-job-identity.md b/.changeset/21602-package-scoped-job-identity.md deleted file mode 100644 index c7043d5a6bb..00000000000 --- a/.changeset/21602-package-scoped-job-identity.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): when two packages declare a job with the same name, both jobs now run. Uninstalling one stops only its own job. - -Clause-②: no - -The metadata registry keys a packaged item by package and name (`:`), so two packages may each declare a job called, say, `nightly_sync`. The job service keys a job by one string and replaces any job with the same name. Before this fix, installing the second package (`os package install`, or a second app on one boot) silently replaced the first package's job. That job stopped running while both installs reported success. - -- **Both jobs run.** A job is scheduled under its authored name unless another package already holds that name on the job service. In that case it is scheduled under the registry's package-scoped identity, `:`, and an `info` line names the package that holds the name. A package's job body and its `handler`'s `jobId` still see the authored name. -- **What an operator sees.** The Background Jobs catalogue (`sys_job`) and run history (`sys_job_run`) list the job under the name it is scheduled under. That is the authored name, or `:` for a package whose job name another package already holds. A reinstall keeps the name the job already has. -- **Each package cancels only its own job.** When a reinstall drops a job, or the package is uninstalled (the `runtime.package-jobs` uninstall cleanup), the job is cancelled under the name it was scheduled under. Another package's job with the same name keeps running. -- **Unchanged:** a runtime in which no two packages declare the same job name schedules every job under its authored name, so its catalogue and run history read exactly as before. No schema, export, `IJobService` contract or accept-set change. diff --git a/.changeset/21604-hook-handler-package-scope.md b/.changeset/21604-hook-handler-package-scope.md deleted file mode 100644 index 0cb4a78c321..00000000000 --- a/.changeset/21604-hook-handler-package-scope.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/objectql': minor -'@objectstack/spec': minor ---- - -fix(objectql,spec)!: a hook's `handler` name resolves inside the hook's own package only (#21604) - -Clause-②: yes (narrowing) - - - -**BREAKING**: a hook whose `handler` is a function NAME (the deprecated form, `handler: 'my_fn'`, with no `body`) now binds only to a function its own package holds. It used to fall back to the engine-wide function registry, which is keyed by bare name, so the hook could bind to a function another package registered under the same name and run that package's code on its own events. - -- **Accepted before:** a string `handler` resolved against the functions handed to the hook's bind, then against every function any package had registered on the engine. A name found nowhere was skipped with a `warn`. -- **Accepted now:** a string `handler` resolves against the functions handed to the hook's bind (the package's `functions`, which an `--artifact` runtime module supplies), then against the functions the same package (`packageId`) registered on the engine. Nothing else. -- **Refused now, at registration:** a name the hook's own package does not hold, whether another package registered it or nobody did. The hook is not bound. The refusal carries `INVALID_REFERENCE` with status `400` (ADR-0112), names the hook, the function and the package, and is recorded on the bind result (`BindHooksResult.errors[]` gains `code` and `status`) and logged at `error`. Under `strict` (`OBJECTQL_STRICT_HOOKS=1`) it is thrown. -- **The doors:** a hook authored at runtime through the metadata API (`PUT /api/v1/meta/hook/:name`) ships with no code package and holds no functions, so a `handler`-only hook authored there is refused when the door binds it; the save itself still answers as before. In a composition of several apps, one app's hook can no longer bind to another app's function. A bind that names no owning package (direct `bindHooksToEngine` use without `packageId`) resolves only the functions handed to it. -- **Unchanged:** a hook with a `body` binds as before. An app's hook naming its own `defineStack({ functions })` entry, or a function its own `--artifact` runtime module exports, binds as before. The install-local door's refusal of a hook with no `body` is unchanged. - -What to do with a refused hook: give it a `body` (sandboxed JS), or declare the function in the hook's own package's `functions`. To reuse another package's function, import it from the package that owns it and declare it there. This ships as `minor`, under the launch-window convention for narrowings of an accept set. diff --git a/.changeset/21613-write-result-declared-fields.md b/.changeset/21613-write-result-declared-fields.md deleted file mode 100644 index 7900c93c53d..00000000000 --- a/.changeset/21613-write-result-declared-fields.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -'@objectstack/objectql': minor ---- - -fix(objectql)!: the record a write returns, and the prior read it binds as `previous`, serve the object's declared fields and the platform's system columns, never a column no metadata declares (#21613) - -**BREAKING (narrowing)** — what a released write door returns shrinks. A column that -no metadata declares, typically a field retired in an upgrade whose column additive -schema sync leaves in the table until `os migrate apply --allow-destructive`, is no -longer returned by any write through the engine. This completes the read-side rule of -the previous release (#21571) for writes. - -| write | before | now | -| --- | --- | --- | -| `PATCH /api/v1/data/:object/:id`, batch `update`, `updateMany` | the post-write row read back with `select *`: retired columns with their stored values | the declared fields, the registry's system columns, `id`, `created_at`, `updated_at` | -| `POST /api/v1/data/:object`, `POST /api/v1/data/:object/:id/clone`, `createMany`, batch `create` / `upsert`, `insertMany` outcomes | the inserted row from `returning('*')`: retired columns as `null` | the same declared set | -| `data.record.created` / `data.record.updated` events, and the webhook deliveries that carry them as `after` | the same whole row | the declared set | -| `engine.insert` / `engine.update` in process (actions, flows, plugins), and a hook's `ctx.result` | the whole row | the declared set | -| a hook's `ctx.previous` on update and delete (by id and per row), and the audit ledger's delete `old_value` and create `new_value` | the whole stored row, retired columns and their values included | the declared set | - -**Unchanged:** declared fields keep their treatment. An `internal: true` field is still -returned whole on the engine-level write result to the privileged writer that just -wrote it, and still stripped from every data-door response; formulas are still -hydrated onto the result; the registry-injected tenant, owner and audit columns are -still returned. No driver changed: the engine shapes the rows any driver returns, so -the answer is the same on every driver and every door. A retired column's values stay -in the table. - -**If you still read a retired column's values off a write** (for example a hook, a -flow or a webhook receiver that copied an old column into its replacement): run that -conversion BEFORE upgrading to this release, while the old field is still declared, or, -once it lands, read the unmapped columns through the operator-only `os migrate` read -(objectstack#21573). There is no flag that re-opens undeclared columns on a write. A -reader that needs a column must declare it as a field; a validation rule, a -`readonlyWhen` or a hook condition that reads a column must name a declared field too. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21620-container-sibling-expansion-name.md b/.changeset/21620-container-sibling-expansion-name.md deleted file mode 100644 index fa582e0122f..00000000000 --- a/.changeset/21620-container-sibling-expansion-name.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -The runtime save door refuses a view container saved under a name another stored container of the same object expands to - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier name refusals shipped with. - -**What was accepted before.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, accepted an aggregated view container (`list` / `form` / `listViews` / `formViews`) saved under a name that another stored container of the same object expands to. For example, with `{ name: 'crm_lead', object: 'crm_lead', list: { … }, listViews: { pipeline: { … } } }` stored, a second container `{ object: 'crm_lead', list: { … } }` saved as `crm_lead.pipeline`. The second container became that name's own stored row, and an expansion fills only a name with no row of its own, so the first container's `crm_lead.pipeline` view was no longer served: the object door (`GET /api/v1/meta/view?object=…`), which never lists a container, listed nothing under the name, and the by-name read answered the raw second container. Nothing said why. - -**What is refused now.** That save, with `VALIDATION_ERROR` / 400, before anything is stored or registered, in draft and in publish mode. The other containers are the stored rows the read doors select for the same caller (environment-wide rows plus the caller's organization's), each expanded exactly as the read doors expand it, so every member kind (a bare or named `list`, `listViews`, `form`, `formViews`), the expander's de-duplicated names, and the names a container on another package's object expands under its own name are all judged where the readers place them. A container with no `name` is judged under the save name the door stamps on it. - -**What still saves.** A container under its object's name, which expands as before, and its own re-save. A container under any other name of its own that no other stored container of its object expands to: this door keeps a container saved under a name other than its object, and this change leaves that alone. A view item (a body carrying `viewKind`) under an expanded name, the sanctioned override for that name. The read doors are unchanged. A row stored in this shape before this change keeps its bytes and is served as before; `migrate meta --stored` and package duplication, which re-save stored rows through this door, report such a row as failed with this refusal instead of re-saving it. - -**The fix.** Add the view as a member of the stored container that already expands the name (in the example, the container `crm_lead`, whose `listViews.pipeline` is that view), or save a view item (`name`, `object`, `viewKind`, `config`) under the expanded name (`crm_lead.pipeline`). diff --git a/.changeset/21623-flow-read-node-evaluate-refusal.md b/.changeset/21623-flow-read-node-evaluate-refusal.md deleted file mode 100644 index a9e0a27a0ba..00000000000 --- a/.changeset/21623-flow-read-node-evaluate-refusal.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(service-automation): a flow's `get_record` node refuses a filter that evaluates the stored-metadata tables' body or content hash, as the generic data door does (#21623) - -Clause-②: no - -The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and content-hash columns computed over it. A flow's `get_record` node now serves those rows projected and keyed, but it still ran its `filter` against the stored values as written, under either run identity (`runAs: 'system'` and `runAs: 'user'`). A filter over the body column or a content-hash column was evaluated row by row, so whether a row came back answered the filter: a predicate over the withheld values. The generic data door refuses those filters before its query runs. - -**What changes.** When the node reads either table, it judges its filter the way the data door judges the same filter, before the data engine is asked, on both branches (one row, and a row list when `limit` is above 1). The columns the filter reads are collected after interpolation, so a condition that a `{token}` supplies is judged too. A filter that reads the body column, or a content-hash column (the history table's parent hash and change note included), refuses the node with the data door's own message and error code, `INVALID_FIELD`. The refusal is a guard failure: the run fails, nothing downstream of the node runs, and a `fault` edge does not route it. A `try_catch` catch region reads the code on `{$error.code}`. To read a stored-metadata row from a flow, filter by `name`, `type`, `state` or another scalar column. - -**What does not change.** A filter over scalar columns is served as before: the body projected and the hash keyed. Every other object is filtered and read exactly as before, including columns that share these names. The write nodes are unchanged. The node consumes the data door's own functions from `@objectstack/metadata-protocol` (`collectStoredMetadataFilterFields`, `storedMetadataBodyPredicateRefusal`, `storedMetadataHashEvaluateRefusal`) and keeps no copy of them. diff --git a/.changeset/21624-flow-write-nodes-family-refusal.md b/.changeset/21624-flow-write-nodes-family-refusal.md deleted file mode 100644 index ee83ec7be07..00000000000 --- a/.changeset/21624-flow-write-nodes-family-refusal.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(service-automation): a flow's `create_record`, `update_record` and `delete_record` nodes refuse a stored-metadata table as their target (#21624) - -Clause-②: no - -The two stored-metadata tables (the current metadata bodies and their version history) have one writer for app-authored work: the metadata protocol, where a change is validated and its provenance is recorded. A flow's write nodes wrote those tables directly, outside it. Under `runAs: 'system'` the write ran elevated, so the security middleware never judged it; under `runAs: 'user'` only a composition with the security plugin refused it, as a routable runtime failure with no code. A write node's `filter` was also evaluated against the stored rows, so whether the write acted answered a predicate over the stored body. - -**What changes.** A `create_record`, `update_record` or `delete_record` node whose `objectName` is either table is refused before it resolves its filter or its field values and before any engine write, under either run identity. The refusal names the metadata API as the way to change metadata and carries the standard `PERMISSION_DENIED` code, the code the data door answers a non-platform principal's write to these tables with. It is a guard failure: the run fails, nothing downstream of the node runs, and a `fault` edge does not route it. A `try_catch` catch region reads the code on `{$error.code}`. Metadata is changed through the metadata API (`PUT /api/v1/meta/:type/:name`), never through a flow's data nodes. - -**What does not change.** Every other object is created, updated and deleted exactly as before. `get_record` keeps serving these tables projected and keyed. diff --git a/.changeset/21624-flow-write-nodes-shared-prescription.md b/.changeset/21624-flow-write-nodes-shared-prescription.md deleted file mode 100644 index e6b72db1f3f..00000000000 --- a/.changeset/21624-flow-write-nodes-shared-prescription.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -fix(service-automation): a flow write node's refusal of a stored-metadata table ends on the same prescription sentence as the save-time refusal (#21624) - -Clause-②: no - -A flow `create_record`, `update_record` or `delete_record` node aimed at a stored-metadata table is refused twice: at save by `FlowSchema`, and at run time by the node itself, for a definition the parse never judged. Both refusals tell the author where a metadata change goes instead, and until now they said it in two spellings of one sentence: the run-time refusal named the elevation as `runAs: 'system'`, the save-time one as `runAs`, a system context. - -**What changes.** The run-time refusal's message keeps its lead (the node type, what it would have done and the table, and that the write was not run) and now ends on `STORED_METADATA_BODY_PRESCRIPTION`, imported from `@objectstack/spec/kernel`: the one sentence the save-time refusal and the hook refusal also end on. Its elevation clause now reads "Elevation (`runAs`, a system context) does not change this." - -**What does not change.** Which writes are refused, the refusal's `PERMISSION_DENIED` code, its guard classification (a `fault` edge does not route it) and every other object's writes are exactly as before. diff --git a/.changeset/21630-echo-refuses-in-memory-bucket.md b/.changeset/21630-echo-refuses-in-memory-bucket.md deleted file mode 100644 index 16af18660ba..00000000000 --- a/.changeset/21630-echo-refuses-in-memory-bucket.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -With a non-UTC `timezone`, the analytics SQL echo of a date-bucketed dimension refuses on every dialect instead of printing `date_trunc` (#21630). - -Clause-②: no - -- **What was wrong.** With a non-UTC `timezone`, the engine buckets a date dimension in memory on that zone's calendar, on every driver. The ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` still echoed the bucket as `date_trunc('month', col)` (or the asked granularity) on PostgreSQL and MySQL, a statement the engine never ran. On PostgreSQL that statement groups on the database session's calendar: measured on PostgreSQL 16.14 with the server at `Asia/Shanghai`, it answered timestamp keys such as `2025-12-31T16:00:00.000Z` where the query answered `2026-01`, and with `timezone: 'America/New_York'` it grouped the rows differently from the query. MySQL has no `date_trunc` at all. SQLite already refused this echo. -- **What it does now.** For a date-bucketed dimension with a non-UTC `timezone`, on every dialect: - - `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. This is the answer SQLite already gave. - - `POST /api/v1/analytics/query` answers the same rows as before, and its answer carries no `sql`. -- **Unchanged.** A query sent with `timezone: 'UTC'`, or with no `timezone`, still echoes the expression the driver groups by: `to_char(…)` on PostgreSQL, `date_format(…)` on MySQL and `strftime(…)` on SQLite. diff --git a/.changeset/21639-view-container-name-collision.md b/.changeset/21639-view-container-name-collision.md deleted file mode 100644 index 6afe55ab1e6..00000000000 --- a/.changeset/21639-view-container-name-collision.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -The runtime save door refuses a view container whose save name, or any name its expansion produces, is a name already served from elsewhere; a package-less container row named after a view item a package ships belongs to no package - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier container-name refusals shipped with. - -**One rule.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, now refuses an aggregated view container (`list` / `form` / `listViews` / `formViews`) when its save name, or any name its expansion produces, is already served from elsewhere: by another stored container's expansion in the caller's selection (environment-wide rows plus the caller's organization's), whatever that container's object, or by a view item (a body carrying `viewKind`) a package ships. A name the container's own expansion produces is refused as its save name too. Every name is judged where the read doors place it, so every member kind, the expander's de-duplicated names and a container on another package's object (which expands under its own name) are all covered. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or registered, and it names the other owner: the stored container, the shipping package, or the container's own expansion. - -**Before and after, per shape** (with `{ name: 'crm_lead', object: 'crm_lead', list, listViews: { pipeline } }` stored where a sibling is named): - -- A container bound to **another object**, saved under a sibling's expanded name (`{ object: 'crm_account', list }` as `crm_lead.pipeline`). Before: accepted; the sibling's `crm_lead.pipeline` view was no longer served on either door, and the by-name read answered the raw container. After: refused, naming the container `crm_lead`. -- An **unbound** container (`{ list }`) under the same name. Before and after: as above. -- A **second container of one object** whose bare `list` takes `crm_lead.default`, under a free name (`{ object: 'crm_lead', list }` as `lead_other_views`), or the object-named container saved after such a one. Before: accepted; whichever container was read last replaced the other's default on both doors, and nothing said why. After: refused, naming the stored container that already serves the name. -- A container saved under the name of a **view item a package ships** (`{ name: 'showcase_task.in_progress', object: 'showcase_task', list }` as `showcase_task.in_progress`), package-less, organization-scoped or in a writable package. Before: accepted; the packaged view was no longer served on the object door and the by-name read answered the raw container. Package-less, the row was also judged a container of the shipping package, so its bare `list` replaced the packaged `showcase_task.default` on both doors, wearing that package's `_packageId`. After: refused, naming the shipping package. -- An overlay of a package's own container whose new member takes the name of a view item **the package ships on its own**. Before: accepted; the member replaced that packaged view on both doors. After: refused, naming the package. -- A container under a name its own expansion produces, or under a name another stored container of the same object expands. Refused before and after, with the same envelope. The own-expansion refusal no longer tells the author to save the container under its object's name when a stored container already holds that name; it names that container to add the view to. - -**What still saves.** A view item under any of these names: it is that name's sanctioned override. A container under its object's name, or under any other name of its own, whose expansion takes no name served elsewhere, and its own re-save. An overlay of a package's own container under that container's name. A container on another package's object, which expands under its own name. A container whose would-be sibling is in another organization: the caller's own selection decides, as it does for the read doors. - -**Rows stored before this change.** They keep their bytes and are served as before, with one change: a package-less container row stored under the name of a view item a package ships now belongs to no package. On that package's object it expands under its own name (`showcase_task.showcase_task.in_progress` for a bare `list`), with no `_packageId` and no default, and the packaged views it used to replace are served again on both doors. The row itself still takes its own name's slot, as any stored row does. A new save of a row in a refused shape, a re-save included, is refused until its body stops colliding; `migrate meta --stored` and package duplication report such a row as failed with this refusal instead of re-saving it. Delete stays open. - -**The fix.** Add the view as a member of the stored container that already serves the name (its `list`, `listViews`, `form` or `formViews`), or save a view item (`name`, `object`, `viewKind`, `config`) under that name to override it. For a name a package ships: save a view item under it to override the packaged view, or save the container under a name of its own that no package ships and no stored container expands. diff --git a/.changeset/21644-narrowed-apply-flag.md b/.changeset/21644-narrowed-apply-flag.md deleted file mode 100644 index 0c88c5f9d17..00000000000 --- a/.changeset/21644-narrowed-apply-flag.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/cli': patch -'@objectstack/service-storage': patch ---- - -`os migrate value-shapes` and `os migrate files-to-references` record the deployment-level ADR-0104 flag only from a run over every object, and every command in the `os migrate` data-migration family refuses an `--object` name the deployment does not declare (#21644). - -Clause-②: no - -- **A narrowed `--apply` records no deployment flag.** The flag attests the stored data of every object and turns strict enforcement on, but a run narrowed by `--object` reads only the named objects. Such a run still applies its fixes: `files-to-references` converts the named objects' values. It records no flag, whether it passes or fails, and leaves a flag that an earlier full-scope run recorded exactly as it was. Its output says why and names the run that records the flag: the same command without `--object`. The `--json` document carries `filter: { objects }`, which is `null` on a full-scope run, so a narrowed run is never mistaken for a full one. Any `--object` narrows, even a list that names every object. A full-scope `--apply` records the flag as before. -- **`runFilesToReferencesMigration`** (`@objectstack/service-storage`) skips the flag write when it is given `objects`. That includes `[]`, which walks nothing. Its `flag` result is `null` on a narrowed run. -- **The column step of `files-to-references` does not run on a narrowed run.** It retypes every single-value media column in the database on the authority of the gate, and a narrowed gate vouches only for the named objects. Before this change, a narrowed `--apply` or a misspelled one moved those columns and stamped `columns_moved_at`. -- **An unknown `--object` is an error.** This applies to `value-shapes`, `files-to-references`, `summary-nulls` and `duplicates`. A name the booted registry does not declare exits 1 with `OBJECT_NOT_FOUND`, and the error names that name and the declared objects. The check runs before anything is read or written. Until now, such a name was filtered out of the scan without a word, so a typo scanned nothing and read as a clean run. `duplicates` reports the refusal as `{ error: 'report_failed', detail, code }`. A declared object that the command has nothing to check on is still accepted. diff --git a/.changeset/21646-seed-created-at.md b/.changeset/21646-seed-created-at.md deleted file mode 100644 index 1d1f55715e0..00000000000 --- a/.changeset/21646-seed-created-at.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -A seed row's authored `created_at` is kept when the row is first inserted, the same as when a later boot replays it (#21646). - -Clause-②: no - -- **Before.** The built-in audit stamp (`sys_stamp_audit_insert`) replaced a seed row's authored `created_at` with the boot instant on insert. A later boot's upsert update then wrote the authored value, so a fresh or reset database showed every seeded record as created at boot until the next restart. This held for a literal instant and for a `cel` value such as ``cel`daysAgo(5)` ``. -- **After.** Under the seed write context (`ExecutionContext.seedReplay`, set by `SEED_WRITE_EXECUTION_CONTEXT`), the insert stamp keeps an authored `created_at` and stamps the boot instant only when the row has none. Both paths now store the authored value. The update stamp is unchanged, so `updated_at` still moves on a replay. All three seed writers pass that context: `SeedLoaderService`, `AppPlugin`'s replay of a stack's `data[]`, and `@objectstack/verify`'s `seed()`. -- **Unchanged.** A REST create, a create from a bare `isSystem` context and every other caller still have `created_at` stamped now. `preserveAudit` is unchanged, and the seed context does not gain it. A non-system create that requests `preserveAudit` gets the same warning as before. `created_by` is not stamped on a seed write, because the seed context has no user. An authored value is kept on insert and on replay, as it was before this change. -- ⛔ No schema, key, export or error code is added or removed. diff --git a/.changeset/21647-echo-never-in-memory-bucket.md b/.changeset/21647-echo-never-in-memory-bucket.md deleted file mode 100644 index 27b673fb25f..00000000000 --- a/.changeset/21647-echo-never-in-memory-bucket.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-analytics': patch ---- - -The analytics SQL echo prints a date bucket only in the expression the driver itself groups it by, and refuses everywhere else, including on the in-memory and MongoDB drivers (#21647). - -Clause-②: no - -- **What was wrong.** At a `timezone` of `UTC`, or with none, the ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` echoed a date-bucketed dimension as `date_trunc('month', col)` (or the asked granularity) wherever the driver renders no bucket expression of its own, and documented that as representative. On `driver-memory` the engine only fetches the rows and buckets them itself, answering keys such as `2026-01` and `2026-W02`, while both faces printed `date_trunc(...)`, a statement nothing ran. `driver-mongodb`, which groups the bucket in its own aggregation pipeline, took the same path. So did any host that wires no `dateBucketSql` hook. -- **What it does now.** Wherever no driver expression stands for the bucket, on every driver and dialect: - - `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. Its message names the cause. A non-UTC `timezone` and SQLite already answered this way. - - `POST /api/v1/analytics/query` answers the same rows as before, and its answer carries no `sql`. -- **Unchanged.** On PostgreSQL, MySQL and SQLite at `UTC` or with no `timezone`, the echo still prints the expression the driver groups by: `to_char(...)`, `date_format(...)` and `strftime(...)`. diff --git a/.changeset/21654-flow-write-node-stored-metadata-target-refused.md b/.changeset/21654-flow-write-node-stored-metadata-target-refused.md deleted file mode 100644 index affe25ceff7..00000000000 --- a/.changeset/21654-flow-write-node-stored-metadata-target-refused.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A flow `create_record`, `update_record` or `delete_record` node whose `objectName` is `sys_metadata` or `sys_metadata_history` is refused at parse, with the runtime's prescription: change metadata through the metadata API. - -Clause-②: yes (narrowing) - - - -**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. - -**Why.** App-authored work may not write the two stored-metadata tables: the metadata protocol is their only writer, where a change is validated and its provenance is recorded, and a flow is app-authored automation. The runtime already enforces that at the node: the three write nodes refuse such a target before they resolve a filter, compute a field or call the data engine, under every run identity. But `FlowSchema` still accepted the flow, so `objectstack validate` passed it, the metadata save door answered 200 for it and `registerFlow` registered it, and the author learned otherwise only at its first run. - -**What is refused.** A `create_record`, `update_record` or `delete_record` node, at any depth including an ADR-0031 region body, whose `config.objectName` is a string naming `sys_metadata` or `sys_metadata_history`. The issue's `code` is `custom`, at `nodes.N.config.objectName`, and its message names the node type and the table and ends with the runtime's prescription. The judge is `flowNodeConfigRefusals`, the one `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share, and its membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.objectName`), `os validate`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`). The refusal joins the closed flow slot refusal set as `write-node-stored-metadata-target`, with `params: { nodeType, objectName }`. - -**What stays accepted, byte for byte.** A `get_record` node on those tables (a read is not a write; the runtime judges its reach at the run), a write node whose `objectName` is dynamic (a `{token}` template or an expression envelope: the parse cannot read it as a name, and the runtime judges the name it hands the data engine), and every write node on any other object. - -**One prescription sentence.** `@objectstack/spec/kernel` now exports `STORED_METADATA_BODY_PRESCRIPTION`, the sentence the hook refusal and this flow refusal both end on. It was the hook refusal's private constant, moved unchanged. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| a `create_record` / `update_record` / `delete_record` node with `objectName: 'sys_metadata'` or `objectName: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the node | -| a write node on any other object, a `get_record` node, or a dynamic `objectName` | unchanged | - -**The one-line fix: delete the node, or point its `objectName` at the object the flow really means to write, and make the metadata change through the metadata API.** The runtime never ran such a write, so removing it changes nothing a flow does. - -**Who is affected, measured.** No authored flow writes either table in this repository's `packages/**`, `examples/**`, `skills/**`, `content/docs/**` or `docs/**` at `417443eb27` (229 write-node declarations); the only hits are the runtime's own tests of its node refusal. Deployed metadata was not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register. - -### The kit - -- **The refusal.** A third arm of `flowNodeConfigRefusals` (`automation/flow-node-config-refusals.ts`), beside the executor-contract arm and the decision arm. -- **The ledger.** The D3 semantic entry `flow-write-node-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused node carries no intent a rewrite could keep. diff --git a/.changeset/21658-hook-handler-without-body-save-door.md b/.changeset/21658-hook-handler-without-body-save-door.md deleted file mode 100644 index 356d033ad2e..00000000000 --- a/.changeset/21658-hook-handler-without-body-save-door.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -The runtime save door refuses a hook whose `handler` names a function and that carries no `body`: a hook stored there ships with no code package, so that name can never bind - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier refusals shipped with. - -**One rule.** `saveMetaItem`, which `PUT /api/v1/meta/hook/:name` and the dispatcher's metadata save both call, now refuses a `hook` whose `handler` is a function name and that carries no `body`. A hook stored through this door ships with no code package, so it holds no functions, and a `handler` name resolves only inside the hook's own package. Before this change the door answered 200, the runtime then refused the hook at bind (`INVALID_REFERENCE` / 400, in the server log only), and the hook never ran. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or bound. It names the hook and the function, and prescribes a `body`. - -**Before and after** (with `{ name: 'stamp_status', object: 'crm_note', events: ['beforeInsert'], handler: 'x_stamp' }`): - -- Before: 200 `Saved hook 'stamp_status'`, the row stored, the hook refused at bind and never run, and nothing on the response said so. -- After: 400 `VALIDATION_ERROR`, naming `stamp_status` and `x_stamp`, and nothing stored. - -**What still saves.** A hook with a `body`. A hook carrying both a `body` and a `handler`: the binder runs the body and never consults the name, and the install-local door accepts the same shape. A malformed `body` still gets the type schema's located `422 INVALID_METADATA`. - -**What is unchanged.** `HookSchema` still accepts the string `handler`, because a build artifact carries it: `objectstack build` lowers an inline function to the hook's name and ships the function in the artifact's runtime module. A hook in an artifact or a `defineStack` config binds to its own package's functions on its own door, which never reaches this one. `os validate` and `os build` are unchanged. - -**Rows stored before this change.** They keep their bytes, nothing re-saves them, and the runtime refuses them at bind as before. A new save of one, a re-save included, is refused until it carries a `body`. Package duplication reports such a row as failed with this refusal; `migrate meta --stored` leaves it as it is. Delete stays open. - -**The fix.** Give the hook a `body`: sandboxed JS (`{ language: 'js', source }`) or an expression (`{ language: 'expression', source }`). A hook that must run a package's own function belongs in that package's code, where its `handler` resolves. diff --git a/.changeset/21663-readonly-value-shape-refused.md b/.changeset/21663-readonly-value-shape-refused.md deleted file mode 100644 index a410a1305a3..00000000000 --- a/.changeset/21663-readonly-value-shape-refused.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -'@objectstack/objectql': minor ---- - -fix(objectql)!: a system write's readonly value is judged for its shape — a seed's `'yesterday'` on a readonly datetime is refused with the sentence any other field gets, never stored (#21663) - -**BREAKING** — a write that keeps a readonly value now has that value's SHAPE -checked. The static readonly strip still exempts a system write (seed replay, -migration, `isSystem` plugin code, a `before*` hook's stamp) and still drops a -non-system caller's readonly value; what changed is that the value the -exemption keeps is no longer stored unjudged. Before, the record validator -skipped every readonly field, so under `isSystem` a malformed readonly value -reached the driver verbatim — a seed's `run_at: 'yesterday'` on a readonly -`datetime`, an unresolved `cel` envelope from a seeder that skips its -resolution, an authored `created_at` the seed now keeps — while the same value -on a non-readonly field was refused. - -Now it is refused the same way: `VALIDATION_FAILED` (400 at an HTTP boundary), -the same field code and the same sentence a non-readonly field gets -(`Run At must be a valid datetime (ISO-8601)`), and a seed counts the row as a -seed error. This holds on insert, on the dry run (`ObjectQL.validate`), and on -both update paths, where the readonly values left after the strip are judged. - -Which checks a readonly value reaches — its type's shape, never a constraint: - -- refused: a `date` / `datetime` / `time` the platform does not read, a - non-number on a number-typed field, a non-boolean on a boolean, a non-array on - a multi-value field, a filter-operator object, and an ADR-0104 reference / - media / structured-JSON shape under the object's own posture (warn-first, as - on any other field, until the deployment's evidence enforces it); -- NOT checked, exactly as before: option membership, `maxLength` / - `minLength`, `valueDomain`, `min` / `max` / `scale` / `precision`, the email / - url / phone formats, and `required`. Option membership stays out on purpose: - `sys_activity.type` is a readonly `select` whose options are the built-in set - of an open vocabulary, and an author-contributed value there is stored. - -A numeric string on a readonly number field is now written as its number, and a -lone scalar on a readonly multi-value field as a one-member list, as on any -other field — the door reads the value the same way it judges it. - -**What moves for consumers.** A seed, migration or `isSystem` write that puts a -malformed value in a readonly field — or a hook that stamps one — is refused -where it was stored. Fix the value at its producer: write an ISO-8601 instant -(or a `Date`) into a readonly `datetime`, resolve a `cel` value before the -write, and stamp numbers and booleans as such. Rows already stored are never -re-read or rewritten. `validateRecord`, as exported, is unchanged: the readonly -scope is the engine write path's own. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21665-seed-replay-per-organization-ids.md b/.changeset/21665-seed-replay-per-organization-ids.md deleted file mode 100644 index 1b766a98950..00000000000 --- a/.changeset/21665-seed-replay-per-organization-ids.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -A per-organization seed replay now gives each organization its own row identity. On a walled deployment, every organization created after the first used to start without the app's fixed-id seed rows. The showcase's `sys_business_unit` tree is one example. The replay inserted each authored `id` again, every insert was refused as a duplicate on the global primary key, and the parent references into those rows stayed unresolved. - -Clause-②: no - -- A row authored with an `id` keeps that id while no row holds it. The first organization a seed is replayed into is unchanged: it gets exactly the ids the seed authors. -- When another organization (or an organization-less row) already holds the authored id, the row gets an id derived from the authored id and the organization. A second replay into the same organization finds that row again, so it is not inserted twice. -- References in the same replay that name the authored id follow the row to its new id, in pass 1 and in pass 2. This includes a UUID-shaped authored id, which used to be kept verbatim and would have linked to another organization's row. -- The replay logs one `info` line per dataset that it re-identified. -- The rule lives in `SeedLoaderService`, so every load that names an organization follows it: the per-organization replayer, and package apply, draft publish and marketplace install into an organization. -- Boot seeding without an organization, dry runs and rows without an authored `id` are unchanged. -- ⛔ No schema, export, accepted input or error code changes. diff --git a/.changeset/21666-create-explicit-organization-meets-wall.md b/.changeset/21666-create-explicit-organization-meets-wall.md deleted file mode 100644 index 4c294624385..00000000000 --- a/.changeset/21666-create-explicit-organization-meets-wall.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@objectstack/organizations": minor ---- - -fix(organizations)!: a create that names an `organization_id` meets the Layer 0 write wall, as the update does — the insert stamp no longer rewrites it (#21666) - -Clause-②: no (narrowing) - -**BREAKING.** On a walled posture (`isolated` / `group`), the insert stamp (Middleware A) overwrote a supplied `organization_id` with the caller's active organization in every user context. A create naming another tenant's organization answered `201` and stored the row in the caller's own organization, while the PATCH naming the same organization and the array insert (`createMany`) were refused `403 PERMISSION_DENIED`. One operation answered two ways, and the caller of the `201` had no signal that its input had been replaced. - -The stamp now fills only an absent or empty `organization_id`, for every non-system context (ADR-0105 D5). A supplied value is left as sent and meets the Layer 0 write wall in `@objectstack/plugin-security` (ADR-0095 D1), which answers the create exactly as it answers the update: - -- **Another tenant's organization** → `403 PERMISSION_DENIED`, nothing stored (was `201`, stored in the active organization). This holds for a member and for a platform administrator on a tenant object. A member's forged `organization_id` stays refused; the wall refuses it now, where the stamp used to rewrite it. -- **No organization** → stamped with the active organization, as before. -- **The caller's own active organization** → admitted, as before. -- **Under `group`, a sister organization the caller holds** → admitted and stored in that organization, the same place the PATCH already moves a row to (was `201`, stored in the active organization). Where `organization_id` is the platform-injected column, the engine still strips it from a non-system payload as `readonly` and reports it in `droppedFields`, on the create as on the update. -- **A platform administrator on a posture-permitting object** (`private`, platform-global, better-auth-managed) is exempt from the wall on the create as on the update. - -Every door that writes one row at a time under the caller's context gives the same answer: `POST /data/:object`, the `create` operation of `POST /batch`, the clone route and the import runner's per-row fallback. Two of these change in ways worth knowing: - -- An import row naming another tenant's organization is now reported as a failed row (`PERMISSION_DENIED`). Before, it was created in the active organization. -- The clone route copies an `organization_id` that the object declares itself. So under `group`, a clone of a sister-organization row now lands beside its source instead of in the active organization. - -System contexts are unchanged. The per-organization seed replay, the orphan claim, migrations and every other `isSystem` writer meet neither the stamp nor the wall. The `single` posture is unchanged too, because `objectstack serve` mounts this package only under a walled posture. - -**What to do.** On create, either omit `organization_id` or name your active organization. If a platform operator needs a row in another organization, write it with a system-context write. - - diff --git a/.changeset/21669-cloned-permission-set-not-unowned.md b/.changeset/21669-cloned-permission-set-not-unowned.md deleted file mode 100644 index 5e96402e777..00000000000 --- a/.changeset/21669-cloned-permission-set-not-unowned.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -fix(plugin-security): a permission set the environment cloned no longer logs `permission_set_declaration_unowned` on every boot (#21669) - -Clause-②: no - -The declared-permission seeding pass walks every `permission` item in the engine registry. That registry also holds the permission sets an environment authored itself, which boot hydration loads from their `sys_metadata` rows. A set made with Setup's **Clone** action is one of them, and it carries no package id because it has none. The pass judged "no owning package" before it looked at the set's row, so on every boot, and on every `metadata:reloaded`, it logged one warning per cloned set: `[permission_set_declaration_unowned] declared permission set "…" has no owning package — not materialized … the Setup admin surface reads sys_permission_set and cannot see this set`. That is false for a clone. Its row exists (`managed_by: admin`), and Setup lists it and edits it. - -The pass now checks the row first. A registry item with no package id whose `sys_permission_set` row the environment owns (`managed_by` other than `package`) is the environment's own set. It is counted as `skippedEnvAuthored`, the count a package declaration over an environment row already gets, and no warning is logged. The pass reads that row from the existence read it already makes, so no query is added. On a per-organization pass, the environment door's organization-less row counts too. - -Unchanged: a declaration with no owning package and no environment row still logs `permission_set_declaration_unowned`, with the same text, and still counts as `skippedUnowned`. If the row could not be read, the warning still fires, because an unreadable row does not prove the environment owns the set. The publish-time materializer is unchanged. Nothing is written or granted differently: only the false warning stops, and the clone moves from the `skippedUnowned` count to the `skippedEnvAuthored` count in the pass's summary line. diff --git a/.changeset/21670-read-envelope-lock-flags.md b/.changeset/21670-read-envelope-lock-flags.md deleted file mode 100644 index dbe36580bd0..00000000000 --- a/.changeset/21670-read-envelope-lock-flags.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch -'@objectstack/spec': patch ---- - -The metadata reads' `lock` / `editable` / `deletable` now say what the write doors do with a packaged item - -Clause-②: no - -Both metadata reads publish the ADR-0010 protection envelope beside the item: `GET /api/v1/meta/:type/:name/layers` (and its deprecated `?layers=true` spelling), and the by-name read `GET /api/v1/meta/:type/:name` where it resolves the envelope. The envelope was resolved from the item's own `_lock` alone, so it ignored the other refusal the write doors apply: an item a code package ships, on a type with no per-org overlay channel, is locked against in-place edits. - -**Before.** A packaged flow, action, object, hook, seed, mapping, datasource, external catalog, doc, picklist, field, job, api, capability or agent with no `_lock` read `lock: 'none'`, `editable: true` and `deletable: true`. A packaged page, app, dataset, book, permission set, position, tool or skill read the same. Yet `PUT` refused each of them with `403 NOT_OVERRIDABLE` (or `403 ITEM_LOCKED` when the write names the read-only package), and the removal of the first group was refused too. - -**After.** Each read reports what its doors answer: - -- The first group reads `lock: 'full'`, `editable: false` and `deletable: false`. -- The second group reads `lock: 'no-overlay'`, `editable: false` and `deletable: true`. Removing a leftover overlay row of these types is allowed: that is the repair path for overlays written before their per-org channel was withdrawn. -- Items of the overlay types (`view`, `dashboard`, `report`, `translation`, `email_template`) are unchanged. So are items no package ships, such as an organization's own flows and actions, and every item while the `OS_METADATA_WRITABLE` operator hatch opens its type. - -An item's own `_lock` still applies on top: the two refusals join, and neither replaces the other. `lockReason`, `lockSource` and `lockDocsUrl` are still present only when the item declares them. `provenance` and `packageId` already name the package. - -The verdict is the one the write doors already share, read rather than re-derived, so the read moves whenever a door moves. The `lock` field's description in `@objectstack/spec` now names both refusals it reports. No key, type or accepted value changes. - -**What to do.** Nothing, unless a client gated an edit or delete affordance on `editable` / `deletable`: it now hides that affordance for packaged items the server refuses, instead of offering a write that answers 403. The refusal itself names the sanctioned route for each type: for a packaged flow, clone it under a new name or switch it off; for a packaged action, switch it off. diff --git a/.changeset/21671-html-literal-type-mismatch-error.md b/.changeset/21671-html-literal-type-mismatch-error.md deleted file mode 100644 index d7674e66f4e..00000000000 --- a/.changeset/21671-html-literal-type-mismatch-error.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/sdui-parser': minor ---- - -A `kind: 'html'` page that hands a component input a literal of the wrong type now fails to compile, instead of compiling with a warning. - -Clause-②: no (narrowing) - - - -**BREAKING**: an accept-set narrowing on the html page compiler, shipped as `minor` under the launch-window convention for accept-set narrowings. No export, type or diagnostic code changes. - -**What changed.** `compile()` used to grade a `type-mismatch` as an `error` only when the input declared an `enum` arm, and as a `warning` otherwise. Every value the type check sees is a literal written in the source: a quoted attribute is a string, a bare attribute is `true`, and a braced value is the exact literal written. (A braced value that is not a literal is reported separately as `inert-expression`, which stays a warning.) A literal's type is known when the page compiles, so a mismatch is certain, and it is now an `error` for every declared type. `member-type-mismatch`, the same check applied to the members of an array or map, follows the same rule. Codes and messages are unchanged. - -**Why.** `aggregate="count"` on an `object-metric` passed `os build` with one warning. The tile reads `aggregate.function` and `aggregate.field`, received a string, and drew no number. - -**What an author now sees.** `os validate`, `os build` and `os lint` fail on the page, and the save door refuses it when the host has a component manifest, with the existing message, for example ` prop "aggregate" expected an object`. To fix the page, write the value in the type the input declares: braces with JSON for an object (`aggregate={{"function":"count"}}`), braces for a number or a boolean (`limit={50}`, `invert={true}`), and braces with an array for an array (`fields={["name","amount"]}`). A string-typed input still takes a quoted value. diff --git a/.changeset/21672-install-local-pull-refusal.md b/.changeset/21672-install-local-pull-refusal.md deleted file mode 100644 index 5a0a7bd80b3..00000000000 --- a/.changeset/21672-install-local-pull-refusal.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/runtime': minor -'@objectstack/cloud-connection': minor ---- - -fix(runtime,cloud-connection)!: install-local refuses an enabled job whose `pull` does not bind, as it refuses a job `body` that does not bind (#21672) - -Clause-②: yes (narrowing) - - - -**BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package whose enabled job declares a `pull` that does not bind. It used to install such a package with a 200, and the job was never scheduled; only a server warn said so. - -- **What does not bind.** The `pull` names a mapping the package does not declare, or a mapping with no `connectorSource`, or the job declares `body` or `handler` beside its `pull`. The door judges this with the scheduler's own judgement, so the door and the scheduler cannot disagree. `defineStack` and `os validate` already refuse the same `pull`, so only a hand-edited package reaches the door with one. -- **The refusal.** The install answers `422` with `VALIDATION_ERROR`, the answer the door already gives an enabled job whose `body` does not bind. One answer names everything the door cannot run, and gives each such job the reason its `pull` does not bind, prefixed with the key it names (`pull.mapping: …`). Nothing is installed: nothing is registered, persisted or scheduled. `os package install` exits non-zero and prints the code beside the status. -- **Unchanged.** A pull job naming a declared mapping with a `connectorSource` installs and is scheduled as before. A disabled pull job does not block its install. A package installed by an earlier version still rehydrates after a restart, and its pull job that does not bind is not scheduled, with a warn naming the job and the reason, as before. -- **Runtime.** `collectJobsWithoutBody` now names an enabled job whose `pull` does not bind, and `JobWithoutBody` gains an optional `pullRefusal`: the reason the scheduler gives when it does not schedule the job. Such a job carries no `bodyRefusal`. - -The route for a refused package: declare the mapping the job's `pull` names in the package, with a `connectorSource` naming the `rest` or `openapi` connector it reads from, or correct the `pull` as the refusal says. `os validate` refuses the same `pull`. This ships as `minor`, under the launch-window convention for narrowings of an accept set. diff --git a/.changeset/21682-dropped-fields-platform-stamp.md b/.changeset/21682-dropped-fields-platform-stamp.md deleted file mode 100644 index 2be7cd2b664..00000000000 --- a/.changeset/21682-dropped-fields-platform-stamp.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -`droppedFields` on a create names only keys the caller sent, never a value a write middleware filled in - -Clause-②: no - -With `@objectstack/organizations` mounted (the walled tenancy postures), a non-system create that names no `organization_id` has that column filled with the caller's active organization by the organizations write middleware. `ObjectQL.insert` took its record of what the caller sent after that middleware had run. So the static `readonly` strip treated the fill as a caller write, took it, and reported it: - -- Before: `POST /api/v1/data/` with a body that names no `organization_id` answered 201 with `droppedFields: [{ object, fields: ['organization_id'], reason: 'readonly' }]`. `onFieldsDropped` fired with the same event. The console showed it as a warning toast on every such create. -- After: the same create answers 201 with no `droppedFields`, and `onFieldsDropped` does not fire. The row is stored in the active organization, as before. - -`insert` now records which keys each row carries before any write middleware runs. Only those keys count as sent by the caller. No field is exempted by name, so this covers every write middleware fill, including the `owner_id` fill of `@objectstack/plugin-security`. The referential-integrity check reads the same record, so it no longer checks a middleware-filled reference as if the caller had sent it. - -Unchanged: - -- A key the caller does send is judged and reported as before. That includes `organization_id` itself, and a key whose value a middleware rewrote. -- The array insert (`createMany`) and the `single` posture already reported nothing for an absent `organization_id`, and they still do. -- The stored row is the same. Before, the filled column was stripped and then filled again from the same active organization further down. Now the fill is kept. - -No export, type, error code or status changes. diff --git a/.changeset/21689-hook-no-body-save-door.md b/.changeset/21689-hook-no-body-save-door.md deleted file mode 100644 index d9a3d766b7a..00000000000 --- a/.changeset/21689-hook-no-body-save-door.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -The runtime save door refuses every hook that carries no `body`, including one with neither a `body` nor a `handler`: a hook stored there ships with no code package, so its `body` is the only code it can run - -Clause-②: no (narrowing) - - - -**BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier refusals shipped with. - -**One rule.** `saveMetaItem`, which `PUT /api/v1/meta/hook/:name` and the dispatcher's metadata save both call, now refuses every `hook` that carries no `body`. It already refused a hook whose `handler` names a function and that carries no `body`; that refusal is now one case of this rule, with the same envelope and the same message. The new case is a hook with neither field (or with an empty `handler`). Before this change the door answered 200 for it, the hook was served by name, the runtime skipped it at every re-sync (`skipping hook with unresolved handler`, in the server log only), and it never ran. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or bound. It names the hook and prescribes a `body`. - -**Before and after** (with `{ name: 'stamp_status', object: 'crm_note', events: ['beforeInsert'] }`): - -- Before: 200 `Saved hook 'stamp_status'`, the row stored and served by name, and the hook never run. -- After: 400 `VALIDATION_ERROR`, naming `stamp_status`, and nothing stored. - -**What still saves.** A hook with a `body`, with or without a `handler` beside it: the binder runs the body and never consults the name. A malformed `body` still gets the type schema's located `422 INVALID_METADATA`. - -**What is unchanged.** `HookSchema` still accepts a hook with no `body`, because a build artifact carries a `handler` hook: `objectstack build` lowers an inline function to the hook's name and ships the function in the artifact's runtime module. A hook in an artifact or a `defineStack` config binds on its own door, which never reaches this one. `os validate` and `os build` are unchanged. - -**Rows stored before this change.** They keep their bytes, nothing re-saves them, and the runtime skips them at every re-sync as before. A new save of one, a re-save included, is refused until it carries a `body`. Package duplication reports such a row as failed with this refusal; `migrate meta --stored` leaves it as it is. Delete stays open. - -**The fix.** Give the hook a `body`: sandboxed JS (`{ language: 'js', source }`) or an expression (`{ language: 'expression', source }`). A hook that must run a package's own function belongs in that package's code, where its `handler` resolves. diff --git a/.changeset/21694-lock-door-every-topology.md b/.changeset/21694-lock-door-every-topology.md deleted file mode 100644 index 956eb8ac605..00000000000 --- a/.changeset/21694-lock-door-every-topology.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol)!: the ADR-0010 `_lock` gate refuses on a host-config kernel too, and the diagnostics `locked` count reads the item envelope's derivation (#21694) - -Clause-②: no (narrowing) - - - -**BREAKING**: a `/meta` write that a host-config kernel used to accept can now be refused. A host-config kernel is one with no `environmentId`: the CLI's lightweight assembler boots one for a stack whose `plugins` are instantiated, which is how the showcase app runs. On such a kernel the item-level `_lock` gate never ran, so `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` performed writes on items whose read envelope said `editable: false` or `deletable: false`. The gate now runs on every kernel, and answers the same `403 ITEM_LOCKED` an environment-bound kernel always gave. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. - -**What is refused now, on a host-config kernel.** A save, publish or rollback of an item whose effective `_lock` is `no-overlay` or `full`, and a delete of an item whose effective `_lock` is `no-delete` or `full`. The effective `_lock` is the packaged artifact's when one declares it, otherwise the stored row's. Each refusal writes its `denied` row to `sys_metadata_audit`, as on an environment kernel. The gate's own `sys_metadata` read fails closed there too: when it fails for any reason other than the table not being provisioned yet, the write is answered `503 SERVICE_UNAVAILABLE` before anything is written, where a delete used to reach the store and answer with the driver's code or a `500`. One shipped case reaches it: the platform's `setup`, `studio` and `account` apps declare `protection.lock: 'full'`, and a `DELETE /api/v1/meta/app/setup` used to pass the package door (removing a legacy app overlay is allowed) and then remove the app's overlay row, or answer success with nothing to remove. It now answers `403 ITEM_LOCKED`. The refusal names the lock and where it came from (`source=artifact` or `source=overlay`). If a host-config deployment relied on writing such an item: a lock a code package declares is changed in that package's source (`protection.lock`) and redeployed; a lock a stored row declares is held exactly as an environment-bound kernel holds it, so a row declaring `no-overlay` can still be deleted and saved again, and a row declaring `full` is no longer writable or removable through `/meta` on any kernel. - -**Unchanged.** Which code a packaged base answers: the `_lock` gate still ranks below the package door on both kernels, so a packaged item on a type with no overlay channel keeps `NOT_OVERRIDABLE` (or `ITEM_LOCKED` when the write names the read-only package) on a host-config kernel too. Every refusal, receipt and envelope on an environment-bound kernel. Both reads (`getMetaItem`, `getMetaItemLayered`), which already reported the declared `_lock` on every kernel. - -**The diagnostics count.** `getMetaDiagnostics().stats[type].locked` (the Studio directory's per-type tile) counted items with a declared `_lock`. It now counts items whose read envelope reports a lock other than `'none'`, from the same derivation the item read publishes. So an item that the package door refuses in place, such as a packaged flow or action with no `_lock` of its own, is counted, as its envelope has read locked since the previous release. The derivation reads the registry only, so the sweep makes no extra store read per item. diff --git a/.changeset/21702-joined-report-block-dataset-required.md b/.changeset/21702-joined-report-block-dataset-required.md deleted file mode 100644 index e0269e1c82d..00000000000 --- a/.changeset/21702-joined-report-block-dataset-required.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -Every block of a `joined` report must bind a `dataset`: a block with none is refused at `blocks[i].dataset`, by name, with the prescription to bind the block to a dataset. - -Clause-②: yes (narrowing) - - - -**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. - -**Why.** A `joined` report carries its data on `blocks`, each an independent query over that block's own `dataset`, and the container selects nothing. `ReportSchema`'s refinement comment and the reports guide both said each block is dataset-bound, but the joined arm only required `blocks` to be non-empty, and a block's `dataset` is optional on its shape. So a block with no `dataset` parsed, `objectstack validate` exited 0 on it, the metadata save door stored it, and the report drew nothing for it: the joined renderer issues no query for an unbound block and draws it as an empty table, a report whose blocks all lack one falls through to the pre-9.0 presentation bridge, which issues no query either, and a dashboard drill-down that opens that report lists the records instead of drawing it. - -**What is refused.** On a report whose `type` is `joined`, each block with no `dataset`. The issue's `code` is `custom`, at `blocks.N.dataset`, one per unbound block, and its message names the block: *a `joined` report draws each block from that block's own `dataset`, and block `NAME` binds none, so nothing queries it and it draws no rows. Bind the block to a dataset: set its `dataset` to the dataset whose measures (`values`) and dimensions (`rows`) it shows.* It is an arm of `ReportSchema`'s own refinement, so it reaches `defineReport`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `reports.N.blocks.M.dataset`), `os validate` / `os build`, and the metadata save door (`422 INVALID_METADATA`). A stored report row is not rewritten: it carries the same issue in its read-side `_diagnostics` and is refused on its next save. - -**What stays accepted, byte for byte.** A `joined` report whose blocks all bind a `dataset`; every non-joined report, including one that carries `blocks` (they are read on a `joined` report only); and `JoinedReportBlockSchema` parsed on its own, where `dataset` stays optional. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| a `joined` report block with no `dataset`, e.g. `{ name: 'open_block', label: 'Open' }` | the same block bound to the dataset it shows: `{ name: 'open_block', label: 'Open', dataset: 'task_metrics', rows: ['status'], values: ['task_count'] }` | -| a `joined` report whose blocks all bind a `dataset`, or any non-joined report | unchanged | - -**The one-line fix: set each block's `dataset` to the dataset whose measures it shows, or delete a block that has nothing to show (a `joined` report keeps at least one block).** The renderer never drew an unbound block, so binding it is the first time it draws anything. - -**Who is affected, measured.** No joined report with an unbound block exists in this repository's `examples/**`, `packages/**`, `skills/**` or `content/docs/**`: the showcase's one joined report, the reports guide's example and every test fixture bind each block, apart from one metadata-door test fixture that left its block unbound on purpose and is bound in this change. The hotcrm application's one joined report binds every block, and the cloud repository has no joined report. Deployed metadata was not measured. Studio's report inspector can still produce one: its `blocks` repeater adds a blank row and requires no column of it, so a block saved with only a name is now refused at save, at `blocks.N.dataset`, where it used to be stored and draw nothing. - -### The kit - -- **The refusal.** A per-block arm of the joined branch of `ReportSchema`'s refinement (`ui/report.zod.ts`), beside the container refusals for `dataset` / `rows` / `columns` / `values`, `order` and `chart`. The block's `dataset` description now says a joined report refuses a block without one, and the generated reference page carries it. -- **The ledger.** The D3 semantic entry `ui-report-joined-block-dataset-required` (protocol 18) and its step-18 rationale fragment. No key is removed, so there is no tombstone, and there is no D2 conversion: which dataset a block shows is the author's decision, and no rewrite can name it. diff --git a/.changeset/21703-job-body-describe-pull.md b/.changeset/21703-job-body-describe-pull.md deleted file mode 100644 index 7d5c268ea87..00000000000 --- a/.changeset/21703-job-body-describe-pull.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -`JobSchema.body`'s description now says an enabled `pull` job installs through `os package install` when its `pull` binds and is refused when it does not - -Clause-②: no - -The description said `os package install` refuses an enabled job with no `body`, "a `pull` job excepted: it is data too". Read plainly, that says the install never refuses a `pull` job. That stopped being true when the install-local door (`os package install`, `POST /api/v1/marketplace/install-local`) began refusing an enabled job whose `pull` does not bind, with the same `422 VALIDATION_ERROR` it gives a job whose `body` the declaration refuses. - -The sentence now reads: a `pull` is data too, so an enabled `pull` job is judged by its `pull` instead. It installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. The generated reference page for `job` carries the same text. - -Text only: no key, schema shape, condition, error code or status moves. A tool or test that matches the old sentence needs the new one. diff --git a/.changeset/21714-report-form-joined-block-dataset-picker.md b/.changeset/21714-report-form-joined-block-dataset-picker.md deleted file mode 100644 index a1e18766ed5..00000000000 --- a/.changeset/21714-report-form-joined-block-dataset-picker.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -`reportForm`: the "Joined blocks" repeater's `dataset` column now declares `widget: 'ref:dataset'` and `required: true`. Studio's report inspector draws a joined report's block `dataset` as the dataset picker, with the required marker, instead of a free-text cell with no marker. - -Clause-②: no - -- **Why.** A `joined` report refuses a block that binds no `dataset`, at `blocks.N.dataset`. The form offered that column as plain text and did not mark it, so a newly added block failed on save. -- **Which renderer honours it.** The pinned console registers `ref:dataset` in its widget registry. Its repeater takes each column's widget and `required` from the row spec, in both the grid and the card layout. -- ⛔ Only this one form row changes. No schema, parse, export or accept-set change: `JoinedReportBlockSchema.dataset` stays optional on the block shape, and the joined arm's refusal is unchanged. diff --git a/.changeset/21716-lock-org-axis-agree.md b/.changeset/21716-lock-org-axis-agree.md deleted file mode 100644 index 835c60a360a..00000000000 --- a/.changeset/21716-lock-org-axis-agree.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -fix(metadata-protocol)!: the ADR-0010 `_lock` gate reads the row the read serves for the request's organization, so an env-wide row's lock binds an organization with no row of its own (#21716) - -Clause-②: no (narrowing) - - - -**BREAKING**: an organization-scoped `/meta` write that used to be accepted can now be refused. The item reads (`getMetaItem`, `getMetaItemLayered`) serve an organization its own stored row, and the env-wide row when it has none (ADR-0005). The item-level `_lock` gate read only the organization's own row. So when the env-wide row declared a lock, an organization with no row of its own read `lock: "full"` and `editable: false`, while its `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` were admitted. The gate now reads the row the reads serve, through the same resolution, and answers `403 ITEM_LOCKED` where the envelope says the write is not allowed. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. - -**What is refused now.** On every kernel topology, a save, publish or rollback with an organization of an item whose env-wide stored row declares `_lock: "no-overlay"` or `"full"`, and a delete with an organization of one whose env-wide row declares `"no-delete"` or `"full"`, when that organization has no stored row of its own for the item. Each refusal writes its `denied` row to `sys_metadata_audit` under the requesting organization. Over the wire this reaches the five per-org overridable types (`view`, `dashboard`, `report`, `translation`, `email_template`): the REST and dispatcher write doors already send no organization for any other type. For those other types the reads never serve an organization-scoped row, and now neither does the gate: an in-process removal of a pre-#6190 organization-scoped row of such a type (`deletePackage`, `discardPackageDrafts`) is judged by the env-wide row's lock, the row both reads serve. If a deployment relied on writing such an item per organization: change or remove the env-wide row's lock (a `no-delete` row can still be saved, a `no-overlay` row deleted), or keep the lock and author the organization's variant under a new name. - -**Unchanged.** When the organization has a stored row of its own, that row is the one both reads serve, and its `_lock` decides, whatever the env-wide row declares (ADR-0005 precedence, never a merge). A request with no organization. The packaged artifact's lock, which still wins when it declares one. Both reads, apart from one case in `getMetaItemLayered`: it now serves an organization's own stored row whose body is JSON `null`, as `getMetaItem` already did, instead of falling back to the env-wide row, because the two reads now share one row resolution. Only residue can reach it: no live writer stores a `null` body (measured: `SysMetadataRepository.put`, the writer behind every `/meta` save, stores `{}` for an absent body; `saveMetaItem` refuses a `null` item with `400 INVALID_REQUEST`; and the only other `sys_metadata` writer, the datasource admin plugin, stores an object env-wide). The gate addresses the canonical type spelling only; the reads' last-resort read of a row stored under the type's other spelling is not extended to the write path. diff --git a/.changeset/21723-object-schema-mask-references.md b/.changeset/21723-object-schema-mask-references.md deleted file mode 100644 index bbf0704e240..00000000000 --- a/.changeset/21723-object-schema-mask-references.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/metadata-core': minor ---- - -The object-schema field mask (ADR-0106 D1) now removes a denied field's references from the rest of the served object document, not only its `fields` entry. - -Clause-②: no - -A caller who cannot read a field was served a document without that field's definition, but other parts of the document could still name the field. Those parts are now projected too, on every exit that serves an object schema to a restricted caller: the by-name read, the list read and the layered view. What happens to each kind of position: - -- **Object-level rule entries** (`validations`, `indexes`, `activityMilestones`). An entry that names or reads a denied field is dropped whole. The platform still evaluates the stored rule on every write. -- **Role pointers** (`nameField`, `displayNameField`, `imageField`, `stageField`, `tenancy.tenantField`, `lifecycle.ttl.field`). A pointer to a denied field is deleted, so the role falls back to its default. -- **Name lists** (`highlightFields`, `searchableFields`, `publicSharing.redactFields`, list-view column lists, `external.columnMap` entries, and a readable field's `relatedListColumns` / `dependsOn`). The denied entries are filtered out. A list-view column in object form is dropped when any of its facets names a denied field: its own `field`, its `prefix.field` or its `summary.field`. A `dependsOn` entry's `param` is the lookup target's key and is not read as a field here. A list left empty is deleted. -- **The inline master-detail grid** (`inlineColumns`, `inlineAmountField`). These are declared on the child object's own `master_detail` field and name the child's own fields. A column that is a denied field, or is computed from one (`expr`), is dropped. A denied `inlineAmountField` is deleted. -- **Expressions** (`titleFormat`, field-group `visibleWhen`, row-CRUD `visibleWhen` / `disabledWhen`, `publicSharing.eligibility`, lifecycle `onlyWhen`, and a readable field's formula `expression`, `visibleWhen` / `readonlyWhen` / `requiredWhen`, `relatedListFilter`, `defaultValue`, `autonumberFormat` and per-option `visibleWhen`). An expression that reads a denied field is deleted. The readable field itself stays. -- **List views and actions.** An entry whose filter, sort, predicate, params or `patch` read a denied field is dropped. So is a list view whose key is a denied field's name. - -In these positions an object key counts as a field reference only where keys are field names: a `FilterCondition`, a lifecycle `onlyWhen` map, an action's `patch`. A denied field named like a schema word (`type`, `source`, `name`) no longer removes every rule, view, action or CEL envelope. A dotted path whose root segment is a denied field counts as a reference to it, whether it is a field-keyed key, a name-list entry or a pointer. The mask also terminates on a cyclic document. - -Names of another object's fields (`lookupColumns`, `displayField`, `summaryOperations`, …) are left alone, because that object's own projection governs them. A key that is not classified is deleted when it mentions a denied field in any string or key, so a new key over-masks until it is classified. A test holds the classification equal to the live `ObjectSchema`, `FieldSchema`, `InlineGridColumnSchema`, `ListColumnSchema`, `ColumnPrefixSchema` and `ColumnSummaryConfigSchema` key sets. A caller who is denied nothing still gets the same document reference, so nothing changes for unrestricted callers. - -The shared contract fixture in `@objectstack/metadata-core/testing` (`FLS_CONTRACT_OBJECT`) now names its fields in each of these positions, including list views (one with object-form columns), actions and the inline grid, and its formula field uses the real `expression` key. The contract's residue check matches a denied name as an identifier token anywhere in the served document, so a name inside an expression fails it. Projection cases also check what must survive (`retained`), so a mask that deletes too much fails as well. diff --git a/.changeset/21724-resume-refusal-codes.md b/.changeset/21724-resume-refusal-codes.md deleted file mode 100644 index 5f5472caae3..00000000000 --- a/.changeset/21724-resume-refusal-codes.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/runtime': minor ---- - -A refused flow resume answers the engine's own code, on the REST resume door and the MCP `resume_run` tool alike, as the 17.1.0 release notes, `client.automation.resume()`'s documentation and the flows guide already state (#21724). - -Clause-②: yes (widening) - -- **What moves on the wire.** `POST /api/v1/automation/:name/runs/:runId/resume` used to hand the error builder a status and no code for the engine's refusals, so `error.code` was derived from the status. It now carries the engine's code: - - | Refusal | Status | `error.code` before | `error.code` now | - |---|---|---|---| - | a screen input that breaks the screen's declared fields | 400 | `VALIDATION_ERROR` | `INVALID_SCREEN_INPUT` | - | a signal that writes an engine-reserved `$` name | 400 | `VALIDATION_ERROR` | `INVALID_SIGNAL` | - | an unknown run, a run whose flow is gone, or a run whose paused node was edited away | 404 | `RESOURCE_NOT_FOUND` | `RUN_NOT_FOUND` | - | the suspended-run store is unreadable | 503 | `SERVICE_UNAVAILABLE` | `STORE_UNAVAILABLE` | - | another resume already holds the run | 409 | `RESOURCE_CONFLICT` | `RESUME_IN_PROGRESS` | - - `PERMISSION_DENIED` (403) is unchanged. No status moves, nothing that was accepted is refused, and a refused resume still leaves the run paused, so a corrected resume still completes it. A caller that branched on the status-derived code reads the documented one instead: `err.code` from `client.automation.resume()` is now `INVALID_SCREEN_INPUT`, `INVALID_SIGNAL` or `RUN_NOT_FOUND`, which is what lets it tell a bad screen value from a reserved signal name. -- **`@objectstack/spec` — `minor`.** The ADR-0112 error-code ledger registers `INVALID_SCREEN_INPUT` under `@objectstack/service-automation`, beside `INVALID_SIGNAL` and `RUN_NOT_FOUND`. The engine already returned it and the docs already promised it, but `ErrorCode`, and therefore `ApiErrorSchema`, refused it. The published vocabulary gains one member and loses none. -- **`@objectstack/runtime` — `minor`.** The resume door's answer set gains the five codes above. Both doors read one classifier, so the REST route and `resume_run` answer the same code for the same engine result. diff --git a/.changeset/21725-flow-metadata-save-arms-flow.md b/.changeset/21725-flow-metadata-save-arms-flow.md deleted file mode 100644 index 5e59dac57f5..00000000000 --- a/.changeset/21725-flow-metadata-save-arms-flow.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@objectstack/service-automation': patch ---- - -A flow saved through the metadata API is armed on the running engine at once, as hooks and actions saved through the same door already are - -Clause-②: no - -`PUT /api/v1/meta/flow/:name` answered `200 "Saved flow … (env-wide, state=active)"` and `GET /api/v1/meta/flow/:name` served the row, but the automation engine registered nothing until the process restarted: `GET /api/v1/automation/:name` and `POST /api/v1/automation/:name/trigger` answered `404 Flow not found`, and a record-triggered flow never fired. The engine armed flows only at boot, at `kernel:ready` and on `metadata:reloaded`, and only the publish doors announce that event. - -The automation service now listens to the metadata protocol's post-write signal (`onMetadataMutation`), the one ObjectQL already re-binds authored hooks and actions on, and makes the engine follow the stored row of each flow it names: - -- an active save or a publish registers the flow, or re-registers it over the definition the engine held; -- a save whose `status` is `'obsolete'` or `'invalid'` keeps it registered and unbound, as a boot does; -- a delete unregisters it; -- a draft save changes nothing until it is published. - -The flow is re-read through the same execution view, precedence and env-wide scope the boot reads, so a save never arms a flow beyond the reach a restart would give it. The save answers first, and the registration follows it by one read of the stored metadata. - -A publish raises this signal and `metadata:reloaded` together, and the published flow is still registered once, not twice. `PUT /api/v1/automation/:name`, which registers the flow before it saves it, is not registered a second time either. - -A run already executing keeps the definition it started with. A suspended run resumes against the definition registered when it resumes, and answers `RUN_NOT_FOUND` once its flow is deleted. Both already held for a re-registration through a publish or `PUT /api/v1/automation/:name`. diff --git a/.changeset/21726-explain-decided-by-depth.md b/.changeset/21726-explain-decided-by-depth.md deleted file mode 100644 index 381b29da7f0..00000000000 --- a/.changeset/21726-explain-decided-by-depth.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -`POST /api/v1/security/explain` with `{ object, operation: 'read', recordId }` now credits read depth for a row that only the caller's read depth admits. - -Clause-②: no - -- **Before.** The object's OWD is private. The caller's read depth is wider than `own` (`own_and_reports`, `unit`, `unit_and_below` or `org`). The caller reads a row they do not own and hold no share on. Explain answered `decidedBy: 'sharing'` for that row, with the sharing layer `admitted` and the detail "0 share(s) attached; access is granted for this record." No share granted anything; the read depth did. `depth` is a member of the published `decidedBy` enum, and no answer ever produced it. -- **Now.** That row answers `decidedBy: 'depth'`. The `depth` layer carries a `record` block: `admitted`, naming the depth and, below `org`, the owner it reached. The sharing layer is `not_evaluated`, and its detail says that no share grants the row and that read depth already admits it. -- **The sharing detail names a grant only when one exists**, meaning a share that names the caller. A filter that admits a row with no such share now says which part of the sharing service let the row through. -- **A row a share admits keeps `decidedBy: 'sharing'`** and its "N share(s) attached; access is granted for this record." detail, also when the read depth admits it too. Of the two layers that admit it, sharing comes last in the pipeline, and the `decidedBy` contract names the last layer to admit. -- **No access decision changes.** `visible` is the same for every record; only the layer the report credits moves. No key is added. `depth` and `not_evaluated` are existing values. -- **The `depth` layer's `record` block is new on every record-grained request.** On a write it is `not_evaluated`, because the write depth is judged inside the sharing service's per-record write gate together with ownership and shares, and this report does not separate them. diff --git a/.changeset/21727-protocol-incompatible-422.md b/.changeset/21727-protocol-incompatible-422.md deleted file mode 100644 index b51169234b5..00000000000 --- a/.changeset/21727-protocol-incompatible-422.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/metadata-core': minor -'@objectstack/runtime': minor -'@objectstack/spec': minor ---- - -A package install refused by the ADR-0087 D1 protocol handshake now answers `422 OS_PROTOCOL_INCOMPATIBLE`, with its structured diagnostic in `error.details`. It used to answer the `500` server-fault fallback, with the diagnostic only inside the message. - -Clause-②: yes - -- **`POST /api/v1/packages`:** a manifest whose declared range (`engines.protocol`, then `engines.platform`, then `engine.objectstack`) excludes this runtime's protocol major answers `422`, with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'` and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. `error.message` is unchanged, and the command after its `Run:` equals `migrateCommand`. Nothing is installed, so a later `GET /api/v1/packages/{id}` still answers `404`. -- **The no-protocol-service fallback:** in a composition without a `protocol` service, `POST /api/v1/packages` used to install such a manifest. It now runs the same handshake and answers the same `422`. -- **`@objectstack/metadata-core`:** `ProtocolIncompatibleError` declares `status` and `statusCode` `422`, with `code` as the literal `'OS_PROTOCOL_INCOMPATIBLE'`. It also carries a `Symbol.for` brand, and the new `isProtocolIncompatibleError(e)` recognises it across module instances, where `instanceof` would not. Any other caller that resolves the error (the boot-time `AppPlugin` load included) reads `422` rather than the `500` fallback. -- **`@objectstack/spec`:** the error-code ledger's `OS_PROTOCOL_INCOMPATIBLE` row states its status (422) and the door that carries the diagnostic. The vocabulary does not change. - -A client that branched on `500` for this code should branch on `422`, or on `error.code`. None was found in this repository, the SDK or the console. - - diff --git a/.changeset/21728-install-local-purge.md b/.changeset/21728-install-local-purge.md deleted file mode 100644 index 18a90cc6f18..00000000000 --- a/.changeset/21728-install-local-purge.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cloud-connection": patch ---- - -`POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data` now deletes an installed package's sample rows. Before, it answered `500 DRIVER_UNAVAILABLE` on every runtime. - -Clause-②: no - -- **What was wrong.** The purge looked up a bare `driver` service, a name no kernel registers (drivers register as `driver.`), so it refused everywhere. Behind that it matched seed records by `id`, which seed records rarely carry: the CRM example's 28 records key by `name`, `email` and `subject`. It also deleted through the driver, past every engine hook. -- **What it does now.** It deletes through the ObjectQL engine, so lifecycle hooks and the audit trail run, under the posture the seed was written with (record-change automation suppressed). Rows are matched by each dataset's `externalId`, the key the install and the reseed upsert by. A row whose key no seed record declares is never touched. Children are deleted before parents, in the reverse of the seed loader's own dependency order. -- **Scope.** Under an organization wall the purge removes only the seed rows of the caller's active organization, the scope the install and the reseed seed into. A session with no active organization is refused with `403 PERMISSION_DENIED` and a message naming the missing active organization, the way reseed refuses it. Without a wall the deployment is one tenant, and the match is table-wide, as the install's own match is. -- **The response keeps its shape**, `{ manifestId, deleted, skipped, errors, withSampleData }`. `skipped` counts seed records no row carries (already deleted). `errors` counts records that could not be purged, each with its reason in the server log: a delete the engine refused (for example, a user's row still requires the seed row as its parent), a key that more than one row carries, or a seed record with no key value. -- A runtime with no data engine or no metadata service still answers `500 DRIVER_UNAVAILABLE`, now naming what is missing. diff --git a/.changeset/21729-attachment-parent-editor-delete.md b/.changeset/21729-attachment-parent-editor-delete.md deleted file mode 100644 index 5992c455775..00000000000 --- a/.changeset/21729-attachment-parent-editor-delete.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/service-storage': minor -'@objectstack/plugin-security': minor ---- - -A user who can edit a record may delete another user's attachment on it, as the attachment gate declares (#21729). - -Clause-②: yes (widening) - -- **What was refused.** The attachment gate's delete rule is "the uploader OR a user who can edit the parent record". For every member holding `org_member`, the platform's row-level delete floor in `member_default` (`owner_only_deletes`: only the rows you created) answered first, so a parent editor's delete of someone else's attachment was refused with `PERMISSION_DENIED` before the gate ran. -- **`@objectstack/service-storage`** contributes a delete-only alternate match for `sys_attachment` (`sys_attachment_parent_editor_delete`, every row) when it installs the attachment gate, and only then. The gate decides: a parent editor's delete answers 200, and a caller who can read the attachment but neither uploaded it nor can edit the parent is refused with `ATTACHMENT_DELETE_DENIED`. A caller who cannot read the parent cannot see the attachment, and is still refused with `PERMISSION_DENIED` before the gate runs, so the parent is not named to them. -- **Without `@objectstack/service-storage`** nothing is contributed. A deployment that registers `sys_attachment` without the storage service keeps the floor, and only a row's creator may delete it. -- **The edit limb is unchanged.** Editing another user's attachment row is still refused by the floor for a member it binds, a parent editor included. -- **`@objectstack/plugin-security`** gains the seam: `contributeOwnershipFloorAlternates(plugin, alternates)` on the registered `security` service, an extension of `ISecurityService` that callers feature-detect. Each alternate names one object (never `'*'`), one floor limb (`update` or `delete`; `all` is refused) and a `using` predicate. It lands beside each enabled floor policy of that limb, in that policy's own `positions` domain, so it reaches only the principals the floor binds. A plugin's second call replaces its first, and an empty list withdraws it. A contribution that breaks these rules throws. - -Nothing that was admitted before is refused now. No principal outside the floor's domain, and no other object or operation, changes. diff --git a/.changeset/21730-file-constraint-400.md b/.changeset/21730-file-constraint-400.md deleted file mode 100644 index 259410974dd..00000000000 --- a/.changeset/21730-file-constraint-400.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/service-storage': patch ---- - -A file field's declared `accept` / `maxSize` refusal now answers `400 ERR_FILE_CONSTRAINT` with a sentence naming the field and the constraint, instead of `500 INTERNAL_ERROR` with the sentence withheld. - -Clause-②: no - -- **`FileConstraintError` declares `status = 400`**, as `FileFieldBulkWriteError` in the same module already did. The data API's declared-status passthrough now answers the refusal on create and on update, e.g. `400 {"error":"File exceeds the maximum size declared for 'doc' (5005 bytes > 10 bytes)","code":"ERR_FILE_CONSTRAINT","object":"…"}`. Before, the error declared a registered `code` but no status, so it fell through to the sanitised `500 INTERNAL_ERROR`, and the field and the reason reached only the server log. -- **It carries `field` and `constraint` (`'accept' | 'maxSize'`) as members**, for in-process callers. The constructor is now `new FileConstraintError(field, constraint, message)`. The new `FileConstraint` type is exported beside it. On the HTTP wire the message names both, and its wording is unchanged. -- The accept set is unchanged: the same files are refused, and a refused write still persists no row and claims no file. `ERR_FILE_CONSTRAINT` was already in the error-code ledger. diff --git a/.changeset/21731-totp-issuer-app-name.md b/.changeset/21731-totp-issuer-app-name.md deleted file mode 100644 index 43952846c7a..00000000000 --- a/.changeset/21731-totp-issuer-app-name.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/cli': patch ---- - -A TOTP enrollment names the deployment, not the auth library. `/two-factor/enable` and `/two-factor/get-totp-uri` answered an otpauth URI whose issuer and label prefix were `Better Auth`, so every authenticator app listed the account under that name. They now carry the deployment's app name: `OS_APP_NAME`, else the configured `appName`, else `ObjectStack`. An explicitly set `branding.workspace_name` setting still outranks it. - -Clause-②: no - -- **Existing enrollments keep working.** The issuer is a display label. The stored enrollment holds only the encrypted secret, the backup codes and the confirmation flag, and the codes depend only on the secret, digits and period. An authenticator app enrolled under `Better Auth` keeps producing codes that verify. It keeps its old label until the user re-enrolls. -- **`@objectstack/plugin-auth`.** `AuthManager` passes its app name to better-auth as `appName`. In better-auth 1.7.3 that key names only these two otpauth URIs. No cookie name or stored value derives from it. -- **`@objectstack/cli`.** `objectstack serve` now passes the deployment app name to `AuthPlugin`. It is resolved by the same chain the email service's template context uses: `OS_APP_NAME` > `config.email.appName` > `config.email.defaultTemplateContext.appName` > `config.appName` > `ObjectStack`. Before, `serve` built `AuthPlugin` with no app name, so auth answered `ObjectStack` whatever `OS_APP_NAME` said. Auth emails were affected too: under `serve` they now name the deployment the way every other email already did. -- The issuer is read when the auth instance is built. A `branding.workspace_name` change made after that reaches new enrollments at the next restart or auth-settings change, while auth emails pick it up on their next send. diff --git a/.changeset/21732-migrate-plan-requires-providers.md b/.changeset/21732-migrate-plan-requires-providers.md deleted file mode 100644 index 7919b2a4bb4..00000000000 --- a/.changeset/21732-migrate-plan-requires-providers.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/cli': patch ---- - -`os migrate plan` and `os migrate apply` boot a config whose connector plugins depend on a service that only `requires` supplies (#21732). Before this fix, both commands exited 1 on a fresh `create-objectstack -t blank` app and on `examples/app-showcase` with `[Kernel] Dependency 'com.objectstack.service-automation' not found for plugin 'com.objectstack.connector.rest'`. - -Clause-②: no - -- **Why it failed.** The connectors (`@objectstack/connector-rest`, `-openapi`, `-mcp`, `-slack`) declare a hard dependency on the automation service. The blank template and the showcase ask for automation only through `requires: ['automation', …]`. `os serve` turns that token into the provider, but the schema-migration composition read `config.plugins` and never read `requires`. -- **What it composes now.** It uses the same token lookup `os serve` uses (`Serve.CAPABILITY_PROVIDERS`, with exact identity matching, and an explicit instance in `plugins` still wins). It composes a provider only when a plugin it already composed hard-depends on that provider and the config's `requires` (or the always-on slate) supplies it. -- **Automation is taken inert** (`armRuntime: false`). The engine and node registry come up. No flow is registered, no trigger or job is bound, no connector is materialized and no suspended run is resumed. Its `init()` declares `sys_automation_run`, `sys_flow_dispatch` and `sys_flow_credential`, so the plan now covers the tables `os serve` creates for this capability. -- **A provider with no measured declaration posture is refused by name.** The refusal names the plugin, its dependency and the token, instead of booting that provider's `start()` inside a dry run. A dependency that no token supplies is still refused by the kernel, as `os serve` refuses it. -- A config that lists no plugin with such a dependency composes exactly what it did before. diff --git a/.changeset/21733-standalone-dev-self-heal.md b/.changeset/21733-standalone-dev-self-heal.md deleted file mode 100644 index 4e57121cd22..00000000000 --- a/.changeset/21733-standalone-dev-self-heal.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/runtime": minor -"@objectstack/cli": patch ---- - -fix(runtime,cli): a plain `os dev` now self-heals safe schema drift on restart and provisions the `telemetry` sibling database, as `content/docs/deployment/cli.mdx` already says (#21733) - -Clause-②: yes (widening) - -- **What was broken.** A config with no instantiated `plugins[]` (every fresh scaffold) boots through the standalone stack. Its `default` datasource was built without `autoMigrate: 'safe'`: only the config-load fallback that a host config or `OS_MODE=off` takes carried it. So safe drift was never applied on restart. An example is a per-organization unique index that an older release left non-NULL-safe. Meanwhile the driver's drift line and `os migrate plan` both said the change was "auto-applied at boot under dev autoMigrate: 'safe'". The same boot never provisioned the `.telemetry.` sibling either. -- **The fix.** The dev self-heal decision now lives in one place, `devAutoMigrateConfig` in `@objectstack/runtime`. That is the driver kinds whose connection contract declares `autoMigrate` (sqlite, postgres, mysql), on a dev boot. The standalone stack, the CLI's config-load fallback and the telemetry sibling all read it, so no kind gains or loses the self-heal relative to the host path. The telemetry provision is one helper (`provisionTelemetryDatasource`) that both serving paths call, under the same `resolveTelemetryDbPath` rule: dev default-on for a file-backed SQLite primary, `OS_TELEMETRY_DB=0` to opt out, `OS_TELEMETRY_DB=` to opt in anywhere. -- **Only a serving boot self-heals.** The standalone stack arms the self-heal on an explicit `dev: true`. That is what `os dev` passes. It does not arm it on the `NODE_ENV=development` default that its sqlite step-down still takes. A one-shot command (`os migrate *`, `os meta resync`, …) passes no `dev`, so it never applies drift its operator did not confirm, whatever `NODE_ENV` says. Production boots are unchanged: the definition carries no `autoMigrate`, and the SQL driver refuses it under `NODE_ENV=production` anyway. -- **Why minor.** `@objectstack/runtime` gains two exports on its only entry, `devAutoMigrateConfig` and its `DevAutoMigrateConfig` type. That is the widening: the existing decision moved out of the CLI so that the CLI reads it rather than keep a second copy. No config key, schema or accept set moves. `@objectstack/cli` is a `patch`: its fix restores documented behaviour and adds no public surface. diff --git a/.changeset/21734-driver-sql-auto-vacuum-read-first.md b/.changeset/21734-driver-sql-auto-vacuum-read-first.md deleted file mode 100644 index df5eb4ad040..00000000000 --- a/.changeset/21734-driver-sql-auto-vacuum-read-first.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/driver-sql": patch ---- - -A connect to a SQLite file that is already `auto_vacuum=INCREMENTAL` no longer writes to it. `SqlDriver.connect()` now reads `PRAGMA auto_vacuum` first and runs `PRAGMA auto_vacuum = INCREMENTAL` only when the file answers something else. - -Clause-②: no - -- **What changed on disk.** On a file that already answered INCREMENTAL, the setter changed no mode, but it still stamped two header counters: the file change counter (bytes 24–27) and the version-valid-for number (bytes 92–95). So a read-only command such as `os migrate duplicates`, which the CLI docs say writes nothing at all, changed the file's md5 on every run. Reading the pragma changes no bytes, so such a file now comes through a connect, and a whole `os migrate duplicates` run, byte-identical. -- **Unchanged.** A fresh file and `:memory:` still come out INCREMENTAL. A legacy NONE file that already holds tables still gets the setter, which leaves it NONE until a `VACUUM` (`os db clean`), exactly as before. The journal-mode step already read first and set only on a difference, and it is untouched. Postgres and MySQL issue no PRAGMA. -- **The WASM SQLite driver** inherits the rule. A connect and disconnect no longer rewrites an already-INCREMENTAL image file, because the setter was what marked the image dirty. -- ⛔ No config key, export, error code or accepted input changes. diff --git a/.changeset/21738-lock-one-resolution.md b/.changeset/21738-lock-one-resolution.md deleted file mode 100644 index ace77360214..00000000000 --- a/.changeset/21738-lock-one-resolution.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix(metadata-protocol): the item reads take an item's ADR-0010 `_lock` from the same resolution the write doors enforce, so the read envelope and the doors agree on the artifact layer too (#21738) - -Clause-②: no - -The `_lock` gate of the write doors (save, publish, rollback, delete) resolves an item's lock from two layers in order: the packaged artifact's `_lock`, unless it is `'none'`, then the stored `sys_metadata` row's. The two item reads did not use that resolution. `getMetaItem` took the lock from its served document, onto which `mergeArtifactProtection` had copied any declared artifact `_lock`, `'none'` included. `getMetaItemLayered` (`GET /api/v1/meta/:type/:name/layers`) took it from the code layer whenever one existed. Now the gate, both reads' envelopes, the served body's lock fields and the `getMetaDiagnostics` per-type `locked` count all call one resolution, `resolveItemLock`. - -**Read answers that change**, on both kernel topologies: - -- An artifact that declares `_lock: 'none'` over a stored row that declares a lock (env-wide, or the organization's own row) now reads with the stored row's lock in `getMetaItem` and `getMetaItemLayered`. Under `'full'`, that is `editable: false` and `deletable: false`. The served body's `_lock`, the `getMetaItems` list item's `_lock` and the diagnostics `locked` count say the same. The doors already refused these writes with `403 ITEM_LOCKED`. An artifact's `'none'` declares no lock; it does not override an administrator's stored lock. -- `getMetaItemLayered` for a packaged item whose artifact declares no `_lock`, under a stored row that declares one, now reads the stored row's lock, as `getMetaItem` and the doors already did. Before, it read `lock: 'none'`, `editable: true`. -- `lockReason`, `lockSource` and `lockDocsUrl` are the binding layer's. When no layer binds, they are absent: a reason explains a refusal, and there is none. Before, they were whatever the served document carried, so an explicit `'none'` artifact's reason was reported. For the same reason, an explicit `'none'` artifact's `_lock`, `_lockReason`, `_lockSource` and `_lockDocsUrl` are no longer copied onto a stored row's served body. -- A `_lock` that only a copy the doors never read declares is no longer reported as binding. Examples are a MetadataService copy the dev watcher reloaded after boot, and an item registered at runtime with no package. Such an item now reads as the doors answer it. - -**Unchanged.** Every write-door verdict, refusal code and refusal text: the gate still reads the stored row only when the artifact's lock does not bind, so a packaged lock is still answered without a store read. `provenance`, `packageId` and `packageVersion` on both reads, and the artifact's `_packageId`, `_packageVersion` and `_provenance` on served bodies. Which stored row each read serves. The one declared difference between the reads and the gate also stays: the reads still serve a row stored under the type's other (plural) spelling when no canonical row is in scope, and the gate does not read it. No export, accepted input, key or error code changes. diff --git a/.changeset/21755-attachment-refusal-not-visible.md b/.changeset/21755-attachment-refusal-not-visible.md deleted file mode 100644 index 90a78e84272..00000000000 --- a/.changeset/21755-attachment-refusal-not-visible.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/service-storage': patch -'@objectstack/plugin-audit': patch ---- - -A write refusal on an attachment or a comment no longer names a parent record the caller cannot read (#21755). - -Clause-②: no - -- **What changed.** The attachment gate (`sys_attachment`, `@objectstack/service-storage`) and the comment gate (`sys_comment`, `@objectstack/plugin-audit`) refuse an update or a delete by a caller who neither wrote the row nor can edit its parent record. That refusal names the parent record. A caller who cannot read the parent now gets the platform's not-visible refusal instead. This is the answer the row-level write check gives the principals it covers: `PERMISSION_DENIED` (403), with the same localized `record_access_denied` sentence. It names neither the parent nor the row's link to it, in the message or in the envelope. -- **What did not change.** A caller who can read the parent but may not edit it keeps the named refusal: `ATTACHMENT_DELETE_DENIED` for an attachment delete, and `RECORD_NOT_ACCESSIBLE` for an attachment update and for a comment update or delete. Who may update or delete is unchanged. -- **A comment whose thread names no record** is read by nobody, so a non-author's write on it now gets the not-visible refusal too, and the thread value is not echoed back. -- **Localization.** `installAttachmentAccessHooks` and `installCommentAccessHooks` accept an optional fourth argument: a lazily resolved i18n lookup. With it, the sentence honours a deployment's `errors.record_access_denied` override, as the row-level write check's sentence does. Without it, the built-in catalog still renders the caller's locale. diff --git a/.changeset/21756-security-service-declared-members.md b/.changeset/21756-security-service-declared-members.md deleted file mode 100644 index 34ffc298f26..00000000000 --- a/.changeset/21756-security-service-declared-members.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -`ISecurityService` declares the two members the registered `security` service already served without a declaration: `discardPermissionSetOverlay` and `contributeOwnershipFloorAlternates`. Both are optional, and callers feature-detect them. - -Clause-②: yes (widening) - -- **`discardPermissionSetOverlay(callerContext, id)`** (`@objectstack/spec/contracts`). The audited operator action behind a permission set's "Discard Overlay" Setup action: it deletes the stale environment overlay that shadows a package-declared permission set, then re-projects the row from the declared artifact before it resolves. Its docblock names the refusals it throws and the codes they carry: `PERMISSION_DENIED` (403) when the caller is not a tenant-level administrator or no installed package declares the set, `NOT_FOUND` (404) for an unknown row, and `INVALID_STATE` (409) when there is no active overlay to discard. It resolves with the new `PermissionSetOverlayDiscardResult` type. The REST route answers `501 NOT_IMPLEMENTED` when the method is absent. -- **`contributeOwnershipFloorAlternates(plugin, alternates)`**. The seam through which a plugin that installs a tighter row gate stops the platform's `created_by` write floor pre-empting that gate on one object and one limb. Its docblock names what it refuses (a wildcard object, an operation other than exactly `update` or `delete`, a missing or malformed policy), and says a second call replaces the same plugin's first and an empty list withdraws it. The new `OwnershipFloorAlternate` type is the minimal contract shape of one alternate. -- **Optional, and absence is typed.** A security service without either member still satisfies the contract, and an unguarded call does not compile. - -Nothing an author writes changes. An implementation typed as `ISecurityService` that serves either name must now serve it under the declared signature; `@objectstack/plugin-security` already does, and needs no change. diff --git a/.changeset/21761-lock-address-selection.md b/.changeset/21761-lock-address-selection.md deleted file mode 100644 index 23662fadd96..00000000000 --- a/.changeset/21761-lock-address-selection.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@objectstack/metadata-protocol": minor ---- - -fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address, at the write doors and on both reads (#21761) - -Clause-②: no (narrowing) - -ADR-0048 lets one item (type, name, organization scope) hold several stored `sys_metadata` rows: one per package (`?package=` saves) and a package-less one. The ADR-0010 `_lock` gate asked for the item with no package and bound whichever row the store returned first, while `getMetaItem` and `getMetaItemLayered` naming a package reported that package's row. So a read and the door could state two different locks for one item, and the door's answer depended on row order. - -Now every caller selects the lock from the item's address through one function: the strictest lock among the item's stored rows in scope. The scope is ADR-0005's (the organization's rows when it holds any row of the item, else the env-wide rows), and within it every row of the item counts, whichever package it is bound to. The strictest lock refuses a write when any of those rows refuses it, and a delete likewise; two rows that refuse different verbs (`no-overlay` and `no-delete`) give `full`. Both reads report that lock in `lock`, `editable` and `deletable`, the served body carries its `_lock` family, and the list item and the `getMetaDiagnostics` locked count follow it. Content stays prefer-local: a read naming a package is still served that package's own row. - -**What moves for consumers.** The door now refuses where it used to depend on which row the store returned first: - -- a package-less row declaring no lock and a package's row declaring `full`: a save or delete, with or without `?package=`, is refused `403 ITEM_LOCKED` in every row order, where it was admitted when the package-less row came back first; -- a package's row declaring `none` and a package-less row declaring `full`: refused in every row order, where it was admitted when the package's row came back first. A package row's explicit `none` is not a grant over another row's lock; -- two rows refusing different verbs (`no-overlay`, `no-delete`): both a save and a delete are refused, where each was admitted under the row order that bound the other row; -- another package's row of the same name declaring a lock binds a save naming this package too, as it did under the row order that returned it first. - -No write the door refused before is admitted now. Both reads become stricter in exactly those arrangements: a read naming a package whose own row declares `none` now reports `editable: false` when another row in scope declares `full`. No key, export, status or error code changes. - - diff --git a/.changeset/21762-install-local-protocol-handshake.md b/.changeset/21762-install-local-protocol-handshake.md deleted file mode 100644 index 575e751fca5..00000000000 --- a/.changeset/21762-install-local-protocol-handshake.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/metadata-core': minor -'@objectstack/cloud-connection': patch -'@objectstack/runtime': patch ---- - -`POST /api/v1/marketplace/install-local` now runs the ADR-0087 D1 protocol handshake. A manifest whose declared range excludes this runtime's protocol major is refused with `422 OS_PROTOCOL_INCOMPATIBLE`, the answer `POST /api/v1/packages` already gives. It used to install with a `200` (#21762). - -Clause-②: yes (widening) - -- **Install.** The handshake runs after the manifest id is parsed and before anything is registered, written or synced. The range is read from `engines.protocol`, then `engines.platform`, then `engine.objectstack`. The refusal answers `422` with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'`, the handshake's own `error.message`, and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. It is the same on the inline-manifest branch and the cloud-snapshot branch. No ledger file is written, and an installed earlier version stays as it was. A manifest with no range, or a range the handshake cannot read, still installs, and the handshake's warning goes to the plugin's logger. -- **Restart.** On `kernel:ready`, a ledger entry whose range excludes this runtime's major is not loaded. Nothing is registered, synced, bound or seeded for it. One `error` line names the package, `OS_PROTOCOL_INCOMPATIBLE` and the replay command (`objectstack migrate meta --from N`). The boot continues with the other entries. The entry stays in the ledger, so `DELETE /api/v1/marketplace/install-local/{id}` still removes it, and installing a compatible version replaces it. Before, it was registered and its schemas synced, with no warning. -- **`@objectstack/metadata-core`:** a new export, `protocolIncompatibleAnswer(err)`, with its return type `ProtocolIncompatibleAnswer`. It turns a `ProtocolIncompatibleError` into the status, code, message and five-member `details` an HTTP door answers. Both install doors call it, so their answers are the same bytes. -- **`@objectstack/runtime`:** `POST /api/v1/packages` answers through that helper. Its response is unchanged. - -A client that relied on install-local accepting a package built for another protocol major gets `422` now. Install a version built for this runtime's protocol, or migrate the package with the `migrateCommand` in the refusal. diff --git a/.changeset/21765-object-image-field-form-row.md b/.changeset/21765-object-image-field-form-row.md deleted file mode 100644 index a3f2f50331d..00000000000 --- a/.changeset/21765-object-image-field-form-row.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/platform-objects': patch ---- - -Studio's object form offers `imageField`, the record's picture, as a text row beside `nameField` - -Clause-②: no - -The object form in the metadata form registry now has an `imageField` row, a plain text input placed beside `nameField`. Until now the only way to set the record picture from Studio was the Source tab's raw JSON. The help text says what the parse accepts: a field of this object whose type is `image` or `avatar`. Left empty, the object has no record picture and no placeholder is drawn. The row brings no picker and no validator of its own. A name that is not an `image` / `avatar` field of the object is refused when the object is saved, by the same parse rule as before. - -`@objectstack/platform-objects` ships the row's label and help text in its metadata-form translation catalogs, translated for `zh-CN`, `ja-JP` and `es-ES`. - -No schema key, accept set, refusal, error code or status changes, and you have nothing to re-author. diff --git a/.changeset/21765-object-image-field-live.md b/.changeset/21765-object-image-field-live.md deleted file mode 100644 index 04d01db63e2..00000000000 --- a/.changeset/21765-object-image-field-live.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -`ObjectSchema.imageField`'s description no longer says the renderer is pending: the record page header draws the picture - -Clause-②: no - -The console's record page header now reads `imageField`: it draws the named `image` / `avatar` field's value in the record chip beside the title, an `avatar` round and cropped, an `image` whole, and a record whose field is empty shows no picture. The `.describe()` text that `os validate`, the JSON Schema and the reference docs carry dropped its last sentence, "Pending renderer: the record chrome does not draw it yet.", and now says the header draws the picture rather than is to draw it. - -Text only: no key, schema shape, refusal, error code or status moves. An `imageField` you already authored takes effect as it is, with nothing to re-author. diff --git a/.changeset/21768-object-form-runtime-field-grid-camelcase-keys.md b/.changeset/21768-object-form-runtime-field-grid-camelcase-keys.md deleted file mode 100644 index a29806a2497..00000000000 --- a/.changeset/21768-object-form-runtime-field-grid-camelcase-keys.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): an inline `object-form` field declares the `grid` widget's eight camelCase field-level keys, and each snake_case spelling is refused naming its camelCase key (#21768) - -Clause-②: yes (widening) - -A widening of a published authoring surface: every value that parsed before still parses, and eight keys that were refused now parse. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`. - -**`@objectstack/spec`** - -- **The runtime form field takes the `grid` widget's field-level keys.** An `object-form` `customFields` member, and the inline entry of an `object-form` or `object-master-detail-form` section's `fields`, now declare `minRows` and `maxRows` (numbers), `allowAdd`, `allowDelete` and `allowReorder` (booleans, on unless `false`), and `totalField`, `addLabel` and `sortField` (strings). These are the value types objectui's `GridFieldMetadata` declares. The `grid` widget reads each one off a `type: 'grid'` field: `minRows` stops Remove, `maxRows` stops Add, Duplicate and the blank entry row, `addLabel` labels the Add button, and `sortField` names the row field the grid stamps with each row's index, so a drag-reorder is saved. objectui renamed the eight from snake_case to camelCase, with no dual read, and the `.objectui-sha` pin `9dfaca654311` carries that rename. -- **`totalField` here is the CHILD column the grid sums into its footer.** On `record:line_items` and on an `object-master-detail-form` detail entry, the same spelling names the PARENT field the sum is saved to, and their child column is `amountField`. The describe states the difference. The other blocks' keys are unchanged. -- **The snake_case spellings are still refused, and each refusal now names its own replacement.** `min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label` and `sort_field` are each answered with "Rename the key to `minRows`" (and so on); the value stays the same. The old answer said the keys would come in once the widget read a camelCase spelling, and the widget now does. Two retired spellings on one field get one line each. -- **`record:line_items`' `sortField` refusal** now says no block takes an authored `sortField` *for child records*. An inline `grid` field takes one for the rows of its own value, so the unqualified sentence was no longer true. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `customFields: [{ name: 'items', type: 'grid', min_rows: 1, allow_add: false }]` | `customFields: [{ name: 'items', type: 'grid', minRows: 1, allowAdd: false }]` | -| `total_field: 'amount'` on an inline grid field | `totalField: 'amount'`, naming the child column summed | -| `add_label: 'Add line'`, `sort_field: 'position'` | `addLabel: 'Add line'`, `sortField: 'position'` | - -The one-line fix: rename each key to its camelCase spelling, keeping its value. Nothing that parsed before is refused, so no ADR-0087 conversion or D3 entry is owed. - -## Who is affected, measured - -- **objectstack** at `75ddcd1b41` (this branch's base): no writer of either spelling on an inline form field in `examples/`, `skills/`, `content/docs/` or `apps/`. The spec's own pin is the only one: its `min_rows` refusal probe. -- **objectui** at the pin `9dfaca654311`: the camelCase writer is the schema catalog's `fields-grid/line-items-grid` example, a `grid` field carrying all eight keys, and it parses. No production source reads a snake_case spelling. The only snake_case occurrences left are objectui's own refusal faces: the TS tombstones, the zod alias refusals, and the widget's refusal. -- **hotcrm**, **cloud** and deployed metadata were not measured. diff --git a/.changeset/21771-write-door-unreadable-is-not-found.md b/.changeset/21771-write-door-unreadable-is-not-found.md deleted file mode 100644 index 6c3cd615fd5..00000000000 --- a/.changeset/21771-write-door-unreadable-is-not-found.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/plugin-security': minor -"@objectstack/spec": patch ---- - -fix(plugin-security)!: on the write doors, a row the caller cannot read answers what a nonexistent id answers - -Clause-②: no (narrowing) - - - -**BREAKING**: a by-id update or delete of a row the caller cannot read now answers `404 RECORD_NOT_FOUND`, with exactly the body an id that names no row gets, for every principal class. On the write doors, "hidden" and "gone" are now one answer to a caller who cannot read the row. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new. - -**What changed.** The answer used to depend on which gate saw the row first. Where a write-class row filter binds the caller, the by-id write pre-image check answered `403 PERMISSION_DENIED`. Where none binds it, a later gate answered with its own 403: `FORBIDDEN` from record sharing, or a parent-derived gate's code on attachments and comments. Meanwhile a nonexistent id answered `404`. So the write door could tell a hidden row apart from a missing one. The pre-image check now asks the read door's own question first, for the by-id write the caller addressed: a by-id read in the caller's context, every data middleware's visibility included. A row that read does not return gets the read door's not-found producer. A store fault propagates as raised, and a read-time policy refusal is not treated as absence. - -**What is refused now that was not.** A principal that no write-class row filter binds could have its by-id write admitted on a row the read door hides from it. One example is the uploader of an attachment, or the author of a comment, whose parent record they can no longer read. That write is now refused with the not-found answer, as it already was for every principal a row filter binds. - -**FROM → TO.** A by-id update or delete of a row hidden from the caller: FROM a `403` (`PERMISSION_DENIED`, `FORBIDDEN`, or a parent-derived gate's code) → TO `404 RECORD_NOT_FOUND`, the body a nonexistent id gets. - -**If you are affected.** A client that read a by-id write's `403` as "the row exists, but you may not change it" should read `404 RECORD_NOT_FOUND` the way the read door means it: no row you can see has this id. - -**Unchanged.** -- A caller who can read the row but may not write it keeps its 403. They already see the row. -- By-id writes the platform issues under the caller's context keep their previous answer, because the caller never named their target: the engine's cascade delete of a dependent row, a hook's write, and the referential clear of a lookup. -- Writes that are not routed by id are unchanged. - -`security/explain` follows enforcement. Its record verdict for an update or delete of a record the principal cannot read is now the missing-record shape: `visible: false`, with no decider. diff --git a/.changeset/21774-install-local-no-active-org.md b/.changeset/21774-install-local-no-active-org.md deleted file mode 100644 index 24b52b59d30..00000000000 --- a/.changeset/21774-install-local-no-active-org.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/cloud-connection": patch ---- - -Under an organization wall, the install-local sample-data doors now refuse a session with no active organization, and the refusal names what is missing (ADR-0123 D2 / D4). Before, they skipped quietly. - -Clause-②: no - -- **Reseed and purge.** `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data` and `…/purge-sample-data` answer `403 PERMISSION_DENIED`, with a message saying the session has no active organization and that one must be joined or selected. Before, the reseed answered `400 RESEED_SKIPPED` (`multi-tenant-no-active-org`); the purge, which starts deleting in this same release, refuses the same way from the start. Reseed's other declines are unchanged and still answer `400 RESEED_SKIPPED`: a package with no seed datasets, a runtime with no data engine or metadata service, and a seed run that threw. -- **Install.** `POST /api/v1/marketplace/install-local` still installs the package, which is environment-wide. Its `seeded` block now reads `{ mode: "refused", reason: "…" }`, where `reason` names the missing active organization and says to select one and then reseed. Before, it read `{ mode: "skipped", reason: "multi-tenant-no-active-org" }`. -- **No organization is guessed.** The active-organization read no longer falls back to the user's first membership. ADR-0123 D1 makes "authenticated, with no active organization" a declared state. A guess would write into an organization the caller never chose. The fallback read an object no package defines, so it never resolved anything. -- **Unchanged.** A session with an active organization seeds, reseeds and purges in that organization, as before. Without a wall (`single` posture), no organization is read, and the three doors act table-wide. diff --git a/.changeset/21775-install-local-listing-per-org-sample-data.md b/.changeset/21775-install-local-listing-per-org-sample-data.md deleted file mode 100644 index 92b5ff0de9d..00000000000 --- a/.changeset/21775-install-local-listing-per-org-sample-data.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cloud-connection": patch ---- - -`GET /api/v1/marketplace/install-local` now answers each entry's `withSampleData` for the caller's own organization. Before, after a purge in organization A, the listing read as organization B answered `withSampleData: false` while B still held every one of its seed rows. - -Clause-②: no - -- **What was wrong.** The listing served the install ledger's `withSampleData`, one value per install. Under an organization wall, sample data is per organization: the install, the reseed and the purge each act in the caller's active organization. A purge in A flipped the one value for every organization, and a restart kept it. -- **What it does now.** The listing reads the rows. An entry answers `true` when at least one of the package's seed rows is in the caller's scope. Rows are matched the way the purge matches them, by each dataset's `externalId`. "At least one" is exactly when the purge has something to delete, and it decides whether the console labels its reseed action "Add sample data" or "Reseed again". The purge's matching is now a separate read-only step, and the purge deletes what it returns, with the same counts and log lines as before. -- **Scope.** Under a wall, the scope is the caller's active organization. A session with no active organization reads nothing, so every entry answers `false` with `200`, and no row is read. Without a wall, the match covers the whole table, and the answer is the one the ledger records after an install, a purge and a reseed. -- **When the rows cannot be read** (for example, a package the runtime did not load), the entry answers `false`, and the server log says why at `warn`, once per entry per request. -- **The response keeps its shape.** The ledger keeps its shape too. Its `withSampleData` and `sampleDataPurged` stay as install-time records, and their docs now say they are not per organization. diff --git a/.changeset/21776-reseed-intact-baseline.md b/.changeset/21776-reseed-intact-baseline.md deleted file mode 100644 index 3ed3bbb7d7e..00000000000 --- a/.changeset/21776-reseed-intact-baseline.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/cloud-connection": minor ---- - -An install-local reseed over sample rows that are all still in place answers success, with the loader's `skipped` count, instead of a refusal naming a false cause (#21776). - -Clause-②: yes (widening) - -- **Intact baseline.** `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`, run while every seed record the package declares is already present, answers `200 { success: true, data: { manifestId, inserted: 0, updated: 0, skipped: N, errors: 0, withSampleData: true } }`. Before, it answered `422 RESEED_NO_ROWS`, "Reseed wrote no rows. The package declares no seedable records for this runtime.", over a package that declares them. The reseed is idempotent, so a run that finds every row in place has reached its goal. The install's record of sample data is set the same way as when rows land. -- **`skipped` on every success.** A successful reseed now answers all four of the loader's counts: `inserted`, `updated`, `skipped` and `errors`. Before, `skipped` was not in the response. -- **Unchanged refusals.** `422 RESEED_NO_ROWS` still answers a run that wrote nothing because records failed, with the error count and the first error, and its `details` are still `{ inserted, updated, errors }`. It also still answers, with the same text, a run in which the loader had no record to process for this runtime: for example, every dataset is scoped to another environment (`Seed.env`). That text now states a true cause. A package with no seed dataset at all still answers `400 RESEED_SKIPPED` (`no-datasets`). diff --git a/.changeset/21777-sync-schemas-federated-object.md b/.changeset/21777-sync-schemas-federated-object.md deleted file mode 100644 index c4028ee4ae7..00000000000 --- a/.changeset/21777-sync-schemas-federated-object.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -A runtime schema sync (`ObjectQL.syncSchemas()`) no longer sends DDL to an object with `external` set. That sync runs on an install-local install, a rehydrate and template seeding. It also no longer logs the durability ERROR "Schema sync FAILED … not durable" for such an object. The ERROR still fires for any other object whose sync fails. - -Clause-②: no - -- **What was wrong.** A federated object (ADR-0015) lives on a datasource whose schema the remote database owns. The boot sync never sends it DDL. It binds the object to its remote table with the driver's DDL-free `registerExternalObject`. The runtime sync had no such branch, so it called `syncSchema` on every federated object in the registry. On an external-schema datasource the driver refuses that DDL, as designed. The refusal was then logged as a durability failure, although nothing durable was lost. A showcase-based host printed two false ERROR lines on every install-local install, one each for `showcase_ext_customer` and `showcase_ext_order`. A false alarm on every run teaches operators to skip the one line that, for any other object, means its data is not on disk. -- **What it does now.** `syncSchemas()` treats a federated object exactly as the boot sync does. It binds the object without DDL and logs a `debug` line. If the driver has no `registerExternalObject`, it skips the object at `debug`. A binding that throws is logged at `warn`. It never calls `syncSchema` for the object. The binding matters for an object registered at runtime: without it, every read resolves to a table named after the object instead of the remote table, and fails with "no such table". -- **One predicate.** The boot sync, `syncSchemas()` and `syncObjectSchema()` now ask one shared predicate, `external != null`, so the runtime and boot syncs cannot drift apart again. The predicate reads the object's own `external` block, not its datasource's `schemaMode`. An object without `external` that lands on an external-schema datasource still gets the ERROR when its DDL is refused, because it expected a table it did not get. -- No accepted input, key, export, status or error code changes. diff --git a/.changeset/21785-email-template-overlay-survives-boot.md b/.changeset/21785-email-template-overlay-survives-boot.md deleted file mode 100644 index 2f499999aec..00000000000 --- a/.changeset/21785-email-template-overlay-survives-boot.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/plugin-email": patch ---- - -An email template edited through `PUT /api/v1/meta/email_template/:name` (the Studio editor's door) now keeps the admin's wording in `sys_email_template` across a restart. Before, the boot sweep wrote the package wording back over the sending row while `GET /meta` kept serving the admin's, so mail went out with the package wording after every boot. - -Clause-②: no - -- The cause: on a deployment with a Default Organization the admin's save is an org-scoped overlay, and boot hydration keeps org-scoped overlays out of the registry the sweep read. An env-wide overlay was already kept. -- `EmailServicePlugin`'s boot sweep now projects the effective template: the layered list `protocol.getMetaItems` serves, read in the organization `tenancy.defaultOrgId()` names. That is the Default Organization under the `single` posture. A host without a `protocol` service reads the registry as before. -- A failed effective read projects nothing for that boot, so the rows keep their last projection. It does not fall back to the package wording. -- Seed-not-clobber is unchanged. A row an admin created (`managed_by: 'admin'`) or edited through the data API (`customized: true`) is still never overwritten. -- The published API is unchanged. The exported `bootstrapDeclaredEmailTemplates` keeps its signature and still reads the registry, so a caller outside the plugin sees the same behaviour as before. diff --git a/.changeset/21787-write-response-credential-mask.md b/.changeset/21787-write-response-credential-mask.md deleted file mode 100644 index 27c03ab3d68..00000000000 --- a/.changeset/21787-write-response-credential-mask.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/core': patch -'@objectstack/runtime': patch ---- - -Credential-class field values are now masked on every write response, as on reads. - -Clause-②: no - -- A `secret` field, and a `password` field on an object that is not `managedBy: 'better-auth'`, already read back as `SECRET_MASK` (`null` when unset) on the generic read path (ADR-0100). Every write response that returns a record (REST, batch and MCP) now answers the same way. -- The shared write-response helper every write door already calls (`omitInternalFieldsFromWriteResponse`, `@objectstack/core`) now applies the credential mask before it omits `internal: true` fields. New exports beside it: `maskCredentialFieldsInWriteResponse` and `collectCredentialWriteResponseFields`, which read the same `isMaskedOnReadFieldType` declaration as the engine's read mask. -- `callData`'s fallback create and update arms (`@objectstack/runtime`, used when no protocol service is registered) now pass their response record through the same helper. -- Unchanged: the engine's own write results still return the stored row whole to privileged server-side callers, and the echoed-mask write guard still treats a `SECRET_MASK` value as "leave unchanged", so a client that saves back a write response does not overwrite the stored credential. diff --git a/.changeset/21788-external-import-saves-like-meta.md b/.changeset/21788-external-import-saves-like-meta.md deleted file mode 100644 index 9937a0395f0..00000000000 --- a/.changeset/21788-external-import-saves-like-meta.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/service-datasource': minor ---- - -fix(service-datasource)!: "Import as Object" saves the imported federated object through the metadata door's own save, so it is durable and reads from its remote table; federated validation stops reporting the platform's injected anchors as missing remote columns (#21788) - -**BREAKING** — the import route now answers `400` to some re-imports that used to answer `201`. - -Clause-②: no (narrowing) - -- **The import was neither durable nor mapped.** `POST /api/v1/datasources/:name/external/tables/:remote/import` held the generated object in the metadata service's memory only. No `sys_metadata` row was written, the object's storage was not synced, and the driver was never told the object's remote table. An object imported under a name that differs from its remote table answered `201` and then `500 DATABASE_ERROR` (`no such table: `) on its first read. Every import was gone after a restart (`404 OBJECT_NOT_FOUND`). -- **It now saves like `PUT /api/v1/meta/object/:name`.** The import calls `saveMetaItem` on the `protocol` service with the request that door sends for an `object`. The object becomes a `sys_metadata` row the next boot binds, it is written through to the engine registry, and it is mapped onto its `external.remoteName` table. An import under a different name and one under the remote table's own name both serve the remote rows, before and after a restart. The save door is looked up when an import runs. A deployment with no metadata save door still refuses the import with "requires a writable metadata store", before any remote introspection. -- **What narrows.** The metadata door's refusals now apply to the import, and the route relays each one as `400 EXTERNAL_IMPORT_ERROR` with the door's message. A re-import that would drop a field the stored object already has, or change its type, is refused as a destructive change. It used to answer `201` with an in-memory overwrite that a restart discarded. An import whose `name` collides with an object the metadata door will not overwrite is refused the same way. The route's request and response shapes are unchanged. -- **If a re-import or a name is refused:** import the table again under a new `name`. To change an object you already imported, save its new definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Re-submitting the import with `?force=true` does not help, because the import route reads no `force`. -- **Federated validation compares only what the remote owns.** A stored federated object is read back with the anchors the platform injects without storage (`organization_id`, `created_by`, `updated_by`, `owner_id`, `owning_business_unit_id`). The boot validation gate and `POST …/external/validate` reported each of them as a `missing_column` at error severity. So once a datasource with the default `external.validation.onMismatch: 'fail'` held such an object, it refused to boot. Validation now skips the columns `unprovisionedInjectedColumns` names. This closes the boot abort for objects saved through `PUT /api/v1/meta/object/:name`, not only for imports. A declared column the remote lacks is still a `missing_column` at error severity, and `fail` still aborts on it. - - diff --git a/.changeset/21789-lock-reads-row-provenance.md b/.changeset/21789-lock-reads-row-provenance.md deleted file mode 100644 index 1961f05f0d8..00000000000 --- a/.changeset/21789-lock-reads-row-provenance.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -A permission set an organization owns, a clone of a packaged set, and a set saved into a writable runtime package are no longer locked as if a code package shipped them - -Clause-②: no - -The packaged-permission-set lock decides "is this set shipped by a code package?" from the engine registry. The registry also holds the stored definition rows, and a metadata list read (`GET /api/v1/meta/permission`, which every Studio page load issues) stamps a stored row's package binding onto it. A set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=`) therefore looked code-shipped after the first list read, and every later edit of it answered `403 NOT_OVERRIDABLE` at both the metadata door and the data door. The lock now skips a stored row by its provenance (`_provenance: 'org'`, which every stored row carries), the same test the platform's code-artifact check applies, so those edits are accepted again. - -The read had the matching defect. The security plugin keeps a marked in-memory copy of each stored definition for the permission evaluator, and the layered read (`GET /api/v1/meta/permission/:name/layers`) serves that copy as the item's `code` layer. The copy carried no provenance, so an org's own set, a clone and a runtime-package set all reported a `code` layer with no `provenance`, which the console's permission-matrix editor renders as "locked by a code package" while the server accepted the save. The copy now carries `_provenance: 'org'` exactly when the lock judges the set not code-shipped, so the layered read reports `provenance: 'org'` for those sets. - -Unchanged: a set a code package ships is still refused at both doors with `403 NOT_OVERRIDABLE` and the same message naming the clone path, and its layered read still reports `provenance: 'package'`, its package id and `editable: false`. No error code, route or field moves. diff --git a/.changeset/21790-metadata-core-ordered-fields-hash.md b/.changeset/21790-metadata-core-ordered-fields-hash.md deleted file mode 100644 index 8c947c9b72d..00000000000 --- a/.changeset/21790-metadata-core-ordered-fields-hash.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/metadata-core': minor ---- - -The content hash keeps the order of an object's `fields`, so a pure field reorder is a new version instead of "no change" (#21790). - -Clause-②: yes - -- **`canonicalize(value, type?)` and `hashSpec(value, type?)`** take the metadata type. For a type whose body has a map the spec declares ordered, that map keeps its insertion order in the canonical form. Today that is one map: `object.fields`, whose traversal order is the field order the platform presents. Every other map stays key-order independent, including the keys around `fields` and the keys inside each field definition. Called without a type, both functions return exactly what they returned before. -- **`orderedMapKeys(type?)`** is a new export. It returns the top-level keys of a `type` body whose map keeps its order (`['fields']` for `object`, `[]` otherwise). -- `InMemoryRepository` and the repository contract suite hash as `ref.type`. Invariant 4 now reads `item.hash === hashSpec(item.body, item.ref.type)`. -- **Stored hashes.** An object whose `fields` are already in sorted key order hashes exactly as before. Any other object hashes differently from the hash stored before this release. A stored hash is still that row's version token: `@objectstack/metadata-protocol` keeps it as written and compares content to decide whether a save changed anything. - -`minor` because two exports widen: a new parameter and a new function. No metadata key, accepted value, wire payload or error code changes. diff --git a/.changeset/21790-metadata-fs-ordered-fields-hash.md b/.changeset/21790-metadata-fs-ordered-fields-hash.md deleted file mode 100644 index 564aa9f0248..00000000000 --- a/.changeset/21790-metadata-fs-ordered-fields-hash.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/metadata-fs': patch ---- - -`FileSystemRepository` hashes each item as its metadata type, so a reorder of an object's `fields` is a change (#21790). A reordered object is written, and an external edit that only reorders `fields` is reported as an update. - -Clause-②: yes - -Event-log entries written before this release carry the order-blind hash. For an object whose `fields` are not in sorted key order, `get()` and `list()` no longer find that entry until the item next changes. They fall back to the defaults: no parent hash, sequence `0`, the filesystem actor, and the epoch timestamp. The body and the version hash are unaffected. diff --git a/.changeset/21790-metadata-protocol-ordered-fields-hash.md b/.changeset/21790-metadata-protocol-ordered-fields-hash.md deleted file mode 100644 index 75412518d80..00000000000 --- a/.changeset/21790-metadata-protocol-ordered-fields-hash.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -Publishing a pure field reorder of an object now saves it. The object designer's drag-to-reorder used to answer success on publish, keep the old order and delete the draft (#21790). - -Clause-②: yes - -- `SysMetadataRepository` hashes each body as its type, so a reorder of an object's `fields` is a content change. It is written, recorded in history and served by `GET /api/v1/meta/object/:name`. -- **Rows stored before this release** keep the `checksum` they were written with. That value is still the version token: reads return it, `If-Match` tokens are derived from it, and the optimistic lock compares against it, so upgrading raises no conflict. -- For an object, "is this save a no-op?" is decided by hashing the stored body under the current rule, not by comparing against the stored checksum. An identical re-save of an old row writes no history row and keeps its checksum. A reorder into sorted key order is written too. Its new hash equals the old order-blind checksum, so a checksum comparison would have dropped it. -- A save that changes nothing returns the stored checksum as its version, so the receipt's token is the one the next `If-Match` save must send. diff --git a/.changeset/21791-membership-decided-at-creation.md b/.changeset/21791-membership-decided-at-creation.md deleted file mode 100644 index ed308049d92..00000000000 --- a/.changeset/21791-membership-decided-at-creation.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -'@objectstack/plugin-auth': minor -'@objectstack/organizations': patch -'@objectstack/types': patch ---- - -Membership under the `auto` policy is settled when the user is created, per ADR-0093 D7. - -Clause-②: yes (widening) - -- **At creation.** A user created under `auto` is bound to the default organization at creation, and the first session of that creating request carries it. Membership is not decided again when the user signs in later. -- **One-time backfill.** The ADR-0093 D6 backfill of pre-existing users runs once per deployment, and once per process even if its record cannot be written. Its verdict is recorded in the `sys_migration` ledger with id `adr-0093-membership-backfill`. A pass on a deployment with no organization at all records nothing, and the backfill runs again once the default organization is created. If the ledger is missing or cannot be read, the pass does not run and logs a warning. If the record cannot be written, that is logged as an error. `OS_SKIP_MEMBERSHIP_BACKFILL=1` still disables the pass. -- **Default organization owner.** The platform admin is bound as owner of the default organization once, by the bootstrap that first decides it, in both the single-org and the walled organizations wiring. The decision is recorded in the same ledger with id `adr-0093-default-org-owner-bind` and held for the rest of the process even if the record cannot be written. After that, a missing default organization is recreated without binding anyone. To recover, an administrator re-adds members, including themselves, through member management. On a kernel without the ledger, the owner is bound only when the bootstrap creates the default organization. If the ledger exists but cannot be read, that call binds nobody and the next trigger decides. -- **Full scan.** The backfill reads the user and membership tables page by page with no row cap. A scan that cannot read either table in full binds nobody and records nothing. With organizations present but no default target, as in multi-organization deployments, the refusal is recorded. -- **Upgrade.** The first boot of an upgraded deployment runs the backfill once. -- **Unchanged.** `invite-only` binds nobody. Multi-organization deployments get no automatic binding. Users created through sign-up, admin create-user, import or SSO are bound under `auto` as before. -- **Narrowed.** A `sys_user` row inserted straight through the data engine never passes through user creation. Once the backfill is recorded, a later `app:seeded` pass leaves it unbound. That includes users written by a seed that finishes after its inline budget. Code that inserts users this way must write their membership itself; the showcase approval-demo personas now do. -- **`keysetWalk` (`@objectstack/types`).** The walk now decides that a page did not advance only when it gets back the same cursor key or the same page again. It no longer compares keys in JavaScript string order, which disagrees with database collations and could report a healthy walk as truncated. -- **New public surface of `@objectstack/plugin-auth` (additive).** - - `createEnsureDefaultOrganizationOnce` and `EnsureDefaultOrganizationOnceOptions` are the gated bootstrap both wirings call. - - `ObjectQLAdapterFactoryOptions` adds `onRecordCreated`, passed as the new optional second argument of `createObjectQLAdapterFactory`. - - `EnsureDefaultOrganizationOptions` gains `bindOnlyOnCreate` and `bindOwner`. - - `EnsureDefaultOrganizationResult.reason` gains `'owner_bind_decided'`. - - `BackfillMembershipsResult.reason` gains `'scan-incomplete'`. - - Code that switches exhaustively over those reasons sees one more member. -- **`backfillMemberships` (exported) changed behaviour.** Its `limit` option used to cap the rows scanned (default 5000); it is now the page size of a full scan with no cap. The function now needs a reader that can page by `id`; a reader that cannot gets `scan-incomplete` and binds nobody, where it used to bind. A direct call is not gated by the one-time ledger and decides membership again on every call; call it through the one-time pass instead. -- **Policy switch.** Once a pass under `invite-only` is recorded, switching the policy to `auto` later does not backfill the users who existed then; they get membership through invitation or member management. -- **Deprecated, not removed.** The ungated `ensureDefaultOrganization`, both plugin-auth's helper and the `@objectstack/organizations` wrapper, is `@deprecated` in favour of `createEnsureDefaultOrganizationOnce`. diff --git a/.changeset/21792-settings-audit-secret-keyed-digest.md b/.changeset/21792-settings-audit-secret-keyed-digest.md deleted file mode 100644 index cbe252e5b6c..00000000000 --- a/.changeset/21792-settings-audit-secret-keyed-digest.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/service-settings': patch -'@objectstack/spec': patch ---- - -The settings audit trail records a secret-valued setting (an encrypted key) with the crypto provider's keyed digest, never an unkeyed one (#21792). - -Clause-②: no - -- **Both ledgers.** The `sys_audit_log` `config_change` row (`valueDigest`, spelled ``) and the `sys_setting_audit` row (`new_hash`) now carry `ICryptoProvider.keyedDigest(value)` for a secret-valued setting. Before, they carried the unkeyed `digest` (`sha256:…`). This covers the `sys_secret` path and the legacy inline-adapter path. -- **Change detection still works.** The keyed digest is stable for equal values under one key, so the trail still shows whether a secret changed and whether it went back to an earlier value. Rotating the data key changes every later fingerprint. Rows written before this release keep their old `sha256:` value. -- **No keyed digest, no fingerprint.** When no crypto provider is wired (a host that builds `SettingsService` with only a `CryptoAdapter`), or the provider refuses a keyed digest, the audit rows record the write with no value fingerprint: `valueDigest` is `` and `new_hash` is null. The service logs this once per key at `warn`. The settings write itself is never refused for it. The adapter's own `digest` is no longer used for secrets. -- **Non-secret settings are unchanged.** They keep the adapter's `digest` of the canonical JSON. -- **Contract text (`@objectstack/spec`).** The `ICryptoProvider` docs for `digest` and `keyedDigest` now state the rule: a secret's audit fingerprint comes from `keyedDigest`, never from `digest`, and with no keyed digest the trail records none. No type, export or schema changes. diff --git a/.changeset/21793-phone-otp-no-provider-4xx.md b/.changeset/21793-phone-otp-no-provider-4xx.md deleted file mode 100644 index 9b0d23f3667..00000000000 --- a/.changeset/21793-phone-otp-no-provider-4xx.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/plugin-auth': patch ---- - -Phone-number OTP with no deliverable SMS service now answers `400 SMS_SERVICE_REQUIRED` instead of a `500` with an empty body (#21793). - -Clause-②: yes (widening) - -- **`@objectstack/plugin-auth`.** `POST /api/v1/auth/phone-number/send-otp` on a deployment that turned phone sign-in on but has no SMS service that can deliver a code (none wired, or only the log transport in production) used to answer `500` with a `null` body: the send callback threw a plain `Error`, and better-auth's router turns anything but its own `APIError` into a bare 500. The login page had nothing to branch on and showed a generic failure. It now answers `400` with the body `{ "code": "SMS_SERVICE_REQUIRED", "message": "…" }`, a typed `APIError`, as the daily-quota branch of the same send already was. The message names the missing SMS delivery service and where an administrator configures it, and never carries the one-time code. `request-password-reset` is unchanged: it still answers `{ "status": true }` and sends nothing, so it reveals nothing about which numbers are registered. -- **`@objectstack/spec`.** `SMS_SERVICE_REQUIRED` is registered for `@objectstack/plugin-auth` in the ADR-0112 error-code ledger, beside its email sibling `EMAIL_SERVICE_REQUIRED`. `ErrorCode` (and so `ApiErrorSchema.code`) accepts one more value. Nothing that parsed before is refused now. - -**Action for clients.** A client that branched on the old `500` for this case should branch on `code === 'SMS_SERVICE_REQUIRED'` instead. The public config already advertises the capability as `features.phoneNumberOtp`, which stays `false` on such a deployment. diff --git a/.changeset/21794-lock-refusal-user-message.md b/.changeset/21794-lock-refusal-user-message.md deleted file mode 100644 index f1088750cc4..00000000000 --- a/.changeset/21794-lock-refusal-user-message.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-security': minor ---- - -The packaged-permission-set lock refusal carries its guidance as `userMessage`, so the console tells the admin to clone the set instead of showing its generic "You don't have permission to save this record." (#21794). - -Clause-②: yes (widening) - -- **`PackagedPermissionSetLockedError.userMessage`**, a new `readonly` member. A save that targets a permission set an installed package ships still answers `403 NOT_OVERRIDABLE` with the same `message`. That holds at the data door (`PATCH` / `POST /api/v1/data/sys_permission_set`) on every kernel. It also holds at the metadata door (`PUT /api/v1/meta/permission/:name`) on a kernel with no environment id, such as a self-hosted app server, where this lock is the refusal that answers. On an environment kernel the metadata protocol's own package-door refusal answers that `PUT` first, and it is unchanged. The error envelope now also carries `userMessage`, the field `ApiErrorSchema` already declares and the console renders verbatim. An edit is told to clone the set with the Clone action and edit the clone. A new set named like a packaged one is told to choose a different name, or clone. -- **`PackagedPermissionSetProvenanceUnknownError.userMessage`**, the same member on the fail-closed refusal (the platform could not tell whether a package ships the set). It tells the admin to try again, or clone. The unreadable source stays in `message`. -- The texts name no set, package, id or API path; `message` keeps that diagnostic for logs and developers. They are English, like every platform refusal. - -Nothing that was accepted is refused now, and nothing that was refused is accepted. Both refusals keep their `code`, `status` and `message`. diff --git a/.changeset/21795-platform-objects-org-admin-actions-by-grade.md b/.changeset/21795-platform-objects-org-admin-actions-by-grade.md deleted file mode 100644 index 1206f864394..00000000000 --- a/.changeset/21795-platform-objects-org-admin-actions-by-grade.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/platform-objects': patch ---- - -The organization's member, invitation and team actions are offered only to the membership grades the server admits. A plain member no longer sees "Invite User", "Change Role", "Remove Member", "Cancel Invitation", "Create Team" and the rest, each of which the server refused with 403. - -- `invite_user` (on the Users, Members and Invitations lists) and `resend_invitation`: owner, admin and delegated_admin. -- `update_member_role`, `remove_member`, `cancel_invitation`, `create_team`, `update_team`, `remove_team`, `add_team_member` and `remove_team_member`: owner and admin. -- `transfer_ownership`: the owner alone, on a non-owner row. -- `add_member` is unchanged. Its door is platform-admin standing, not a membership grade. - -Each action declares `requiresMembershipReach` from `@objectstack/spec`, which is lowered into its `visible` predicate. diff --git a/.changeset/21795-spec-membership-reach-action-gate.md b/.changeset/21795-spec-membership-reach-action-gate.md deleted file mode 100644 index 29a2a59b459..00000000000 --- a/.changeset/21795-spec-membership-reach-action-gate.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -An action can declare which organization membership grades it is offered to: `requiresMembershipReach` names a row of the new `MEMBERSHIP_REACH` table and is lowered at parse time into `visible`, the way `requiresFeature` is. - -Clause-②: yes (widening) - -- **`MEMBERSHIP_REACH`** (`@objectstack/spec/identity`) says which membership grades reach which better-auth organization endpoint. `invite_member` is reached by owner, admin and delegated_admin. `cancel_invitation`, `update_member_role`, `remove_member`, `create_team`, `update_team`, `remove_team`, `add_team_member` and `remove_team_member` are reached by owner and admin. `transfer_ownership` (setting the creator role on a member) is reached by the owner alone. The rows are read off better-auth's own access-control statements plus the `delegated_admin` registration, and plugin-auth pins them equal to the door. It is reach, not authority (ADR-0108 D1): a fourth fact beside the membership names, the administrative-grade rule and the identity projection, and merged into none of them. Also exported: `MEMBERSHIP_REACH_NAMES`, `membershipReachPredicate`, `lowerRequiresMembershipReach`, and the `MembershipReachEntry`, `MembershipReachName` and `MembershipReachStatement` types. -- **`ActionSchema.requiresMembershipReach`** is optional and enum-checked against the table's row names. At parse time it becomes one `'' in current_user.positions` term per grade, in the names `mapMembershipRole` projects them to (`org_owner`, `org_admin`, `delegated_admin`). The terms are AND-composed with an explicit `visible`, and the key is stripped from the parsed output. It composes ahead of `requiresFeature`, so a feature gate stays the last term. `visible: false`, a non-CEL or AST-only `visible`, and a blank `source` are refused at parse time, at the key. -- It is UI courtesy, not authorization: the endpoint's own door stays the authority. The capability channel (`current_user.can`) is unchanged and carries no grade (ADR-0108). - -Nothing that parsed before is refused now, and an action without the key lowers exactly as before. diff --git a/.changeset/21803-artifact-lock-package-axis.md b/.changeset/21803-artifact-lock-package-axis.md deleted file mode 100644 index f0dc40338f0..00000000000 --- a/.changeset/21803-artifact-lock-package-axis.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"@objectstack/metadata-protocol": minor ---- - -fix(metadata-protocol)!: an item's lock is the strictest among the installed packages that ship its name, at the write doors and on both reads (#21803) - -Clause-②: no (narrowing) - -ADR-0048 lets two installed code packages ship one `(type, name)`. The ADR-0010 `_lock` gate looked the packaged artifact up with no package, so it bound the artifact of whichever package was registered first, while `getMetaItem`, `getMetaItemLayered`, the metadata list and the `getMetaDiagnostics` locked count looked it up with the request's package. One item had two lock answers, and the door's answer depended on registration order. - -Now every caller takes the artifact layer from one selection: the artifact of every installed package that ships the name, a disabled package included (it is still installed). The lock is resolved once per shipping package, that package's artifact over the stored rows in scope exactly as before, and the item's lock is the strictest of those answers. With one package shipping the name, or none, nothing changes. - -**What moves for consumers.** The door now refuses where it used to depend on registration order: - -- one package ships a lock and another ships none: a save or delete, with or without `?package=`, is refused `403 ITEM_LOCKED` under both registration orders, where it was admitted when the unlocked package was registered first; -- one package ships no lock, the stored row in scope declares a lock, and another package ships a lock refusing the other verb (for example `no-overlay` on the row and `no-delete` on the artifact): both a save and a delete are refused, where each was admitted under the registration order that bound the other answer; -- a disabled package's packaged lock binds under both registration orders, where it bound only when that package was registered first. - -No write the door refused before is admitted now: the artifact the door bound before is always one of the shipping packages. Both reads report the same lock as the door in `lock`, `editable` and `deletable`, under both orders: a read naming a package that ships no lock now reports `editable: false` when another installed package ships one. The refusal and the reads carry the prose of the binding package (the request's own package first, then the others by package id), and no prose when no single package's answer is the strictest. Content stays prefer-local: a read naming a package is still served that package's own artifact, under that package's provenance. - -A served body (`getMetaItem`'s `item`, `getMetaItemLayered`'s `effective`, the list items) now carries exactly the lock family of the resolution's answer, and no `_lock` key when nothing binds. Before, a body served from a stored row outside the lock's scope kept that row's `_lock` while the envelope reported `none` (an organization holding only another package's row, with the request's package served its env-wide row), and an explicit `_lock: 'none'` stayed on a body. No key, export, status or error code changes. - - diff --git a/.changeset/21804-list-slot-prefer-local.md b/.changeset/21804-list-slot-prefer-local.md deleted file mode 100644 index 15f34c8a00f..00000000000 --- a/.changeset/21804-list-slot-prefer-local.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -fix(metadata-protocol): a package's slot in the metadata list serves the package's own stored row in every row order, as `getMetaItem` naming that package does (#21804) - -Clause-②: no - -ADR-0048 lets one item (type, name, organization scope) hold a package's stored `sys_metadata` row beside a package-less one. `getMetaItem` naming the package has always served the package's own row (prefer-local), and the organization's rows before the env-wide rows (ADR-0005). The list (`getMetaItems`, and the `getMetaItemsForExecution` view of it) built each package's slot from the LATEST of the package's row and the package-less row in the order the store returned them. So with both rows stored, the list served the package-less body for the package in one row order, and an env-wide row of the package could beat the organization's package-less row. - -Now the list and the by-name read take one resolution: the organization's rows, then the env-wide rows; within each, the type's canonical spelling, then the other; within each, the package's own row, then the package-less row, never another package's. A package's list slot serves the row `getMetaItem` naming that package serves, whatever the row order, and the `previewDrafts` list previews the draft the by-name read previews. A package with no row of its own still falls back to the package-less row, as before. The lock the list item reports is unchanged: it is still the strictest lock among the item's rows in scope. - -No key, export, status or error code changes. A list request scoped to one package (`packageId`) reads only that package's rows and is unchanged. diff --git a/.changeset/21806-runtime-ai-patch-mount.md b/.changeset/21806-runtime-ai-patch-mount.md deleted file mode 100644 index 8f5457ac7a7..00000000000 --- a/.changeset/21806-runtime-ai-patch-mount.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -A declared `PATCH` AI route is reachable over HTTP: the dispatcher's `/ai/*` method wildcards mount `patch` beside `get`, `post`, `put` and `delete`, so `PATCH /api/v1/ai/conversations/:id` (the SDK's `ai.conversations.update`, the console's conversation rename) reaches its handler instead of answering `405` (#21806). - -Clause-②: no - -- **Where it failed.** On a host where the wildcards are the only door into `/ai/**`, a `PATCH` never reached the dispatcher. The server adapter answered `405 METHOD_NOT_ALLOWED` with `Allow: DELETE, GET, HEAD, POST, PUT`, because the path matched the wildcards under the four other verbs. Both bases the wildcards serve are fixed: `/api/v1` and `/api/v1/environments/:environmentId`. -- **An undeclared method still answers `405`.** The AI route table now tells its two misses apart. A method the table does not declare on a path it does declare answers `405 METHOD_NOT_ALLOWED`, with an `Allow` header that names exactly the declared methods, and no handler runs. A path the table declares under no method still answers `404 ROUTE_NOT_FOUND`. The rule is the same for every verb. -- **What a caller sees change.** A `GET`, `POST`, `PUT` or `DELETE` that names a declared AI path under the wrong method used to answer `404 ROUTE_NOT_FOUND`. It now answers `405` with `Allow`. A `PATCH` to an AI path the table does not declare at all used to answer the adapter's `405`. It now answers `404 ROUTE_NOT_FOUND`. No request that was refused before is served now, except a `PATCH` to a route the table declares. diff --git a/.changeset/21817-scoped-list-fallback.md b/.changeset/21817-scoped-list-fallback.md deleted file mode 100644 index cedda56fcfb..00000000000 --- a/.changeset/21817-scoped-list-fallback.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -fix(metadata-protocol): a package-scoped metadata list serves a package-less customization of an item the package ships, as `getMetaItem` naming the package does (#21817) - -Clause-②: no - -ADR-0048 lets one item hold a package-less stored `sys_metadata` row, an ordinary customization, beside the package's own artifact or row. `getMetaItem` naming the package serves the package's own row, else the package-less row, the organization's rows before the env-wide rows (ADR-0005). A list scoped to the package (`getMetaItems({ type, packageId })`, `GET /api/v1/meta/:type?package=`, and the `getMetaItemsForExecution` view of it) read only the package's own rows. So it served the package's artifact over a package-less customization of it, and an env-wide row of the package over the organization's package-less row, while `getMetaItem` naming the package served the customization. - -Now each slot of a package-scoped list resolves the way `getMetaItem` naming the package does: the package's own row, else the package-less row, by the same order the unscoped list uses. This holds over a registry item, a MetadataService item and a view the package's stored container expands, and in the `previewDrafts` list, where a package-less draft stands in when the package has none. The list's membership is unchanged: it still lists only the items the package ships, and a package-less row of an item the package does not ship adds no item to it. The lock each item reports is unchanged. - -No key, export, status or error code changes. diff --git a/.changeset/21822-install-local-listing-not-loaded-marker.md b/.changeset/21822-install-local-listing-not-loaded-marker.md deleted file mode 100644 index 47566e2d24d..00000000000 --- a/.changeset/21822-install-local-listing-not-loaded-marker.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/cloud-connection": patch ---- - -`GET /api/v1/marketplace/install-local` now marks an installed package that this runtime refused to load. Before, after a restart whose rehydrate refused a package built for another protocol major, the listing served it like any loaded package, and the console's Installed Apps showed it as installed. - -Clause-②: no - -- **What was wrong.** On a restart, a ledger entry whose `engines.protocol` range excludes this runtime is not loaded, and the boot logs `OS_PROTOCOL_INCOMPATIBLE` at `error`. The entry stays in the ledger, so `DELETE` and a compatible re-install still act on it. The listing served it with the same fields as a loaded package. Each request also tried to read its seed rows from objects that were never registered, and logged a `warn` saying it could not. -- **What it does now.** That entry is listed with `"notLoaded": { "code": "OS_PROTOCOL_INCOMPATIBLE", "requiredRange": "^16" }` (the range the package declares) in place of `withSampleData`. No seed row is read for it, so the per-request `warn` is gone. `notLoaded` has exactly these two members, and every authenticated caller sees it. -- **Unchanged.** A loaded package's entry is exactly as before, with no `notLoaded` key. `DELETE /api/v1/marketplace/install-local/:manifestId` removes a marked entry as before, and once a compatible version is installed over it, the entry is listed as loaded. -- **Where the marker comes from.** The rehydrate records each entry it refuses, and the listing reads that record. The listing does not run the protocol check again. diff --git a/.changeset/21828-metadata-loader-one-content-hash.md b/.changeset/21828-metadata-loader-one-content-hash.md deleted file mode 100644 index aa1a17a4378..00000000000 --- a/.changeset/21828-metadata-loader-one-content-hash.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/metadata': patch ---- - -`DatabaseLoader` now persists a `register` whose only change is the order of an object's `fields` (#21828). Before, the loader's checksum sorted every map, so a field reorder hashed equal to the stored row and was never written. The running process still saw the new order, but the persisted `sys_metadata` row kept the old one. - -Clause-②: no - -- **One hash vocabulary in `sys_metadata.checksum`.** The loader now stamps the hash `SysMetadataRepository` stamps on the same column: `hashSpec(body, type)` from `@objectstack/metadata-core`, written as `sha256:` + 64 hex. It is hashed as the item's metadata type, so a reorder of an object's `fields` is a change and every other map is still key-order independent. Its history rows carry the same value as the row they record. Before, the loader wrote bare hex from `calculateChecksum`. That function is unchanged and still exported, but nothing writes the column with it. -- **Rows stamped before this release.** Whether a `register` is a no-op is now decided by re-hashing the stored body, not by comparing the stored checksum. A row with an unchanged body is not rewritten: no version bump, no history row, and it keeps its old checksum until its content next changes. The first real change rewrites it with the new stamp. A reorder into sorted key order is written too, even against a stored checksum that sorted every map. -- **ETags.** `load()` and `stat()` report the row's checksum as `etag`, so a row the loader writes from this release on reports a `sha256:` value. diff --git a/.changeset/21829-predicate-write-matches-readable-rows.md b/.changeset/21829-predicate-write-matches-readable-rows.md deleted file mode 100644 index 02a1a746f1a..00000000000 --- a/.changeset/21829-predicate-write-matches-readable-rows.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@objectstack/plugin-security': minor -"@objectstack/spec": patch ---- - -fix(plugin-security)!: a predicate-scoped update or delete matches only the rows the caller can read - -Clause-②: no (narrowing) - - - -**BREAKING**: a predicate-scoped (`multi: true`) update or delete now matches only the rows the caller can read. A row the read door would not return to the caller is not written, not counted and not refused, so a predicate that reaches only such rows answers exactly what a predicate that matches nothing answers: success, zero rows. This is the by-id write doors' rule ("hidden" and "gone" are one answer to a caller who cannot read the row) carried to the predicate door. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new. - -**What changed.** The rows a predicate write matched came from its write scope alone. A row the caller cannot read was matched whenever that scope reached it, for example through a write-class row-level policy wider than the read policy, or on an object whose read visibility follows a parent record. A per-row gate then refused the whole write with a `403`, or the row was written and counted. Either answer told a hidden row apart from no row. The write middleware now asks the read door which rows the caller's own predicate returns, through a read in the caller's context that every data middleware's visibility applies to, and narrows the write to those rows: readable ∩ writable. A read the read door refuses (no read grant on the object) keeps the write's previous answer. A store fault on that read propagates as raised. - -**What is refused or narrowed now that was not.** -- A predicate write no longer writes, counts or refuses rows its caller cannot read, including rows its write scope reaches. -- A predicate write whose predicate matches more than 10 000 rows the caller can read is refused with `400 INVALID_FILTER`, before anything is written, rather than narrowed by a cut-off list. The limit is the platform's existing row ceiling for one predicate write. - -**FROM → TO.** -- A predicate update or delete reaching rows the caller cannot read: FROM a per-row gate's `403`, or those rows written and counted → TO those rows not matched; success with zero rows when no readable row matches. -- A predicate update or delete whose readable match exceeds 10 000 rows: FROM attempted → TO `400 INVALID_FILTER`, nothing written. - -**If you are affected.** An operator who needs a user to change rows grants that user read access to them first. A caller that read a predicate write's `403` as "a row exists here" reads the result as the count of rows it can see. A predicate whose readable match is over the ceiling is narrowed and written in batches. - -**Unchanged.** -- A caller who can read a matched row but may not write it keeps its answer. -- Writes the platform issues under the caller's context keep their previous answer, because the caller never addressed them: a cascade, a hook's own write, and the referential clear of a lookup. -- By-id writes keep their answers. System-context writes are not narrowed. -- `security/explain` takes no predicate, so it has no predicate-write verdict to change. diff --git a/.changeset/21834-reseed-purge-protocol-handshake.md b/.changeset/21834-reseed-purge-protocol-handshake.md deleted file mode 100644 index f49990697af..00000000000 --- a/.changeset/21834-reseed-purge-protocol-handshake.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/cloud-connection": patch ---- - -`reseed-sample-data` and `purge-sample-data` on an installed package that this runtime refused to load now answer `422 OS_PROTOCOL_INCOMPATIBLE` before they change anything. Before, both acted on such a package anyway. - -Clause-②: no - -- **What was wrong.** On a restart, a ledger entry whose `engines.protocol` range excludes this runtime is not loaded: nothing is registered, synced, bound or seeded for it. `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data` on that entry loaded the package's translations into the i18n service and merged its seed datasets into the kernel's shared `seed-datasets` list, and then failed with `400 RESEED_SKIPPED` because the package's objects were never registered. `POST …/:manifestId/purge-sample-data` answered `200` with every record counted in `errors`, and set the ledger's `withSampleData` to `false` with no row deleted. -- **What it does now.** Both doors run the protocol check on the ledger entry right after reading it. An entry whose declared range excludes this runtime gets the answer the install route gives the same manifest: `422`, `error.code` `OS_PROTOCOL_INCOMPATIBLE`, the check's own message, and `error.details` with `requiredRange`, `rangeSource`, `protocolVersion`, `targetMajor` and `migrateCommand`. No translation is loaded, no dataset is merged, no seed row is read or deleted, and the ledger is not written. The refusal comes before the organization check too, so a session with no active organization on a walled deployment also gets the `422` for such an entry. -- **Unchanged.** An entry this runtime loads is answered exactly as before. An entry that declares no range, or a range the check cannot read, is admitted as before, with no new warning. `DELETE /api/v1/marketplace/install-local/:manifestId` still removes a refused entry, and installing a compatible version over it makes both doors act on it again. diff --git a/.changeset/21836-search-skip-unreadable.md b/.changeset/21836-search-skip-unreadable.md deleted file mode 100644 index 2710331ce75..00000000000 --- a/.changeset/21836-search-skip-unreadable.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -Global search (`GET /api/v1/search`) skips the objects a caller cannot read instead of failing the whole request with `403 PERMISSION_DENIED` (#21836). - -Clause-②: no - -- **Object level.** Before an object is queried, `searchAll` asks the `security` service's `canReadObject` with the caller's context, the same read gate the engine middleware enforces. An object it refuses is skipped. A member whose scope included any object they hold no read grant on used to get 403 for every query. The console palette then showed "No results found." with no error. -- **Field level.** Each object is searched only on the fields the caller may query (`getQueryableFields`). The engine refuses a search that would match on a hidden field, and `sys_user`'s searchable fields include admin-only columns, so a member's unscoped search hit that refusal as well. An object left with no queryable search field is skipped. -- **Nothing about a skipped object reaches the response.** It is not queried, named or counted in `totalObjects`, and the decision is made before any row is read. An explicit `objects=` naming an unreadable object gets the same answer as a name that matches no object. -- **Other failures still fail the search.** A read error on a readable object, or an admission check that throws, propagates as before. Callers that can read every object, and calls without a context, get the same answer as before. diff --git a/.changeset/21839-share-link-password-handling.md b/.changeset/21839-share-link-password-handling.md deleted file mode 100644 index 4a2f94b3f95..00000000000 --- a/.changeset/21839-share-link-password-handling.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-sharing': patch -'@objectstack/plugin-hono-server': patch -'@objectstack/hono': patch -'@objectstack/runtime': patch ---- - -Share-link passwords follow the platform's credential rules (#21839). - -- **The stored hash never leaves the server.** The share-link mint response (`POST /api/v1/share-links`, and `ShareLinkService.createLink`'s return value) no longer carries `password_hash`. The list and the redemption result are projected the same way. A client that reads a link's password state keeps reading it from the redemption route's `NEEDS_PASSWORD` answer, as before. -- **The stored form is the platform's slow password hash.** New passwords are hashed with scrypt at the parameters account passwords use, instead of one salted SHA-256. Links minted before this release keep working: a stored password in a legacy form still verifies, and it is re-hashed into the new form on its first successful redemption. Every comparison is constant-time. A deployment that injects its own `hashPassword` / `verifyPassword` pair is unaffected, and its stored forms are left alone. -- **The password travels in a header.** Both public share-link routes (`GET /api/v1/share-links/:token/resolve` and `/:token/messages`) accept the `x-share-password` request header, the preferred form, because a header is not part of the request URL. The `?password=` query parameter is still accepted for compatibility, so current consoles keep working until they move to the header. `/messages` accepted only the query parameter on this mount before. -- **Cross-origin clients can send the header.** `X-Share-Password` is in the default CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS` in `@objectstack/plugin-hono-server`, which the `@objectstack/hono` adapter also applies). A deployment that passes its own `allowHeaders` is unchanged; add the header to that list to let a cross-origin client use it. -- **Public share-link answers are not cached.** Both public routes answer with `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, on both mounts (the sharing plugin's routes and the runtime dispatcher's `/share-links` domain). The authenticated create, list and revoke routes are unchanged. -- **Hashing works in WebContainer.** On StackBlitz WebContainer, where `node:crypto.scrypt` is incomplete, the password is hashed with the pure-JS scrypt from `@noble/hashes` (now a dependency of `@objectstack/plugin-sharing`, as it already is of `@objectstack/plugin-auth`), at the same parameters and in the same stored form. A hash made on either runtime verifies on the other. diff --git a/.changeset/21841-import-refusal-working-remedy.md b/.changeset/21841-import-refusal-working-remedy.md deleted file mode 100644 index f77c825104d..00000000000 --- a/.changeset/21841-import-refusal-working-remedy.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/metadata-protocol': minor -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): a destructive re-import's refusal names the remedies that work from the import route, instead of a `?force=true` that route never reads (#21841) - -Clause-②: yes (widening) - -- **What was wrong.** "Import as Object" (`POST /api/v1/datasources/:name/external/tables/:remote/import`) saves through the metadata door's own `saveMetaItem`. A re-import that would drop or retype a field the stored object still carries is refused by that save's destructive-change gate, and the import route relays the refusal as `400 EXTERNAL_IMPORT_ERROR`. The refusal ended `re-submit with ?force=true to proceed.` The import route reads no `force`, so a caller who did exactly that got the identical refusal back. -- **What the refusal says now.** The import states its own write face, and the refusal ends: this import cannot be forced, because the external-table import route accepts no `force`. Import the table under a new `name`, or save the changed definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Both remedies are measured on the showcase: each one answers `201` or `200` where the re-import answered `400`. The same words appear in this package's earlier changeset for the import. -- **What widens.** `SaveMetaItemRequestSchema.writeFace` (`@objectstack/spec`) and `saveMetaItem`'s `writeFace` parameter (`@objectstack/metadata-protocol`) gain one member, `'external-import'`. The member is stated by the server. No door reads it from a request body, and the import's own options cannot carry it, or a `force`, into the save. Nothing accepted today is refused. -- **What does not change.** The refusal itself stays: a destructive re-import is still `400 EXTERNAL_IMPORT_ERROR`, and the stored definition does not move. The import route gains no `force`. Acknowledging a destructive change stays on the metadata door. The other faces' wording is unchanged. A `422 INVALID_METADATA` relayed by the import keeps its full findings in the message, because the import route's envelope carries no `issues`. diff --git a/.changeset/21842-validate-reads-live-registry.md b/.changeset/21842-validate-reads-live-registry.md deleted file mode 100644 index ae9a5cb897a..00000000000 --- a/.changeset/21842-validate-reads-live-registry.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/service-datasource': patch ---- - -fix(service-datasource): `POST /api/v1/datasources/:name/external/validate` sees a federated object saved at runtime, with no restart (#21842) - -Clause-②: no - -- **What was wrong.** The federation service read its objects from the `metadata` service. That service holds a copy of the engine's object registry taken once at boot. `PUT /api/v1/meta/object/:name`, and the external-table import that saves through it, write `sys_metadata` and the engine registry, but never that copy. So after a federated object was saved at runtime, validate answered the code-defined objects only, and listed the saved one after a restart. An object re-saved at runtime was judged on its definition as it stood at boot. -- **What it reads now.** `ExternalDatasourceServicePlugin` reads objects (`listObjects` and `getObject`) from the engine's object registry on the `objectql` service, which is the registry the save writes through to. The registry is looked up when validation runs, not when the plugin starts. A saved or imported object is listed and judged on what was saved, the moment the save answers. -- **What does not move.** The comparison is unchanged: the same federation predicate, the same column and type checks, and datasource definitions read from the same place. On `objectstack dev` the boot validation gate sweeps the same objects with the same verdicts as before. No route's request or response shape changes, and no export is added. diff --git a/.changeset/21846-implicit-account-linking-ownership.md b/.changeset/21846-implicit-account-linking-ownership.md deleted file mode 100644 index e065393f9f9..00000000000 --- a/.changeset/21846-implicit-account-linking-ownership.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@objectstack/plugin-auth': minor ---- - -fix(plugin-auth)!: implicit account linking on external sign-in requires the library's standard local-ownership condition; the platform identity provider keeps its documented exception; an unlink is honoured - -Clause-②: no (narrowing) - - - -**BREAKING for deployments that relied on external sign-in (OAuth, OIDC, SSO) linking implicitly to a local user whose email is not verified.** - -**What changed.** - -- An external sign-in links implicitly to an existing local user only when that local user's email is verified. Otherwise the sign-in is refused with `error=account_not_linked`, the same code better-auth's own refusal produces. No link is written and the local user stays unverified. A verified local user links as before. -- The platform's own identity provider (`objectstack-cloud`) keeps its documented exception and still links to an unverified local user, because it seeds the environment owner's row without a mailbox round-trip. -- After a user unlinks a provider, an implicit sign-in through it no longer links the identity again, for any provider. An explicit, signed-in link from account settings (`/link-social`) is still allowed and ends the refusal. If the unlink cannot be recorded, the unlink itself is refused and the provider stays linked. Deleting a user removes the user's unlink records. -- A deployment that passes `secondaryStorage` to the auth plugin now also keeps verification values in the database (`verification.storeInDatabase: true`). The cache still fronts them. This keeps the unlink records durable when the cache evicts entries. Deployments without `secondaryStorage` are unchanged. -- `account.accountLinking.requireLocalEmailVerified` now reads as follows. Unset (the default) means the rules above. `true` applies the strict check to every provider, including `objectstack-cloud`. `false` turns off only the local-verification check and keeps the unlink rule. - -**What to do after upgrading.** - -- A user refused this way signs in with their existing method, then links the provider from account settings, or verifies their email first. -- To let unverified local users link implicitly again, set `account.accountLinking.requireLocalEmailVerified: false`. Before you do, read the library's warning about account takeover. -- If you pass `secondaryStorage`: verification values written to the cache alone before the upgrade (password-reset links, one-time codes, magic links and email-verification links that were in flight at deploy time) can no longer be consumed afterwards. Users who hit this request a fresh link or code once. diff --git a/.changeset/21847-approval-notification-body.md b/.changeset/21847-approval-notification-body.md deleted file mode 100644 index e21712a7704..00000000000 --- a/.changeset/21847-approval-notification-body.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/plugin-approvals': patch ---- - -Approval notifications reach their recipient with their text (#21847). The approvals service put each notification's text in `payload.message`. The messaging service builds the delivered notification from `payload.title` and `payload.body`, the fields its `EmitInput` documents, and no channel reads `message`. So `GET /api/v1/notifications` served every approval notification as a title over an empty `body`, and the inbox row's `body_md` was empty too. The lost texts were comments, request-info questions, send-back notes, reassignments, reminders, escalations, SLA breaches and out-of-office substitutions. Every one of them now travels in `payload.body`. - -Clause-②: no - -- The texts are unchanged. Only the field they travel in moved. Who is notified, and when, is unchanged. -- Approval notifications no longer carry `payload.message`. Nothing in the platform or the console read it. A tenant-authored `sys_notification_template` for an `approval.*` topic that wrote `{{ message }}` should write `{{ body }}`. -- The service's notify helper now declares its payload: `title`, `body`, `actionUrl`, and a reminder's `actions`. A call site that spells the text any other way no longer compiles. -- `@objectstack/service-messaging` is unchanged. There is one field, as documented, and no alias for the old one. diff --git a/.changeset/21848-node-config-values-at-registration.md b/.changeset/21848-node-config-values-at-registration.md deleted file mode 100644 index 34abbdd3c23..00000000000 --- a/.changeset/21848-node-config-values-at-registration.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/service-automation': minor ---- - -fix(service-automation)!: a flow the `kernel:ready` cold-boot bind refuses is no longer left registered and `active` from the boot pull - -Clause-②: no (narrowing) - - - -**BREAKING**: a flow that loaded `active` before can now be absent after boot. It ships as `minor` under the launch-window convention for accept-set narrowings. - -**What was kept before.** A boot registers a package's flows twice. The boot pull runs before a plugin that contributes a node type has registered its executor from its own `start()`, so it cannot check that node's config keys against the descriptor's `configSchema`, and it registers the flow and arms its trigger. The `kernel:ready` bind then re-registers every flow once the executor exists. When it refused one, for an undeclared config key for instance, it logged `[Automation] cold-boot flow bind: failed to register flow` and nothing else: the boot pull's registration stayed, `active` and bound to its trigger, so every run reached the node the refusal located. - -**What happens now.** A flow the `kernel:ready` bind refuses is withdrawn: it is not registered, its trigger is unbound, and the same warning names the flow and the refusal. Only that flow is withdrawn; the rest of the package loads, as it already did for a flow the boot pull refuses. A failed or empty read of the flow list still tears nothing down. - -**What an author sees now.** The flow is absent (`GET /automation/:name` answers `404`), and the boot warning carries the located refusal. The handling is to correct the config the warning locates; the flow then registers as before. - -**Unchanged.** What `registerFlow` refuses, at any door. A flow refused through the `/automation` write doors keeps the definition the engine already held, and a runtime reload that brings a refused body keeps the registered one. diff --git a/.changeset/21849-retire-sys-account-link-social.md b/.changeset/21849-retire-sys-account-link-social.md deleted file mode 100644 index bfceb82e8a9..00000000000 --- a/.changeset/21849-retire-sys-account-link-social.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/platform-objects': minor ---- - -fix(platform-objects)!: retire the `sys_account` `link_social` action, which was dead on every boot; `unlink_account` stays - -Clause-②: no (narrowing) - - - -**BREAKING**: `sys_account` no longer declares the `link_social` action, so the "Link Social Account" toolbar button is gone from the Account app's Linked Accounts list and from Setup's Identity Links. It never completed a link on any boot: it navigated to a `GET` of the social sign-in route, which is served as `POST` only, and it offered a fixed list of seven providers whatever the boot had configured. It is retired under ADR-0049 (enforce or remove) and ships as `minor` under the launch-window convention for narrowings. - -**What stays.** `unlink_account` is unchanged: the same type, target, placement and row-id parameter. Its confirm question no longer says the user can re-link "from their account settings", because no console surface offers that now. The `sys_account._actions.link_social` leaves are gone from the `en`, `zh-CN`, `ja-JP` and `es-ES` bundles. - -**What to do after upgrading.** Linking a social or OIDC identity stays available through the signed-in `POST /api/v1/auth/link-social`, which is `auth.accounts.linkSocial({ provider, callbackURL })` in `@objectstack/client`: call it and navigate to the `url` it answers. diff --git a/.changeset/21850-flow-approval-node-config-contract-refused.md b/.changeset/21850-flow-approval-node-config-contract-refused.md deleted file mode 100644 index 145dbe0f836..00000000000 --- a/.changeset/21850-flow-approval-node-config-contract-refused.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -A flow `approval` node's `config` is judged at parse against the contract the spec declares for it, `ApprovalNodeConfigSchema`, whole: an undeclared key, a refused value and a required key left out are each refused with a location, in the contract's own words. - -Clause-②: yes (narrowing) - - - -**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. - -**Why.** The approval node's executor parses `node.config` against `ApprovalNodeConfigSchema` before it does anything else and fails the node on any issue. Registration already refused an undeclared key, but a refused value such as `escalation.timeoutHours: 0.5` registered and then failed every run that reached the node, and no build door asked about either: `objectstack validate` and `objectstack compile` exited 0 on an `escalation.bogusKey` or a `timeoutHours: 0.5`, and compile copied it into `dist/objectstack.json`. - -**What is refused.** An `approval` node, at any depth, whose `config` the approval contract refuses. The judge is `flowNodeConfigRefusals`, the one `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share; the approval contract joins it as a declared contract map beside the builtin executor contracts, with no plugin loaded. Every issue that contract raises is refused, because the executor refuses on every one: - -- an undeclared key, at the key (`nodes.N.config.escalation.bogusKey`, one issue per key, the top level included), and a refused value, at its key (`nodes.N.config.escalation.timeoutHours` for `0.5` under its minimum of 1): the new closed-set code `node-config-refused-by-contract`, `params: { nodeType, key }`, whose message carries the contract's own sentence — for an alias, its did-you-mean (`timeout` → `timeoutHours`); -- a required key left out (`approvers`; `timeoutHours` inside an `escalation` block): `node-config-key-missing`, as for a builtin node, or `node-config-key-required-by-rule` where a rule of the contract requires it. - -The issue's `code` is `custom`. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.`), `os validate`, `os compile`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`). - -**What stays accepted, byte for byte.** Every approval node the contract accepts, an `approval_revise` node, and every builtin node: the builtin arm still judges only a key left out, so an undeclared key or a wrong-typed value on a builtin node is judged where it was before. A plugin node type whose contract the spec does not declare stays outside the build doors. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `escalation: { …, bogusKey: 1 }`, or any key the contract does not declare | delete the key, or rename it to the one the refusal's did-you-mean names (`timeout` → `timeoutHours`, `mode` → `behavior`, `quorum` → `minApprovals`) | -| `escalation: { timeoutHours: 0.5 }` | `escalation: { timeoutHours: 1 }` — whole wall-clock hours, at least 1 | -| `escalation: { enabled: false }` with no `timeoutHours` | delete the `escalation` block | -| `steps`, `entryCriteria`, `onApprove`, `onReject` or `rejectionBehavior` on the node | the flow graph, as the refusal's guidance says (successive nodes, the entering edge's `condition`, the `approve` / `reject` out-edges, a back-edge) | -| an approval node with no `approvers` | `approvers: [{ type: 'position', value: '' }]` (or any approver the contract accepts) | - -**The one-line fix: write the shape the approval contract declares at the key the refusal names.** The runtime never ran such a node, so the fix changes nothing a working flow does. - -**Who is affected, measured.** At `5e0b489bca`, every approval node `config` authored in this repository parses under the contract: `examples/**` (15 nodes, all in the showcase), `content/docs/**` (6 snippets), `skills/**` (5 snippets) and the `packages/qa/dogfood` fixtures (6 nodes), and so does the Studio designer's approval seed at the pinned objectui commit. Deployed metadata, and repositories other than these two, were not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register. - -### The kit - -- **The refusal.** The declared contract map in `automation/flow-node-config-refusals.ts`, read by the same executor-contract arm of `flowNodeConfigRefusals`; the new code joins `FLOW_SLOT_REFUSAL_CODES`. -- **The ledger.** The D3 semantic entry `flow-approval-node-config-contract-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: the platform cannot know the approvers, the key or the value the author meant. diff --git a/.changeset/21855-action-member-params-array-only.md b/.changeset/21855-action-member-params-array-only.md deleted file mode 100644 index 60341e16eb2..00000000000 --- a/.changeset/21855-action-member-params-array-only.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: an `action:group` / `action:menu` member refuses a non-array `params` unless its `type` is `api`, with the prescription to author an action with static parameter values as its own `action:button` node - -Clause-②: yes (narrowing) - - - -**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports the refusal as an advisory `component-props-invalid` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. - -**Why.** A container member's `params` is its input list: both containers forward an array as the `ActionParam[]` inputs to collect before the action runs. They forward any other `params` value only for a `type: 'api'` member, as its request payload, and drop it for every other `type` (an absent one included), with a development-build warning only. The member declared `params` as any value, so an object `params` on a `navigate_edit` member passed the gate and then had no effect: no error and no static values. `params` carries one shape, and no second value-bag key is declared; a member's `properties.params` is already refused. So static parameter values are not part of the inline action vocabulary at all, and an action that needs them is its own `action:button` node, whose `params` object carries them. - -**What is refused.** On an `action:group` or `action:menu` member whose `type` is not `api`, a `params` that is not an array — an object, a string, a number or `null`. The issue's `code` is `custom`, at `actions.N.params`, and its message names the container, the member's `type` and the prescription: *to run an action with static parameter values, author it as its own `action:button` node, whose `params` object carries them* (and, for a `type: 'api'` member's request body, `bodyExtra`). - -**What stays accepted, byte for byte.** An array `params` on any member; every `params` value on a `type: 'api'` member (its request-payload window, unchanged); a member with no `params`; and an `action:button` / `action:icon` node's object `params`, which is its static values. The member's `params` stays `unknown` in the types — the narrowing is a refinement on the member, and no export, key or type moves. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| `actions: [{ name: 'edit', type: 'navigate_edit', params: { objectName: 'account', recordId: '${record.id}' } }]` on `action:group` / `action:menu` | the action as its own node: `{ type: 'action:button', properties: { name: 'edit', label: 'Edit', actionType: 'navigate_edit', params: { objectName: 'account', recordId: '${record.id}' } } }` | -| `actions: [{ name: 'save', type: 'api', target: '/api/save', params: { status: 'closed' } }]` | unchanged — or, preferred, `bodyExtra: { status: 'closed' }` | -| `actions: [{ name: 'ask', type: 'script', params: [{ name: 'reason', type: 'text' }] }]` | unchanged — an array is the input list | - -**The one-line fix: move a member that carries static parameter values out of its container into its own `action:button` node, with the same `params` object; drop a non-array `params` from any other member.** The container never forwarded those values, so the `action:button` node is the first place they reach the handler. - -## Who is affected, measured - -A writer is an `action:group` / `action:menu` member authoring a non-array `params` on a non-`api` type. The census walked every `params` key in every file that names either block (TypeScript AST over `.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`/`.mts`/`.json` and fenced Markdown code, same-file constants resolved; YAML by text), plus every non-array `params` on an element of any `actions` array corpus-wide, and each hit was read by hand. Lit control: a planted fixture with four non-`api` object members (flat, inside a node's `properties` bag, through a same-file constant, in a Markdown fence), an `api` member and an array member — all six found and classified. - -- **objectstack** at `5b2d189e28`, this branch's base: 24 files name a block, holding 32 `params` keys, 23 not an array; none is a container member's — conversion fixtures of the inline `element:button` action, schema source, the liveness ledger, CHANGELOG quotations, and one `properties.params` refusal probe. No example, doc, skill or fixture writes the refused spelling. -- **objectui** at the `.objectui-sha` pin `0abd4f9f8` and at `main` `f1a177c41` (the same census at both; the action renderers byte-identical): 89 files, 47 `params` keys, 41 not an array. The only container members with a non-array `params` on a non-`api` type are objectui's own tests asserting that the container drops it (`action-entry-object-params-10462.test.tsx`, `action-container-member-params-10290.test.tsx`); the `type: 'api'` controls beside them stay accepted. -- **hotcrm** at `4054ec2680`: no file names either block. -- **cloud** was not reachable from this session. **Deployed metadata** was not measured. - -### The kit - -- **The refusal.** A refinement on each container member (`ui/component.zod.ts`), applied to the `action:group` and the `action:menu` member alike; the member's `params` description says what it now takes, and the generated reference page carries it. -- **The ledger.** The D3 semantic entry `ui-action-group-menu-member-params-array-only` (protocol 18) and its step-18 rationale fragment. No key is removed, so there is no tombstone, and there is no D2 conversion: the static values belong on a different node, which no rewrite can build in the author's place. diff --git a/.changeset/21860-discard-overlay-eligibility.md b/.changeset/21860-discard-overlay-eligibility.md deleted file mode 100644 index facf57e6672..00000000000 --- a/.changeset/21860-discard-overlay-eligibility.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -Discard Overlay no longer deletes the stored definition of a permission set saved into a writable runtime package - -Clause-②: no - -The Discard Overlay action (`POST /api/v1/security/permission-sets/:id/discard-overlay`) is declared to refuse any set that is not package-declared, so that it can never destroy a set the environment authored. It decided that by asking whether any engine-registry item of the set's name carried a package id. The registry also holds the stored definition rows, and a metadata list read (`GET /api/v1/meta/permission`, which every Studio page load issues) stamps a stored row's package binding onto it. A set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=`) therefore passed the check after the first list read: the action answered `200` and deleted the set's only `sys_metadata` row. It now asks the same classifier the packaged-permission-set lock's write doors ask, and refuses every set that classifier does not judge shipped by a code package (including when it cannot decide), with the action's existing `403 PERMISSION_DENIED`. - -The drift diagnostics behind the record's `drift_status` / `drift_detail` (whose `overlay_shadow` detail names Discard Overlay as the remedy) read the package id the same way. They now judge the same population: only sets a code package ships. - -Unchanged: a set a code package ships still has its overlay discarded and its record resynced to the shipped artifact, its drift is still reported as before, and no error code, route or field moves. The refusal's message now says the set is not shipped by any installed code package. diff --git a/.changeset/21861-write-through-update-keeps-package.md b/.changeset/21861-write-through-update-keeps-package.md deleted file mode 100644 index 5dc331c303e..00000000000 --- a/.changeset/21861-write-through-update-keeps-package.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/plugin-security': patch ---- - -A data-door edit of a permission set saved into a writable runtime package updates that set's stored definition instead of forking it - -Clause-②: no - -Setup saves a permission set through the data door (`PATCH /api/v1/data/sys_permission_set/:id`), which redirects the edit into the metadata store. A stored definition row is keyed by its package as well as its name, and the redirected save named no package. For a set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=`), whose only stored row is bound to that package, the save therefore created a second, package-less row carrying the edit and left the package's row untouched: two active definitions for one name, with the package's copy no longer receiving the organization's edits. The save now goes into the package the edited row is bound to, read from that row through the metadata door's own item read, so the edit lands on the set's one row and the row stays in its package. - -Unchanged: a set with no package binding still saves with none, and a set a code package ships is still refused with `403 NOT_OVERRIDABLE` before anything is read or written. The edit answers `200` as before; no error code, route or field moves. diff --git a/.changeset/21863-action-on-success-outcome-messages-form-rows.md b/.changeset/21863-action-on-success-outcome-messages-form-rows.md deleted file mode 100644 index 79751a21386..00000000000 --- a/.changeset/21863-action-on-success-outcome-messages-form-rows.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@objectstack/spec': patch -'@objectstack/platform-objects': patch ---- - -Studio's action form now offers `onSuccess` (the route an `api` or `script` action opens once it succeeds, and whether it opens in place or in a new tab) and `outcomeMessages` (a JSON map from each `outcome` the handler returns to the success message shown for it), with their labels and help text translated for `zh-CN`, `ja-JP` and `es-ES`. diff --git a/.changeset/21867-flow-trigger-record-credential-mask.md b/.changeset/21867-flow-trigger-record-credential-mask.md deleted file mode 100644 index 7f67ba6acc6..00000000000 --- a/.changeset/21867-flow-trigger-record-credential-mask.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@objectstack/trigger-record-change': minor -'@objectstack/spec': patch ---- - -fix(trigger-record-change)!: a record-change flow's trigger record carries the credential mask and omits internal fields - -Clause-②: no - - - -**BREAKING**: the `record` and `previous` a record-change flow receives are now served on the generic read path's terms (ADR-0100). A credential-class field — every `secret` field, and every `password` field outside the exempt `managedBy` buckets — reads as the mask `SECRET_MASK` when set and `null` when unset, and a field declared `internal: true` is absent. It ships as `minor` under the launch-window convention for a changed answer. No export, schema key or error code is added or removed. - -**What changed.** The trigger built both roots from the engine's own write result, which keeps the stored row whole for privileged in-process callers. A credential's stored value and an internal field's value therefore reached the flow, and from there its variables map, a paused run's persisted state and the read doors over that state. The trigger now projects both roots through `omitInternalFieldsFromWriteResponse` from `@objectstack/core`, the helper every external write response already uses, with the trigger object's definition. Everything downstream inherits the projection: the variables map, a paused run's persisted state and its read doors, and the run a resume rehydrates, in the same process and after a restart. - -**FROM → TO.** -- `{record.}` and `{previous.}` in a record-change flow: FROM the stored value (the plaintext password, or the secret's stored handle) → TO `SECRET_MASK` when set, `null` when unset. -- `{record.}` and `{previous.}`: FROM the stored value → TO absent. - -**If you are affected.** A flow that needs a credential reads it through a privileged binder (the flow credential channel, or a privileged server-side read such as the engine's `resolveSecretField`), never off the trigger record. A start or edge condition that compared such a field with a literal tests whether it is set (`!= null`) instead. A condition that compares `record.` with `previous.` now sees two equal masks whenever the field is set on both sides, so it can no longer detect a change; use a privileged binder to detect a credential change. - -**Runs stored before this release.** The mask applies to trigger records built after the upgrade. Paused runs, and terminal runs that keep a restorable snapshot, created before it still hold the clear values in `variables_json`, `context_json` and `steps_json`. After upgrading, resume, cancel or purge those runs. - -**Unchanged.** -- Every ordinary field of the trigger record keeps its value, and every other flow variable is untouched. -- The engine's own write result, the stored row and the privileged read paths (`resolveSecret`, `resolveSecretField`) are unchanged. -- Records a flow reads later through its data nodes already came through the generic read path, which masks them. diff --git a/.changeset/21868-default-org-id-revalidated.md b/.changeset/21868-default-org-id-revalidated.md deleted file mode 100644 index f9a74ea9ac1..00000000000 --- a/.changeset/21868-default-org-id-revalidated.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/plugin-auth': patch ---- - -A user created after the default organization is deleted and recreated in the same process is now bound to the organization that exists, not to the deleted one's id. - -Clause-②: no - -- **What was wrong.** The `tenancy` service's `defaultOrgId()` memoized the default organization id for the life of the process and never checked it again. The single-org bootstrap recreates a missing `slug='default'` organization on the next `sys_user` write, under a new id. Users created under the `auto` membership policy after that were bound to the deleted id. Membership is decided once, at creation (ADR-0093 D7), so nothing repaired them later. -- **What changed.** Every call checks the memoized id against `sys_organization` with one read by primary key. If the organization still exists, it is returned and nothing is re-resolved. If it is gone, the id is resolved again by the same rule that set it (the `slug='default'` organization first, else the only organization), so the replacement is the one a fresh boot would pick. If none exists yet, the answer is `null`, and the next call resolves again. -- **A read the store cannot answer** (a failed read, or a reply that is not a row list) keeps the memoized id. It is not treated as proof that the organization is gone, because that would bind the next user to no organization at all. -- **Who sees it.** Every reader of `defaultOrgId()` gets the check: the membership bind at user creation and its first-session settle, the self-registration grant, the admin create-user path, the membership backfill, the anonymous public form doors and the check on organization-scoped form writes, and the email-template bootstrap. Each call with a memoized id costs one primary-key read of `sys_organization`. -- No public export, option or accepted input changes. Walled postures still answer `null` without reading anything. diff --git a/.changeset/21876-datasource-metadata-read-at-use.md b/.changeset/21876-datasource-metadata-read-at-use.md deleted file mode 100644 index 3f9f36f1e5e..00000000000 --- a/.changeset/21876-datasource-metadata-read-at-use.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@objectstack/service-datasource': minor ---- - -fix(service-datasource)!: on `objectstack start`, external validation and the boot gate compare every federated object - -**BREAKING (narrowing)** — on `objectstack start` a deployment with real schema drift on a -federated object can now refuse to boot, as its `external.validation.onMismatch` setting -declares. - -The federation service (`ExternalDatasourceServicePlugin`) read the `metadata` service once, -in its `init()`, and kept the answer. `objectstack start` composes no metadata plugin: its -`metadata` service is the kernel's in-memory fallback, which the kernel registers after every -plugin's `init()`, just before the start phase. So on `start` the federation service kept "no -metadata service" for the life of the process, and every read behind it answered as if the -deployment declared nothing. It now asks for the `metadata` service each time it reads it, so -`start` sees what `objectstack dev` always saw. - -| on `objectstack start` | before | now | -| --- | --- | --- | -| the boot gate (ADR-0015 §5.2) | logged "all federated objects match their remote schema" with `objects: 0`, having compared nothing, so no `onMismatch` policy ever applied | compares every federated object and applies each datasource's `onMismatch` to every measured mismatch | -| `POST /api/v1/datasources/:name/external/validate` | `ok: true` with no rows | one row per federated object bound to the datasource, with its diffs | -| `POST /api/v1/datasources/:name/external/refresh-catalog` | answered the snapshot, never stored it | also stores it as the datasource's `external_catalog` record | -| `GET /api/v1/datasources/:name/external/tables` | ignored the datasource's `external.allowedSchemas` | leaves out a table whose schema is outside them | -| draft and import names | never resolved a package namespace: drafts were unprefixed, and an import's explicit `name` was never held to the ADR-0028 prefix rule | resolve the namespace of the datasource's package when the datasource carries package provenance; an import whose explicit `name` lacks that prefix is refused `400 EXTERNAL_IMPORT_ERROR` | - -**What to do if a `start` deployment now refuses to boot.** Under `onMismatch: 'fail'` (the -default) the boot stops with `ExternalSchemaMismatchError`, naming the object, the datasource -and each drifted column. Either fix the drift (align the object's fields, its -`external.columnMap` or `external.ignoreColumns`, or the remote table) or set -`external.validation.onMismatch: 'warn'` on that datasource, which boots and logs the drift -instead. To see the drift before deploying, call -`POST /api/v1/datasources/:name/external/validate` on `objectstack dev`, which already -compared. A remote that cannot be reached still never stops the boot. An import refused for -its name takes a `name` carrying the datasource package's namespace prefix. - -**Unchanged.** `objectstack dev`, and every composition that registers a metadata plugin before -the federation service, answers exactly as before: measured on the showcase, every federation -door and the boot gate gave the same answers before and after. What validation judges, what -each `onMismatch` value does, and the boot gate's skip for a datasource that sets -`external.validation.checkOnBoot: false` are unchanged. - -Clause-②: no (narrowing) - - diff --git a/.changeset/21878-activity-label-display-field.md b/.changeset/21878-activity-label-display-field.md deleted file mode 100644 index 6cd8a919a18..00000000000 --- a/.changeset/21878-activity-label-display-field.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/plugin-audit': patch ---- - -An activity row names its record by the record's title as every renderer resolves it, not by a guess from a fixed list of field names - -Clause-②: no - -The record-change mirror writes a label for the record into each `sys_activity` row (`record_label`, and inside the created, deleted and generic updated summary). It used to pick that label from a fixed list of field names (`name`, `subject`, `title`, `full_name`, `label`, `first_name`, `company`, `email`) and fall back to the record id. An object titled by any other field showed its record id on every activity row: an object titled by `company_name` read `Created Customer "RECORD_ID"`, with the raw record id, while its record page showed the company name. - -The label is now the value of the object's title field as ADR-0079 resolves it (`nameField`, then the deprecated `displayNameField`, then the same derivation the record page, the picker and the approvals inbox use). The record id stays the floor: when nothing resolves, when the title field is the primary key, a credential or a field declared `internal`, or when the record's title value is empty. An empty title no longer borrows another populated field. - -- Objects titled by `name`, `title` or `subject` are labelled as before. -- An object whose `nameField` names another field is now labelled by that field. An object whose only list match was not its resolved title is labelled by its title now; in the bundled examples that moves the CRM contact from `full_name` (a formula that declares no `returnType: 'text'`, so it is not derived as the title) to `first_name`, its registered title. -- The activity row records the resolved field as the label's source, so the read-side redaction keeps serving the label only to a reader served that field. - -Rows written before this change keep the label they were written with; nothing is backfilled. diff --git a/.changeset/21880-search-companion-field-scope.md b/.changeset/21880-search-companion-field-scope.md deleted file mode 100644 index ddc169f1378..00000000000 --- a/.changeset/21880-search-companion-field-scope.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -A field-narrowed `$search` no longer matches through the pinyin search companion of a field outside the search-field set (#21880). - -Clause-②: no - -- **What changed.** When the optional pinyin search companion is on (`OS_SEARCH_PINYIN_ENABLED`), the engine's search expansion (`expandSearchToFilter`) adds the companion clause only when every field the companion mirrors is inside the effective search-field set: the set `resolveSearchFields` computes, after any `$searchFields` narrowing. The mirrored fields are read from `resolveSearchCompanionSources`, the same function the companion is provisioned and filled from. -- **What stays the same.** A search with no narrowing keeps the clause whenever the display/name field is in the object's searchable set, so pinyin recall there is unchanged. A CJK term still skips the clause. Deployments with the companion off see no change. -- **Who notices.** A search narrowed to fields that leave out the display/name field, by a `$searchFields` override, by the narrowing global search applies to the fields a caller may query, or by a declared `searchableFields` that omits it, no longer matches through that field's pinyin form. diff --git a/.changeset/21884-fls-mask-object-override.md b/.changeset/21884-fls-mask-object-override.md deleted file mode 100644 index cb99ac09860..00000000000 --- a/.changeset/21884-fls-mask-object-override.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/metadata-core': minor -'@objectstack/rest': patch -'@objectstack/runtime': patch ---- - -The object-schema field mask (ADR-0106 D1) judges an action param that names another object's field through `objectOverride` against that object, not the one being served. - -Clause-②: yes (widening) - -**What a user saw.** A `delegated_admin` may invite members, and the invite door admits them, but `GET /meta/object/sys_user` served that principal no `invite_user` action. The action's `role` param is `{ field: 'role', objectOverride: 'sys_member' }`: it names `sys_member.role`. The mask read every param's `field` as a field of the served object, so a caller denied `sys_user.role` lost the whole action. A plain `member` lost it the same way. The member is now served the action too, and still not offered it: the action's `requiresMembershipReach` predicate excludes the member grade. - -**The rule.** A param whose `objectOverride` names another object reads that object's field. It is judged against the caller's readable fields on that object, and it is not a reference to the served object's fields. The action is still dropped when the caller cannot read the field there, and when that object's readable fields cannot be determined (no answer from the security service, a security service that throws, or an object that does not exist). Nothing about the other object is served on a guess. The rest of the param is still read against the served object: `visible`, an option's `visibleWhen`, `defaultValue`, and an explicit `name` that differs from `field`. A `name` that only repeats `field` is read as that field. With `defaultFromRow`, the param also reads `field` from the served object's row, so `field` is judged against the served object too. An exempt caller (platform admin, `isSystem`) is served the whole schema, as before. - -**The API (`@objectstack/metadata-core`), additive.** - -- `relateObjectSchemaMaskPosture(posture, ...documents)` completes a `project` posture for the documents it is about to mask. It reads the caller's readable fields on each other object their action params name through `objectOverride`. It runs after the fetch, because only the document names those objects. It returns every other posture, and any document with no such param, unchanged, and it never throws. -- The `project` member of `ObjectSchemaMaskPosture` gains two optional fields. `relate` asks the posture's question (same caller, same security service) about another object. `resolveObjectSchemaMaskPosture` sets it. `related` holds the answers. A `project` posture built without `related` gets no answers, so `applyObjectSchemaMask` drops every action with such a param. -- `applyObjectSchemaMask` folds each related read it withholds into the fingerprint, written as `object.field`. Two callers who are denied the same fields on the served object but differ on the other object get different validators. An unrestricted caller's ETag is unchanged. -- The shared contract fixture `FLS_CONTRACT_OBJECT` (`@objectstack/metadata-core/testing`) gains two actions whose params read `contact` fields through `objectOverride`. The contract's projection cases now require the readable one to be served and the denied one to be dropped. An exit that never relates its posture fails the contract by name. - -**Every exit relates its posture (`@objectstack/rest`, `@objectstack/runtime`).** These exits relate the posture after the fetch, before the projection: the shared item, layered and list chains, `RestServer`'s cached read and published read, and the runtime dispatcher's mask. The `/meta` diff route masks `fields` only and needs no relate step. - -**Measured on a showcase boot.** We read every object schema (78 objects, by-name read and list read) as five principals: a platform admin, an org owner, an admin, a `delegated_admin` and a `member`. Before and after this change, the only served action that moved is `sys_user.invite_user`, which is now served to the `delegated_admin` and the `member`. This repository has two authored params with `objectOverride`: `sys_user.invite_user`'s `role` and `sys_member.invite_user`'s `email` (on `sys_invitation`). The second was served to all five principals before and after. diff --git a/.changeset/21889-code-datasource-namespace.md b/.changeset/21889-code-datasource-namespace.md deleted file mode 100644 index eb112ab1c0f..00000000000 --- a/.changeset/21889-code-datasource-namespace.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/runtime': patch -'@objectstack/service-datasource': patch ---- - -An import over a code-defined datasource is held to the namespace of the package that declares it (ADR-0028), and the draft door answers the prefixed name (#21889). - -Clause-②: no - -- **Before.** `POST /api/v1/datasources/:name/external/tables/:remote/import` with an explicit `name` that carried no namespace prefix answered `201` and saved an unprefixed federated object, and `POST …/external/tables/:remote/draft` answered the bare remote table name with a `TODO(namespace)` note. Measured on the showcase's `showcase_external` under `objectstack dev` and `objectstack start`. -- **`@objectstack/runtime`.** `AppPlugin` registers each code-defined datasource through `applyProtection` with the id and version of the package body that declares it, so the item carries `_packageId`, `_packageVersion` and `_provenance: 'package'`. On an ADR-0130 `packages[]` artifact each datasource takes its own body's id, never the artifact's top-level manifest id. A top-level datasource that no body declares keeps its registration under the artifact's own id, and a warning names it. -- **`@objectstack/service-datasource`.** The federation service reads the datasource's package record from the engine registry (`registry.getPackage` on the `objectql` service), the store the runtime publish gate reads for the same check. It used to ask the `metadata` service, which holds no package records in any composition, so no datasource resolved a namespace. The package id still comes only from the stamped `_packageId`. -- **What a caller sees now.** On a datasource whose package declares `manifest.namespace`, an unprefixed import `name` answers `400 EXTERNAL_IMPORT_ERROR` with ADR-0028's message, which names the prefixed name to use. An import with no `name` override saves the prefixed name the draft derives (for example `showcase_customers` instead of `customers`). `GET /api/v1/meta/datasource` lists the three provenance keys on a code-defined datasource; all three are declared on `DatasourceSchema`. The datasource admin list (`GET /api/v1/datasources`) is unchanged, and the admin door still refuses to edit or remove a code-defined datasource. -- **Unchanged.** A datasource that carries no `_packageId` (the host `default` is one) and a package that declares no namespace resolve no namespace, so their imports and drafts answer as before. diff --git a/.changeset/21899-meta-door-code-datasource-read-only.md b/.changeset/21899-meta-door-code-datasource-read-only.md deleted file mode 100644 index e05a99e92c9..00000000000 --- a/.changeset/21899-meta-door-code-datasource-read-only.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/metadata-protocol": minor ---- - -fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one (#21899) - -Clause-②: no (narrowing) - -A datasource an installed package declares in `*.datasource.ts` is code-defined: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin door already refused to edit or remove one. The metadata door did not. The runtime registers a code-defined datasource in memory only, never as a registry item, so the door's artifact check missed it and the write took the runtime-create tier: `PUT /api/v1/meta/datasource/:name` answered 200, persisted a row, and the metadata read then served that row in place of the code definition. - -The door's artifact check now reads the datasources the installed packages declare, so the package door that refuses every other code-shipped item of a type with no overlay channel refuses this one too. - -**BREAKING — what moves for consumers.** - -- `PUT /api/v1/meta/datasource/:name` on a code-defined datasource answered 200 and now answers `403 NOT_OVERRIDABLE`: "Datasource ':name' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy." -- `DELETE /api/v1/meta/datasource/:name` on a code-defined datasource with no stored row answered 200 ("nothing to delete") and now answers the same `403 NOT_OVERRIDABLE`, saying "cannot be removed at runtime". -- The admin door keeps its own `400 DATASOURCE_ADMIN_ERROR`. The two doors' codes differ; the verdict and the remedy are the same. -- The metadata read envelope reports the same answers: `editable: false`, and `deletable` true only while a stored row exists under the name. - -**Remedy.** - -- To change a code-defined datasource, edit the `*.datasource.ts` source that declares it and redeploy. -- A row an earlier `PUT` stored under a code-defined datasource's name is still removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it, once per name. After the next restart both doors serve the code definition again. Until that restart the datasource-admin service keeps the stored copy it restored at boot (tracked in #21922). - -**Unchanged.** A runtime datasource, one no package declares, saves and deletes through the metadata door as before. `OS_METADATA_WRITABLE=datasource` opens the lock exactly as it did. The host's `default` datasource is declared by no package, so the metadata door still accepts edits to it as before; the admin door refuses them. - - diff --git a/.changeset/21910-cascade-federated-tenant-anchor.md b/.changeset/21910-cascade-federated-tenant-anchor.md deleted file mode 100644 index c1b2270dfb1..00000000000 --- a/.changeset/21910-cascade-federated-tenant-anchor.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -Deleting an organization no longer fails with a 500 on a deployment that has a federated (ADR-0015 `external`) object bound. The engine's referential cascade no longer treats the `organization_id` the platform injects into a federated object as a reference to `sys_organization`. - -Clause-②: no - -- **What was wrong.** The registry injects `organization_id` into every object, federated ones included, and the platform provisions no storage for a federated object. The cascade's dependents probe filtered the remote table on that column, the SQL driver refused the unknown column (`INVALID_FILTER`), and the probe's failure propagated, so the delete failed. The showcase, with its federated fixture provisioned, answered every organization delete with 500. -- **What changed.** The cascade scan skips a federated object's injected tenant anchor. It asks the same `isFederatedObject` predicate as the driver-option builder and the related-record read, plus the injected-column provenance marker, so an `organization_id` the author declared on a federated object is still probed. The cascade's atomicity plan asks the same question, so it keeps counting exactly the relations the scan probes. -- **What did not change.** Any lookup an author declares on a federated object is still probed, and a probe that cannot run still fails the delete. Only a missing child table is passed over as having no dependents. diff --git a/.changeset/21911-principal-less-producers.md b/.changeset/21911-principal-less-producers.md deleted file mode 100644 index e72e179df1d..00000000000 --- a/.changeset/21911-principal-less-producers.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch -"@objectstack/objectql": patch -"@objectstack/core": patch ---- - -The platform's own `sys_metadata` reads and writes now carry the explicit system opt-in (`isSystem: true`) instead of reaching the data engine with no principal at all - -Clause-②: no - -- **What moved.** Each engine call in these functions now passes `context: { isSystem: true }`. Inside a repository transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along. - - `@objectstack/metadata-protocol`: the overlay reads (`findServedOverlayRow`, `overlayLockLayerAt`), the list read (`readActiveOverlayRows`, and `readFlattenedMetaItems`' draft preview), the authoring gate's stored-collection fold (`foldStoredCollection`), the audit and commit trail writes (`recordMetadataAudit`, `persistPackageCommitRow`), and the package verbs' store calls (`publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage`, `reassignOrphanedMetadata`). - - `SysMetadataRepository`: `get`, `put`, `delete`, `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage counters. - - `@objectstack/objectql`: `ObjectQLPlugin`'s authored action and hook reads, at boot and on resync. - - `@objectstack/core`: the authored-translation read (`readAuthoredTranslationLayer`). -- **Why.** plugin-security passes an engine operation whose context has no user, no position, no permission set and no `isSystem` straight on to the next handler (ADR-0096's principal-less hand-off). These calls worked only because of that pass-through. They are platform plumbing: any door in front of them has already authorized the caller, and the protocol scopes its own rows by organization. So they now say so with the opt-in that already exists. -- **No gate verdict moves.** A system context skips the six gates the middleware still runs before that pass-through: package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. Four of them only act on other objects. The engine-owned gate never fires on a context with no user. The delegated-administration gate only acts on the RBAC link tables. So none of the six applies to the `sys_metadata` family. An instrumented run of the dogfood suite and a booted dev composition recorded no gate firing on any of these calls before the change. After the change it recorded no principal-less call from these functions. -- **One engine check also stands down under `isSystem`.** That is the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The instrumented runs recorded no refusal from it on any of these calls. -- ⛔ No new API, no export change, and no change to what any door authorizes. diff --git a/.changeset/21912-principal-less-producers.md b/.changeset/21912-principal-less-producers.md deleted file mode 100644 index 9000f83d45f..00000000000 --- a/.changeset/21912-principal-less-producers.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/plugin-auth': patch -'@objectstack/runtime': patch ---- - -Four producers that reached the data engine with no principal and no `isSystem` now carry the explicit system opt-in. Each is already authorized by its own door, so nothing it answers changes. - -Clause-②: no - -- **`@objectstack/plugin-auth` — the platform-admin OAuth client toggle route** (`POST /api/v1/auth/admin/oauth2/toggle-disabled`). Its `sys_oauth_application` read and write go through `withSystemContext`, the wrapper better-auth's adapter already writes those rows through. The platform-admin judge still runs first. The answers (`200`, `404 RESOURCE_NOT_FOUND`, the refusals) and the stored row are unchanged. One log line goes away: the engine's read-only `updated_at` warning on every toggle. The value it warned about was discarded before and the driver still stamps the column. -- **`@objectstack/plugin-auth` — `verifyScimBearerToken`.** The credential probe passes `isSystem: true` in the read's trailing options. It runs before any caller is known, and the digest equality is still all it matches. An unknown, inactive or expired bearer is still `null` (`401`). -- **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). Its `sys_organization` and `sys_environment` reads go through `withSystemContext`. The organization id stays in the `where`. A slug change while an active environment references the organization is still refused (`FORBIDDEN`), and any other change is still allowed. The catches around both reads are unchanged: a read that throws still ends the hook without refusing. -- **`@objectstack/runtime` — the dispatcher's environment-membership gate.** The `sys_environment_member` read carries `isSystem: true` as its query context. The caller's user id stays in the `where`. A member still passes and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. The catch around the read is unchanged: a read that throws still lets the request through. - -Why: the security middleware hands a context with no principal and no `isSystem` straight through (ADR-0096). That hand-through is not an authorization. A caller that is the platform acting for itself says so explicitly. ⛔ No new elevation API, no door's authorization moves, and no accept set changes. diff --git a/.changeset/21913-principal-less-producers.md b/.changeset/21913-principal-less-producers.md deleted file mode 100644 index f685843e1ed..00000000000 --- a/.changeset/21913-principal-less-producers.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/service-settings": minor -"@objectstack/service-messaging": patch -"@objectstack/service-datasource": minor -"@objectstack/plugin-webhooks": patch ---- - -Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off. - -Clause-②: yes (widening) - -- **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes. -- **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too. -- **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`. -- **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in. -- **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt. -- **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged. -- What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects. -- ⛔ No new export on any package entry, and no new elevation API. diff --git a/.changeset/21918-federated-injected-anchors.md b/.changeset/21918-federated-injected-anchors.md deleted file mode 100644 index b597b27158a..00000000000 --- a/.changeset/21918-federated-injected-anchors.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/objectql': patch ---- - -Deleting a business unit or a user no longer fails on a deployment that has a federated (ADR-0015 `external`) object bound. The engine's referential cascade no longer treats any column the platform injects into a federated object as a reference. - -Clause-②: no - -- **What was wrong.** The registry injects its own columns into every object, federated ones included: the tenant anchor `organization_id`, the business-unit anchor `owning_business_unit_id`, the owner `owner_id`, and the audit lookups `created_by` and `updated_by`. The platform provisions no storage for a federated object, so none of them exists on the remote table. An earlier fix taught the cascade to skip `organization_id` alone. The cascade's dependents probe still filtered the remote table on the other anchors, the SQL driver refused the unknown column (`INVALID_FILTER`), and the failure propagated. On the showcase with its federated fixture provisioned, deleting a business unit answered 400 and removing a user answered 500. -- **What changed.** The cascade scan and its atomicity plan skip every column the registry injected into a federated object and the object does not provision. They read which columns those are from the registry's own injected-column provenance, not from a list of names, so a column the registry injects later is covered too. The lifecycle reap and archive passes no longer split a federated object's rows per tenant on its injected `organization_id`: such rows carry no organization, so a tenant-scoped retention override for that object has no rows to select, and the object is swept in one global pass. -- **What did not change.** A lookup the author declares on a federated object, including the author's own `organization_id` or `owner_id`, is still probed, and a probe that cannot run still fails the delete. Only a missing child table is passed over as having no dependents. diff --git a/.changeset/21922-code-datasource-wins-at-restore.md b/.changeset/21922-code-datasource-wins-at-restore.md deleted file mode 100644 index 31aebb21fb3..00000000000 --- a/.changeset/21922-code-datasource-wins-at-restore.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@objectstack/service-datasource": minor -"@objectstack/metadata-protocol": minor -"@objectstack/runtime": minor ---- - -fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host's `default` (#21922, #21944) - -Clause-②: no (narrowing) - -A code-defined datasource (one the installed artifact declares in `*.datasource.ts`, or the host's own `default`) is read-only: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin service states "code wins on collision". The boot restore broke both. It registered every stored `datasource` row in `sys_metadata` over whatever the runtime had registered from code, so after a restart a row left under a code-defined name was served by the admin door, editable there when it carried `origin: 'runtime'`, and handed to pool rehydration. A stored `default` row opened a second live pool named `default` on the row's own connection. The metadata door also still saved edits to `default`, the one code-defined datasource no package declares. - -The runtime now keeps one in-memory set of the datasource names it registers from code, on the kernel service `code-datasource-names`: `AppPlugin` adds the datasources the artifact declares and `DefaultDatasourcePlugin` adds `default`, both in `init()`, so the set is complete before any `start()` runs. The boot restore skips a stored row under a name in that set, and the metadata door's code-datasource check reads the same set. - -**BREAKING — what moves for consumers.** - -- After a restart over a stored row under a code-defined datasource's name, `GET /api/v1/datasources` serves the code definition (`origin: code`) instead of the row, and `PATCH /api/v1/datasources/:name` answers `400 DATASOURCE_ADMIN_ERROR` ("… is code-defined and cannot be edited at runtime.") where it answered 200 for a row that carried `origin: 'runtime'`. -- No live pool is opened from such a row at boot. -- `PUT /api/v1/meta/datasource/default` answered 200 and now answers `403 NOT_OVERRIDABLE`. `DELETE /api/v1/meta/datasource/default` with no stored row answered 200 and now answers the same `403`. The refusal's remedy names the host's database configuration (the database URL the server starts with), which is what defines `default`; every other code-defined datasource's refusal still names its `*.datasource.ts` source. -- The skipped row is kept, and the boot logs one warning naming it. - -**Remedy.** - -- To change a code-defined datasource, change its code definition and redeploy: its `*.datasource.ts` source, or the host's database configuration for `default`. -- A row the boot warning names is removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it. - -**Unchanged.** A runtime datasource with no code twin restores, saves and deletes through both doors as before. A host that composes neither `AppPlugin` nor `DefaultDatasourcePlugin` registers no set, and its stored rows restore as before. While a stored row exists under a code-defined name, `GET /api/v1/meta/datasource/:name` still serves that row (the metadata door reads its stored overlay first); after the `DELETE` above it serves the code definition, in the same boot. - - diff --git a/.changeset/21934-publish-lock-resolved-package.md b/.changeset/21934-publish-lock-resolved-package.md deleted file mode 100644 index 07d2a66b56b..00000000000 --- a/.changeset/21934-publish-lock-resolved-package.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -A metadata publish consults the item lock at the package key it resolved - -- A publish that states no package promotes the draft row it resolves, under that row's own package. Its ADR-0010 lock lookup now uses that same resolved key (the stated package, else the draft row's own), the key the gate reads the draft under and the promotion writes under, instead of only the package the request stated. Where several packages' rows declare the strictest lock, the refusal now carries the lock of the package whose draft is being promoted. -- The authoring gate's package narrowing is unchanged: it still uses only the package the caller stated. diff --git a/.changeset/21934-publish-promotes-judged-draft.md b/.changeset/21934-publish-promotes-judged-draft.md deleted file mode 100644 index f7e656bc267..00000000000 --- a/.changeset/21934-publish-promotes-judged-draft.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/metadata-protocol': minor ---- - -A metadata publish promotes only the draft its gate judged - -Clause-②: yes (widening) - -- A publish (`publishMetaItem`, and each promotion of `publishPackageDrafts`) reads the draft to judge it and then promotes the draft row. The promotion is now handed the judged draft's hash. A draft saved after the judgement, or a draft that appears where the judgement found none, is refused with `409 METADATA_CONFLICT`, and nothing is published. Publishing again judges and promotes the current draft. -- `SysMetadataRepository.promoteDraft` takes a new optional `expectedDraftHash` (`string | null`). When it is stated, the draft row the promotion reads must carry that hash (with `null`, no draft row may exist); otherwise the promotion throws a `ConflictError` before anything is written. When it is omitted, the promotion behaves as before. diff --git a/.changeset/21934-save-check-anchor-per-package.md b/.changeset/21934-save-check-anchor-per-package.md deleted file mode 100644 index 3f8a55bf117..00000000000 --- a/.changeset/21934-save-check-anchor-per-package.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -The organization-scoped save check judges a view overlay against every package's environment-wide definition of its row - -- An organization-scoped `view` save or publish in the organization the anonymous form endpoints read is refused when it would leave open a form the environment-wide definition withdraws. Its row anchor is now resolved per package, the way the list read resolves each package's item: each package's own environment-wide row, else the package-less environment-wide row (which stands in for every package), else that package's artifact. Before, with no environment-wide row stored, it judged only the first package's artifact in registry order, and a stored row of any one package hid every package's artifact of the name. -- The known limit stated with the public-form withdrawal ("it may over-close, never under-close") is narrowed. A withdrawal of a view name still closes that name in every package, so it may over-close. The organization-scoped save check judges every package's environment-wide definition of the name. The anonymous endpoints do too, with one exception: where a package's environment-wide copy of a view container is saved, the endpoints read that copy's expansion alone for each form it expands, and can miss another package's withdrawal of that form, whether saved or shipped, until the form is withdrawn in every saved environment-wide copy of that container as well. Reading each package's expansion separately is tracked in #21967. diff --git a/.changeset/21934-view-list-one-item-per-package.md b/.changeset/21934-view-list-one-item-per-package.md deleted file mode 100644 index 1d41810b0b0..00000000000 --- a/.changeset/21934-view-list-one-item-per-package.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -The view list serves one item per package for a view name that several installed packages ship, whether or not a view row is stored - -- Where two installed packages ship a view of the same name, the list read (`getMetaItems` for `view`) now serves each package's item of that name, as it already did while no view row was stored. Only a name that a stored view container's expansion writes is upserted by name. -- The environment-wide view list is the layer the anonymous form endpoints judge a withdrawal against. A package-less organization copy of the view, stored before one package withdrew the form, is now judged against every package's body of the name there, so it stays closed whichever package withdraws. diff --git a/.changeset/21941-access-guards-fail-closed.md b/.changeset/21941-access-guards-fail-closed.md deleted file mode 100644 index 957a7cc5c60..00000000000 --- a/.changeset/21941-access-guards-fail-closed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/runtime': minor -'@objectstack/plugin-auth': minor ---- - -fix(runtime, plugin-auth)!: two access guards refuse, instead of admitting, when their own read cannot answer - -Clause-②: no (narrowing) - - - -**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention: a request that one of these two guards let through only because the guard's own read faulted is now refused. Nothing an author or caller writes changes shape. - -- **`@objectstack/runtime` — the dispatcher's environment-membership gate.** When its `sys_environment_member` read throws, or no ObjectQL engine resolves on the request's kernel, the request is refused with `503 SERVICE_UNAVAILABLE` — the `AuthzStoreUnavailableError` answer the identity step and the domain gates already give an authorization input they could not read. Before, the gate logged at debug level and let the request through. A member is still admitted, and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. An engine whose registry does not register `sys_environment_member` declares the gate inapplicable, and nothing is read. -- **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). When its `sys_organization` or `sys_environment` read throws, the organization update is refused with `503 SERVICE_UNAVAILABLE`. Before, the hook ended without refusing and the slug changed. A slug change while an active environment references the organization is still refused (`403 FORBIDDEN`), and any other change is still allowed. An engine that does not register `sys_environment` — the open-source composition, where it is a cloud-provided object — declares the guard inapplicable from its registry (`getSchema`): nothing is read and the update proceeds as before. Without a data engine the guard does not apply either. - -What changes for you: nothing in what you write. A `503 SERVICE_UNAVAILABLE` on these doors is a store outage that used to be hidden behind an admitted request; it clears when the store answers again. diff --git a/.changeset/21948-spec-field-help-served-on-description.md b/.changeset/21948-spec-field-help-served-on-description.md deleted file mode 100644 index 7a294fa74df..00000000000 --- a/.changeset/21948-spec-field-help-served-on-description.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -A served object field no longer carries an undeclared `help` key. `translateObject` now serves a field's translated help on the field's `description` (#21948). - -Clause-②: no - -- **What moved.** A bundle's `objects..fields..help` entry is the translation of the field's `description`, because the i18n extractor writes it from that key. `translateObject` (and so `GET /api/v1/meta/object/:name` in a non-English locale) used to put it on a `help` key that `FieldSchema` does not declare. The served field then failed `FieldSchema` with `unrecognized_keys`. A consumer that reads only declared keys rendered the English `description`, and the console logged one ingestion warning per such field. The translation is now served on `description`, and the served field carries no `help`. -- **Precedence (ADR-0029 D9.2a).** The catalog applies only while the served field's `description` still equals the packaged field's. This is judged by the same comparison the object scalars, views and dashboards use. A description that diverged (an `objectExtensions` field, or a tenant's own edit) keeps its authored value in every locale. With no packaged base supplied, the catalog applies, as before. A field's `label` is unchanged and stays a flat `catalog ?? document`. -- **`ObjectFieldLike`** (`@objectstack/spec/system`) drops its `help?: string` member. Its `[key: string]: any` index signature still accepts and types a `help` key, so a caller that writes or reads one still compiles. `inlineHelpText` is not touched. -- Readers that fall back from `help` to `description` (`field.help || field.description`) render the same translated text as before. -- ⛔ No schema, parse or export change. The translation bundle's own field `help` key is unchanged. diff --git a/.changeset/21955-spec-redaction-driver-identity.md b/.changeset/21955-spec-redaction-driver-identity.md deleted file mode 100644 index 759ef51e8b8..00000000000 --- a/.changeset/21955-spec-redaction-driver-identity.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The datasource read redaction resolves a driver's identity the way its sibling helper does. The per-driver half of `redactableConfigKeys` now looks a driver up through `resolveDriverId`, the resolver `passthroughSecretPaths` and the write door's contract lookup already use. So every spelling the write door accepts as a builtin driver is redacted as that driver. - -Clause-②: no - -- The still-writable credential key is withheld on the datasource admin read (`GET /api/v1/datasources/:name`) and on the metadata read under every accepted spelling of its driver, and `redactedConfigKeys` names it. -- A crafted driver id that made the read throw now answers as a driver the platform ships no contract for: the canonical credential spellings and the former aliases are withheld, and the read succeeds. -- `restoreRedactedConfig` and the credential migration read the same list, so an untouched Save still restores the stored value under every accepted spelling, and the migration names the still-writable key as residue there too. -- Unchanged: what the write door accepts, every export and its type, and the answer for a canonical spelling. diff --git a/.changeset/21958-retire-locale-format-settings.md b/.changeset/21958-retire-locale-format-settings.md deleted file mode 100644 index 2a4d49b6a9f..00000000000 --- a/.changeset/21958-retire-locale-format-settings.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/service-settings': minor ---- - -fix(service-settings)!: the Localization settings no longer offer `date_format`, `time_format`, `number_format` or `first_day_of_week`: dates, times, numbers and the week start follow the locale (#21958) - -Clause-②: no (narrowing) - - - -**BREAKING**: the Localization settings namespace (`localizationSettingsManifest`) drops four specifiers that nothing ever read: `date_format`, `time_format`, `number_format` and `first_day_of_week`, together with the Formats group they made up and their copy in the `en`, `zh-CN`, `ja-JP` and `es-ES` settings bundles. Setup → Localization no longer shows them, and `GET /api/settings/localization` no longer serves them. The maintainer ruled that dates, times, numbers and the first day of the week follow the user's locale (language and region), as Salesforce derives them from a Locale, and that the four separate settings retire rather than being implemented. `timezone`, `locale`, `default_country`, `currency` and `fiscal_year_start` are unchanged. The manifest's `version` is now 2, as the `SettingsManifest.version` contract asks when keys are removed. - -**A value a workspace already stored for one of the four is kept.** Measured at the REST surface: - -- The `sys_setting` row stays exactly as it was. No read, save or reset rewrites or deletes it. -- `GET /api/settings/localization` does not resolve it: the key is absent from both `manifest.specifiers` and `values`. -- A `PUT /api/settings/localization` that names one of the four is refused with `400 UNKNOWN_KEY` (`details.key` names it), the answer every undeclared key gets. The refusal covers the whole batch, so a live key sent beside it does not land either. -- A stored row never blocks saving the live keys, and the built-in `reset` action leaves it in place. -- In process, `settings.get('localization', 'date_format')` (or any of the four) now rejects with `SETTINGS_UNKNOWN_KEY`. -- An `OS_LOCALIZATION_DATE_FORMAT`, `OS_LOCALIZATION_TIME_FORMAT`, `OS_LOCALIZATION_NUMBER_FORMAT` or `OS_LOCALIZATION_FIRST_DAY_OF_WEEK` variable is no longer read. It set a value that nothing read before either. - -**What to do after upgrading.** Nothing has to be rewritten: the four keys never changed how anything rendered. There is no replacement key. `locale`, the workspace's language and region, is what decides how dates, times and numbers are written. The console's calendar and timeline do not yet take their first day of the week from it; that is objectui work, tracked and shipped separately. - -It ships as `minor` under the launch-window convention for narrowings. diff --git a/.changeset/21960-setup-users-all-users.md b/.changeset/21960-setup-users-all-users.md deleted file mode 100644 index bd0e6664246..00000000000 --- a/.changeset/21960-setup-users-all-users.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/platform-objects": patch ---- - -Setup → Users now opens on the "All Users" list. Before this, the console opened `sys_user`'s first declared list view, "My Profile". That view is filtered to the caller with a page size of 1, so an administrator saw one row, themselves, and nothing said the rest of the organization was one tab away. - -Clause-②: no - -- The Setup app's `nav_users` entry now sets `viewName: 'all_users'`. The key is the one the spec already declares on an object navigation item, and the console honours it. No new key, no `listViews` reorder, and no view is removed. -- "My Profile" (`me`) is still a tab on the Users page. The Account app's profile entry is unchanged: it is the `account:profile_card` component, which reads the signed-in user from the session, not this list view. The `me` view's code comment no longer says the Account app surfaces it. -- ⛔ No schema, parse, export or accept-set change. diff --git a/.changeset/ai-chat-window-retired.md b/.changeset/ai-chat-window-retired.md deleted file mode 100644 index 4aaed4b92a2..00000000000 --- a/.changeset/ai-chat-window-retired.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec)!: `ai:chat_window` is retired — refused by name at the schema door, the floating chat overlay is the AI chat entry point (#21504, ADR-0049) - -Clause-②: yes (narrowing) - - - -**BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings (the `user:profile`, `element:filter` and `element:form` retirements shipped the same way). - -`ai:chat_window` was declared in `PageComponentType` and mapped to `AIChatWindowProps` (`mode`, `agentId`, `context`, `aria`) in `ComponentPropsMap`, and no renderer for it ever shipped — not in objectui, framework or cloud. The console leaves it unregistered on purpose: the floating chat overlay it mounts on every page is the supported AI chat entry point, and an inline page-level chat window is not part of the supported surface. So an authored `ai:chat_window` node validated clean and then drew "Unknown component type" in front of an end user, and none of its four props configured anything. The triage ruling retired it under ADR-0049 enforce-or-remove, refused by name, following the `user:profile` precedent. - -**What is refused:** an authored `ai:chat_window` component node, at `PageComponentSchema.type`. That covers `definePage()`, `PageSchema`, and every door that parses pages: `os validate`, `os build`, `os lint` and the metadata save door. The issue is located at the node's own path, with `code: 'custom'` and `params.retiredComponentType`, and its message is the retirement prescription. `PageComponentType`'s own error map refuses the name with the same text when the enum is parsed alone. `ComponentPropsMap['ai:chat_window']` stays as a row, so every reader that dispatches on it keeps recognising the name: the component-props gate, `check-yaml-examples` and the type vocabulary's known set. The row now refuses every props bag, `{}` included, with the same prescription. One prescription string, `RETIRED_PAGE_COMPONENT_TYPES` in `@objectstack/spec/ui`, answers at all three doors. - -**What is removed from the exports:** `AIChatWindowProps` (`@objectstack/spec/ui`), the props schema the element no longer has. Its JSON Schema (`ui/AIChatWindowProps`) is no longer published. - -**What stays accepted:** every other member of `PageComponentType` and `ComponentPropsMap`, byte-identically. That includes `ai:suggestion`, which keeps its row and its place in the enum, so `ai:` stays a namespace the `component-type-unknown` authoring rule claims. The open string arm also stays open: custom and plugin-registered types keep parsing. The only string refused is the retired name itself. - -## FROM → TO - -| you wrote | write instead | -|:--|:--| -| a `{ type: 'ai:chat_window' }` component node in a page region, slot or container | nothing: delete the node. The floating chat overlay is on every page already | -| `properties: { agentId: '…' }` on that node | the app's `defaultAgent` (a platform agent: `ask`, the default, or `build` on an authoring surface) | -| `properties: { mode, context, aria }` on that node | nothing: none of them was ever read, and the overlay is not configured per page | - -The one-line fix: delete the `ai:chat_window` component node. No ADR-0087 conversion is registered, because the only edit is deleting an authored page node, and a mechanical conversion does not delete page nodes: which region closes up is a layout decision. The D3 entry `ui-ai-chat-window-retired` carries that delegation, so `os migrate meta --from 17` lists it as a manual change for every stack that still names the type. - -## Who is affected, measured - -- **objectstack** at `529d9711fb`: zero authored `ai:chat_window` nodes in `examples/**`, `packages/apps/**`, `apps/**`, `skills/**` and `content/docs/**` code samples. The only hits were the spec's own type list, its row, its tests and the generated reference docs. The control in the same query shape: `element:divider` is authored in 3 example files and `record:details` in 12. -- **objectui** at the `.objectui-sha` pin `89cad75d55`: no renderer is registered. `components/src/renderers/placeholders.tsx` omits the type on purpose, and Studio's page palette excludes it. The remaining hits are tests asserting its absence, the palette exclusion, a parity-ledger entry and comments. No non-test source imports `AIChatWindowProps` or indexes the row. -- **cloud** and **hotcrm** (triage's census): zero producers. hotcrm names it once, in a comment, as dropped. -- **Deployed metadata** was not measured. - -The retirement kit: - -- the retired-type map entry, the enum value removed (`packages/spec/src/ui/page.zod.ts`), and the row turned into a whole-bag refusal, with `AIChatWindowProps` removed (`packages/spec/src/ui/component.zod.ts`) -- the D3 semantic entry `ui-ai-chat-window-retired`, its step-18 rationale fragment, and the `RETIRED_DEFS_BY_MAJOR` entry `ui/AIChatWindowProps` -- pin tests: in `component.test.ts`, `code`, `path`, `params` and the first sentence at each of the three doors, with `ai:suggestion` as the control and the open arm left open. In `component-type-vocabulary.test.ts`, the type stays known, leaves the typo candidates, and `ai:` stays reserved. The `ComponentPropsMap` `z.unknown()` enumeration loses its `ai:chat_window` `context` line with the row's keys. -- generated baselines and docs follow the schema: `api-surface/`, `export-origins/`, `declaration-map/`, `authorable-surface/`, `authorable-defaults/`, `json-schema.manifest/`, `spec-changes.json`, the upgrade guide and the reference docs. The hand-written `content/docs/ui/pages.mdx` component list now says the truth. diff --git a/.changeset/console-0abd4f9f8769.md b/.changeset/console-0abd4f9f8769.md deleted file mode 100644 index e51f757e1f1..00000000000 --- a/.changeset/console-0abd4f9f8769.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `0abd4f9f8769`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 3 releasing of 5 changesets added across 5 non-merge commits; omitted: 2 release-nothing changesets (they ship no package code). - -- **minor** — `@object-ui/types` declares each renderer's NODE SLOTS once (`NODE_SLOT_DECLARATIONS`, `nodeSlotsFor`), and `objectui check`, core `validateSchema`, the SDUI parser's `validateTree` and the `kind:'html'` page compile walk those slots as well as `children` (objectui#11170). Its changeset declares `Clause-②: yes (narrowing)`: a node under a slot that was never judged is judged now. (objectui `c4c506b9e`) -- **minor** — The screen-flow runner names the flow by its label, in the user's language (objectui#11092, the objectui half of objectstack#20318). (objectui `39a3e91fa`) -- **patch** — The External Datasource panel in Setup and Studio reads the `{ success, data }` envelope its routes answer (objectui#11628). On a federated datasource such as the showcase's `show… (objectui `0abd4f9f8`) - -No objectui commit in the range carries `!`, and no changeset in it declares `major` or carries the breaking annotation. objectui#11170's narrowing is objectui's own validation reach and changes no ObjectStack-authorable key. The manifest this repository ships is generated without `slotsFor`, so its entries carry no slot list. The release-nothing pair is objectui#11396, which derives `MasterDetailDetailConfig` from the spec's `details` entry by reference, member for member the same, and objectui#11095, a KPI-tile invalidation pin. - -objectui range: `9dfaca654311...0abd4f9f8769` diff --git a/.changeset/console-2e818d0b51ec.md b/.changeset/console-2e818d0b51ec.md deleted file mode 100644 index dd6d3c82410..00000000000 --- a/.changeset/console-2e818d0b51ec.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `2e818d0b51ec`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 17 releasing of 18 changesets added across 22 non-merge commits; omitted: 1 release-nothing changeset, 6 commits carrying no changeset (they ship no package code). - -- **minor** — `FlexBlockNode`, the TypeScript type of an authored `flex` node, types its bag's child list as nodes: `properties.children` is `SchemaNode | SchemaNode[]`, where it was `unknown[]… (objectui `b10c68e58`) -- **minor** — The dashboards' metric card node, `plugin-dashboard:metric`, is a declared node type, and both dashboard surfaces hand `SchemaRenderer` declared nodes with no cast (objectui#11466… (objectui `83e3f8377`) -- **minor** — **BREAKING** — **An `object-metric` node in a dashboard widget's legacy `component` envelope now draws the retired-format prompt instead of its number (objectui#11466).** This follows the mainta… (objectui `83e3f8377`) -- **minor** — **BREAKING** — A node slot and `SchemaRenderer`'s `schema` prop take the union of the declared node types, `DeclaredNode` (objectui#11466). (objectui `83e3f8377`) -- **minor** — `safeValidateSchema`, and so `objectui validate`, accepts `record:line_items`, the last ADR-0080 public block it refused at `type` (objectui#10872). `@objectstack/spec` 17.6.0 car… (objectui `8d0ca9183`) -- **minor** — `ObjectKanbanSchema.grouping` is declared on both faces, as `@objectstack/spec`'s `GroupingConfig`, by reference (objectui#11216). (objectui `a7557a7d4`) -- **patch** — The dashboard surfaces read every `widgets[]` entry without leaning on `BaseSchema`'s index signature: a widget key is read on the widget arm alone, and the `chart` node is built… (objectui `2e818d0b5`) -- **patch** — A refused save, pin, reorder, view setting, report save, publish or discard in the console is now said to the user, and the view-config panel no longer reports a refused save as s… (objectui `e8c0b9614`) -- **patch** — The `object-grid` summary footer reads `currency`, `defaultCurrency`, `precision` and `scale` from the object field only. A column that carries one of these keys no longer changes… (objectui `d768c3178`) -- **patch** — fix(types): `AnyComponentSchema`'s declaration prints every category union by name, so `@object-ui/types` no longer sits at the edge of TypeScript's serialization ceiling (objectu… (objectui `bdc9049ed`) -- **patch** — "Save as view" now saves a Kanban view the platform accepts, and both Create View doors save the same view from the same dialog choices (objectui#11581). (objectui `b65aa5e65`) -- **patch** — The console and runner stylesheets compile only from their declared `@source` lines. Tailwind's automatic source detection is now off (`source(none)`), as it already was for `@obj… (objectui `5a2ca6b12`) -- **patch** — A refused view save is now said to the user, and the Create View dialog no longer closes as if the view were saved (objectui#11578). (objectui `f1c937966`) -- **patch** — The console's Create View dialog now creates chart views the platform accepts (objectui#11576). (objectui `d0097af2e`) -- **patch** — fix(plugin-grid): switching a server-grouped grid's grouping field shows exactly the new field's groups (objectui `2f54dca53`) -- **patch** — fix(app-shell): an interface page relays its source view's `tree` and `chart` blocks, and its own `allowPrinting` (objectui#11572) (objectui `09036173c`) -- **patch** — A stored list view now renders the same on an interface page and on the object page when it carries a legacy `options` bag (objectui#10380). (objectui `068564691`) - -⚠️ 2 of these carry a breaking change: 2 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 6 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ fix(release): the publish lane pushes the version's git tags and creates its GitHub Releases itself — changesets/action@v1 finds no `New tag:` line under CLI v3 (objectui#11596) (… (objectui `973fc20f3`) -- _(no changeset)_ docs(skills): `schema-expressions.md` states `{ condition, style }` as the only authorable conditional-formatting rule on every list carrier (objectui#11534) (#11595) (objectui `94985a92b`) -- _(no changeset)_ chore: release packages (#5400) (objectui `b493919c7`) -- _(no changeset)_ docs(plugin-detail): the reference-rail README says `entries` go in the `properties` bag, and that the validator refuses a flat one (objectui#10872) (#11584) (objectui `7a7660c65`) -- _(no changeset)_ docs(skills): the page-builder guide's object-form example names its fields; a per-form override goes on a section entry (objectui#11550) (#11561) (objectui `0a53c67f9`) -- _(no changeset)_ docs(skills): type three marked fences' schema as SchemaRendererProps['schema'] (objectui#11543) (#11558) (objectui `5cde2c6fe`) - - - -objectui range: `ab1879721595...2e818d0b51ec` diff --git a/.changeset/console-89cad75d5570.md b/.changeset/console-89cad75d5570.md deleted file mode 100644 index e3527c0e273..00000000000 --- a/.changeset/console-89cad75d5570.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `89cad75d5570`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 74 releasing of 88 changesets added across 64 non-merge commits; omitted: 14 release-nothing changesets, 6 commits carrying no changeset (they ship no package code). - -- **minor** — **BREAKING** — chore(console)!: drop the lazy `tree` registration stub (objectui#10859, batch 8) (objectui `990a2d616`) -- **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the thirty node type keys objectui#10859 batch 8 retired (objectui `990a2d616`) -- **minor** — **BREAKING** — chore(core)!: the record-source `data` arm table drops `tree` and `view:tree` (objectui#10859, batch 8) (objectui `990a2d616`) -- **minor** — **BREAKING** — refactor(fields)!: the 28 field widgets that still registered a bare node-type fallback register `field:` only (objectui#10859, batch 8) (objectui `990a2d616`) -- **minor** — **BREAKING** — refactor(plugin-tree)!: retire the bare `tree` node type key; `object-tree` is the one spelling (objectui#10859, batch 8) (objectui `990a2d616`) -- **minor** — **BREAKING** — refactor(plugin-view)!: retire the bare `view` node type key; `object-view` is the one spelling (objectui#10859, batch 8) (objectui `990a2d616`) -- **minor** — Three more reader sites stop riding `BaseSchema`'s index signature (objectui#11355 round 2, part of the preparation for objectui#8347's removal of that signature). None changes ru… (objectui `31987bd50`) -- **minor** — **BREAKING** — feat(types): the six `@object-ui/plugin-designer` node types validate; `ProcessDesignerSchema.variables` and `ReportDesignerSchema.parameters` leave the TypeScript face (objectui#… (objectui `063832222`) -- **minor** — **BREAKING** — BREAKING (`@object-ui/core`): `mergeAuthoredPresentation` and `axisPresentation` are no longer exported (objectui#11372). (objectui `f9c8c4e45`) -- **minor** — **BREAKING** — A `page` node refuses `maxWidth` and `padding` by name, and the layout guide teaches the controls that work: `pageType` for the page's width, a `container` for a narrower column o… (objectui `a1a44d621`) -- **minor** — `object-timeline` and `view:timeline` publish the ten `@objectstack/spec` 17.5.0 row keys their renderer honours, and `objectName` is no longer required (objectui#11168 slice 5, u… (objectui `6cd5ae3ea`) -- **minor** — The console build now writes `dist/sdui.manifest.json`, the SDUI component manifest of the Console it built (objectui#11403). (objectui `f88a900e7`) -- **minor** — A bind-only `list` is accepted: `ListSchema.items` is optional on both faces, and the zod face requires at least one of `bind` / `items` (objectui#11405). (objectui `9547063da`) -- **minor** — **BREAKING** — feat(core): the `flex()` builder emits its props in the `properties` bag (objectui#11276) (objectui `138ad4554`) -- **minor** — **BREAKING** — feat(types): an authored `flex` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#11276) (objectui `138ad4554`) -- **minor** — **BREAKING** — feat(types): an authored `object-grid` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#11276) (objectui `6aa029b63`) -- **minor** — **BREAKING** — objectui's app document refuses `mobileNavMode` by name, the answer the platform already gives (objectui#11363). (objectui `e100589f3`) -- **minor** — fix: the widget width / height editors write a whole four-number `layout` (objectui#11388) (objectui `6e9c8d27e`) -- **minor** — **BREAKING** — A gate that is declared but cannot be evaluated is a fault, not "no gate" (objectui#11358) (objectui `063119f2b`) -- **minor** — A public block's prop written directly on the node, instead of inside its `properties` bag, is refused by name on both faces, with a message naming `properties.KEY` (objectui#1087… (objectui `b5696d344`) -- **minor** — Five label positions that `@objectstack/spec` types as `I18nLabel` now accept the per-locale map in `@object-ui/types` too, where they were typed `string` (objectui#10993, batch 4… (objectui `b4075c088`) -- **minor** — feat(plugin-gantt): `object-gantt` publishes the eleven `@objectstack/spec` row keys its renderer honours (objectui#11168 slice 4) (objectui `8673402a3`) -- **minor** — The strict authoring face accepts the `layout` that the editable dashboard grid's Save Layout writes onto a `metric-card` in a dashboard's widget slot (objectui#11070, round 11).… (objectui `0a78a20c8`) -- **minor** — The spec's page blocks, the `element:text_input` / `element:record_picker` rows and a stored page document under its page kind have a TypeScript authoring type, and `SchemaRendere… (objectui `304f61137`) -- **minor** — Small reader sites stop riding `BaseSchema`'s index signature (objectui#11355, part of the preparation for objectui#8347's removal of that signature). Each key was measured on its… (objectui `3c3ce15a7`) -- **minor** — Declare `pageSize` on `ObjectDataTableSchema`, on both faces (objectui#11348). (objectui `6c3da53ae`) -- **minor** — A form field of `type: 'grid'` declares the grid widget's field-level keys (objectui#11070, round 10). (objectui `edfcf5a5e`) -- **minor** — The grid field's `sort_field` is declared, and a master-detail detail's sort field is derived only (objectui#11070, round 9). (objectui `0a3e5409f`) -- **minor** — `formatMetadataError` and `formatMetadataIssue` are exported from `@object-ui/data-objectstack`: the one reader of a failed metadata save (objectui#11302). (objectui `d89329033`) -- **minor** — `object-map` publishes the three keys its `@objectstack/spec` 17.5.0 row declares and its registration left out: `mapStyle`, `navigation` and `enableClustering` (objectui#11168 sl… (objectui `20d23befe`) -- **minor** — `object-tree` publishes the keys its `@objectstack/spec` 17.5.0 row declares and its renderer honours (objectui#11168 slice 3, objectui#11111 decision 3 = B). Each key was measure… (objectui `20d23befe`) -- **minor** — `ObjectTreeSchema` mirrors the `object-tree` row of `@objectstack/spec` 17.5.0 (objectui#11168 slice 3). The change applies to both faces, TypeScript and zod. (objectui `20d23befe`) -- **minor** — `UIActionSchema.size` takes the `action:button` row's vocabulary by reference (objectui#11168 slice 3). Before this, the type was `'sm' | 'md' | 'lg'`. That made `size: 'default'`… (objectui `20d23befe`) -- **minor** — Eight renderers stop riding `BaseSchema`'s index signature for node keys their types did not declare (objectui#11347, the `@object-ui/components` preparation for objectui#8347's r… (objectui `c82ff391f`) -- **minor** — **BREAKING (rendering):** a dataset-bound dashboard widget no longer reads `chartConfig.series`, `chartConfig.xAxis` or `chartConfig.yAxis` (objectui#11315). (objectui `1a88ce22f`) -- **minor** — **BREAKING (authoring, TypeScript only):** on a dashboard widget, `chartConfig.type`, `chartConfig.xAxis`, `chartConfig.yAxis` and `chartConfig.series` are now compile errors, the… (objectui `1a88ce22f`) -- **minor** — The grid field reads each field-level key under the one spelling `GridFieldMetadata` declares (objectui#11070, round 8). (objectui `55a12a8e1`) -- **minor** — A region-tagged language code reaches the built-in catalogue of its base language (objectui#11326) (objectui `d0fba91aa`) -- **minor** — The grid field's `columns` is `@objectstack/spec`'s inline grid column list, by reference, and `object-chart` declares the per-element `dataSource` binding like the other gate-wra… (objectui `75dcc81c3`) -- **minor** — A custom page publishes the console's record navigator to the blocks placed on it (objectui#11293). (objectui `2124d0411`) -- **minor** — A standalone `object-calendar` honours `navigation: { mode: 'page' }`, and a `navigation` block written without `mode`, by opening the record page (objectui#11293). (objectui `2124d0411`) -- **minor** — A standalone `object-kanban` honours `navigation: { mode: 'page' }`, and a `navigation` block written without `mode`, by opening the record page (objectui#11293). (objectui `2124d0411`) -- **minor** — `useNavigationOverlay` hands an authored `page` click with no `onNavigate` to the record navigator the host publishes (objectui#11293). (objectui `2124d0411`) -- **patch** — fix(fields): a read-only number field shows its value the way its table cell does (objectui#11431) (objectui `52c95a166`) -- **patch** — fix(i18n): every count plural family carries every plural form its language uses (objectui#11432) (objectui `55d18c649`) -- **patch** — fix(plugin-dashboard): a dimensioned `pie` / `donut` / `funnel` / `treemap` / `sankey` widget with several measures now says which measures it drops (objectui#11417) (objectui `175df47ef`) -- **patch** — `AiUsageIndicator` renders the reset line for the rolling 5-hour pace window, `resetKind: 'fiveHour'` (objectui#11415, consumer of cloud#2059 / cloud#2574). (objectui `c681b9ff2`) -- **patch** — The `object-calendar` / `calendar` `navigation` input description said `openNewTab: true` "outranks the mode". That does not hold for `none`: `useNavigationOverlay` checks `mode =… (objectui `6cd5ae3ea`) -- **patch** — The `object-kanban` `navigation` input description said `openNewTab: true` "outranks the mode". That does not hold for `none`: `useNavigationOverlay` checks `mode === 'none'` befo… (objectui `6cd5ae3ea`) -- **patch** — fix(plugin-dashboard): a dimensionless `column` / `horizontal-bar` draws every measure; the dropped-measure warning speaks whenever the widget's own branch leaves a declared measu… (objectui `db0e9d3a0`) -- **patch** — fix(plugin-designer): the dashboard editor's type picker no longer turns a multi-measure widget into a type the widget door refuses (objectui#8894) (objectui `db0e9d3a0`) -- **patch** — A host feed slot written on a `record:activity` or `record:history` node is refused by name: `items` and `entries`, and the `loading` flag paired with each (objectui#11321). (objectui `e0a9c6760`) -- **patch** — fix(plugin-gantt): a number row in the gantt tooltip shows the field's declared decimals, and none when it declares none (objectui `c1763e50c`) -- **patch** — fix(fields): the number cell ignores a malformed `scale` instead of flooring it or crashing (objectui `c1763e50c`) -- **patch** — The dataset designer no longer writes `field: ''` for a row whose Field box is blank (objectui#11402). (objectui `0858267e4`) -- **patch** — fix(layout): the mobile tab bar draws its tabs in the sidebar's order, and shows an entry's badge (objectui `7728c67c8`) -- **patch** — docs(plugin-grid): authored `object-grid` examples write their props in the `properties` bag (objectui#11276) (objectui `6aa029b63`) -- **patch** — fix(app-shell): a refused metadata save shows the server's message and field path on every transport (objectui `d59f11c0d`) -- **patch** — fix(layout): the mobile tab bar draws only the entries its sidebar draws (objectui `5ad9f5dc8`) -- **patch** — fix(app-shell): a published html page that gains a plugin component can be published again from the Studio (objectui `3ae919307`) -- **patch** — fix(app-shell): a datasource created as External or Validate only, or switched to either from Managed, now saves without a credential (objectui#11368) (objectui `8001068b9`) -- **patch** — The Studio surfaces import `formatMetadataError` from `@object-ui/data-objectstack`, where the reader now lives (objectui#11302). What they show is unchanged; the publish-failure… (objectui `d89329033`) -- **patch** — `MetadataFieldsPage` shows the per-field prescription when the spec refuses a save, not only the refusal headline (objectui#11302). (objectui `d89329033`) -- **patch** — `object-map` reads `mapStyle` before `map.style`, as `@objectstack/spec`'s `object-map` row says in `mapStyle`'s own description ("Read before `map.style`"). This is objectui#1116… (objectui `20d23befe`) -- **patch** — The page-block inspector labelled the `object-form` `columns` field "Columns (grid layout)" in English and 「列数(网格布局)」 in Chinese. That pointed at the `grid` form layout, which obj… (objectui `20d23befe`) -- **patch** — The `object-timeline` / `view:timeline` `navigation` input description had three wording errors, and all three are corrected (objectui#11168 slice 3, from the contract record on o… (objectui `20d23befe`) -- **patch** — The README's "View tabs" section listed `form.layout` as `vertical | horizontal | inline | grid`. It now lists `vertical | horizontal`, the two values the form layout keeps after… (objectui `20d23befe`) -- **patch** — fix(plugin-gantt): a percent row in the gantt tooltip shows the field's declared decimals (objectui `b149617e6`) -- **patch** — fix(plugin-dashboard): the `object-metric` tile shows a percent or number aggregate at the field's declared width (objectui `b149617e6`) -- **patch** — fix(plugin-grid): the mobile card's percent value shows the field's declared decimals (objectui `b149617e6`) -- **patch** — fix(layout): the mobile tab bar opens the same page as the sidebar (objectui#11211) (objectui `c18a0754b`) -- **patch** — A bulk action whose `visible` is blank now shows on the grid's selection bar and runs over every selected record, as it already does on the row menu and the toolbars of the same g… (objectui `5638529e6`) -- **patch** — Docblock only: `BulkActionDef.visible` now says what the grid's selection bar does with an `ast`-only envelope (objectui#11322). (objectui `5638529e6`) -- **patch** — Docblock only, no behavior change: `partitionRowsByPredicate` now names its callers and says who decides "is a gate declared?" (objectui#11322). (objectui `5638529e6`) - -⚠️ 16 of these carry a breaking change: 16 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 6 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ docs(fields): the number page and catalog teach `scale` for the decimal width (objectui#11413) (#11429) (objectui `01f99e31e`) -- _(no changeset)_ docs(fields): the percent page and catalog teach `scale` for the decimal width (objectui#11255) (#11411) (objectui `549aaa831`) -- _(no changeset)_ docs(skills): the page-builder guide authors object-grid and object-gantt in the properties bag (objectui#10859; objectui#11276 rider) (#11404) (objectui `64c173d70`) -- _(no changeset)_ docs(skills): the mobile guide teaches mobileNavMode where it is read, not on the app schema (objectui#11363) (#11397) (objectui `abca9867e`) -- _(no changeset)_ fix(site): move next 16.3.3 to 16.3.6 for GHSA-vcvr-r3jv-pc5j (critical) (#11361) (objectui `ad58cc159`) -- _(no changeset)_ docs(guide): slotted-pages header example keeps only PageHeaderProps keys; pin it (objectui#11165) (#11339) (objectui `743181a48`) - - - -objectui range: `31971ff1e28f...89cad75d5570` diff --git a/.changeset/console-9dfaca654311.md b/.changeset/console-9dfaca654311.md deleted file mode 100644 index 48c2c542cb9..00000000000 --- a/.changeset/console-9dfaca654311.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `9dfaca654311`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 24 releasing of 24 changesets added across 17 non-merge commits. - -- **minor** — **BREAKING** — The default (`simple`) `object-form` draws a self-describing inline section entry, as the `tabbed`, `wizard`, `split`, `drawer` and `modal` forms already did (objectui#11615). Bef… (objectui `9dfaca654`) -- **minor** — The `record:related_list` registration no longer declares `columns` required, so the page compile accepts a related list that lists no columns of its own (objectui#11613). (objectui `4c127cdef`) -- **minor** — The record page header draws the record's picture beside its title, from the field the object names in its object-level `imageField` (`@objectstack/spec` 17.6.0, objectstack#21182… (objectui `c096f0327`) -- **minor** — **BREAKING** — A form view's `subforms[].columns` entry is judged by `@objectstack/spec`'s `InlineGridColumnSchema` now, by reference, so `objectui validate` and `os validate` give one verdict o… (objectui `9db9ff3f9`) -- **minor** — **BREAKING — `PartialSchema` is RETIRED from `@object-ui/types`** (objectui#11608, enforce-or-remove). The utility type leaves the `.` entry, the one entry that published it, w… (objectui `8b14aecbd`) -- **minor** — **BREAKING** — The `grid` field's eight field-level keys are camelCase now, and their snake_case spellings are retired and refused by name on every face (objectui#11610). (objectui `2abec3a96`) -- **minor** — **`BaseSchema` no longer declares `[key: string]: any`** (objectui#8347, executing the objectui#7927 ruling: the TypeScript face is a contract). Every node type extends `BaseSchem… (objectui `b403bb36f`) -- **minor** — All ten locale packs gain `view.noObject`, the hint an object-bound block shows when its node names its object in neither place (objectui#11605). (objectui `fd060f076`) -- **minor** — The `object-chart` and `view:chart` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object, and a ch… (objectui `fd060f076`) -- **minor** — The `object-metric` and `object-pivot` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object, and a… (objectui `fd060f076`) -- **minor** — The `object-form`, `view:form`, `embeddable-form` and `object-master-detail-form` registrations no longer declare `objectName` required, so the page compile accepts a node whose `… (objectui `fd060f076`) -- **minor** — The `object-grid` and `view:grid` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object (objectui#1… (objectui `fd060f076`) -- **minor** — The `object-kanban` registration no longer declares `objectName` required, so the page compile accepts a board whose `dataSource` binding names the object, and a board that names… (objectui `fd060f076`) -- **minor** — The `list-view` and `view:list` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object, and a list t… (objectui `fd060f076`) -- **minor** — `ElementDataSourceGate` takes a `requiresObject` prop: when a placement opts in and its node names its object in neither place, the gate renders a short "no object named" hint ins… (objectui `fd060f076`) -- **minor** — A Studio form field that declares the `ref:dataset` widget now renders a dataset picker instead of the JSON editor fallback (objectui#11601). (objectui `b508ac50d`) -- **minor** — The console asks `GET /api/v1/usage/storage` only when the runtime serves it (objectui#11002). On a self-hosted or open-source runtime, an environment admin's console used to requ… (objectui `d2e859936`) -- **minor** — The `record:line_items` registration no longer declares `childObject` required, so the page compile accepts a node whose `dataSource` binding names the child object (objectui#1156… (objectui `902ebab63`) -- **patch** — `deriveColumns`, the default columns of a master-detail inline grid whose author listed none, now takes which columns it draws, their order and which of them are `defaultHidden` f… (objectui `15f67025b`) -- **patch** — An object page's view tab, and the breadcrumb that names the open view, draw the label of a view the object document embeds as the server served it (objectui#11336). (objectui `6e9090c26`) -- **patch** — The docs portal's book sidebar now shows what the book resolver answers, with nothing narrowing the docs in front of it (objectui#11340, ADR-0046 §6.4). The resolver decides book… (objectui `7c9a6b194`) -- **patch** — Studio's "Organization flows" page no longer says its drafts publish atomically, and a deep link to a flow that is not on the page no longer says no metadata designers are registe… (objectui `b92329c89`) -- **patch** — On a read-only package, a click on a flow canvas node in Studio Automations selects the node and opens its inspector read-only again (objectui#11546). (objectui `278d2444e`) -- **patch** — fix(plugin-detail): `record:details` read mode shows a `textarea` value with its line breaks (objectui#11577) (objectui `b61c116b2`) - -⚠️ 4 of these carry a breaking change: 4 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - - - -objectui range: `2e818d0b51ec...9dfaca654311` diff --git a/.changeset/console-ab1879721595.md b/.changeset/console-ab1879721595.md deleted file mode 100644 index e50a91e91dd..00000000000 --- a/.changeset/console-ab1879721595.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `ab1879721595`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 111 releasing of 119 changesets added across 65 non-merge commits; omitted: 8 release-nothing changesets, 1 commit carrying no changeset (they ship no package code). - -- **minor** — Studio reaches the organization's own flows that belong to no package (objectui#11553). (objectui `f624f278d`) -- **minor** — `record:line_items` now publishes every key of its `@objectstack/spec` 17.6.0 row (objectui#11536). Each key was decided by measuring it through `SchemaRenderer` and the block's r… (objectui `072b7e843`) -- **minor** — The import wizard's "Download template" button now downloads the server's import template, an Excel workbook, instead of building a CSV of every field (objectui#9600). (objectui `b253c4e28`) -- **minor** — The Studio dataset filter builder writes "Is empty" / "Is not empty" as the spec's `{ FIELD: { $empty: true } }` / `{ FIELD: { $empty: false } }` instead of `$exists` (objectui#10… (objectui `d0c0c7fe9`) -- **minor** — `FilterConditionField` writes "Is empty" / "Is not empty" as the spec's one 「is empty」 operator, `{ FIELD: { $empty: true } }` / `{ FIELD: { $empty: false } }` (objectui#10813). (objectui `d0c0c7fe9`) -- **minor** — The list view's live query sends "Is empty" / "Is not empty" as the spec's `isempty` / `isnotempty` instead of an equality to `null` (objectui#10813). `@objectstack/spec` 17.6.0 a… (objectui `d0c0c7fe9`) -- **minor** — An `object-grid` honours `keyboardNavigation`: arrow-key cell navigation on the WAI-ARIA grid pattern (objectui#11068). `@objectstack/spec` 17.6.0 declares the key on its `object-… (objectui `154075ab1`) -- **minor** — The action success toast is composed from the action's `outcomeMessages`, then its `successMessage`, then the runner's default text. A `message` in the server's answer is no longe… (objectui `c476be0e0`) -- **minor** — The drill `filter[...]` URL dialect can spell "is empty" (objectui#11547). (objectui `7121221fa`) -- **minor** — `ValueDataSource` executes the empty pair `is_empty` / `is_not_empty` and the `$empty` operator, and `convertFiltersToAST` lowers `$empty` (objectui#11094). (objectui `6158e4c93`) -- **minor** — An `object-grid` publishes `description` and `emptyState` now that `@objectstack/spec` 17.6.0 declares them on its `object-grid` row, and `emptyState.title` / `.message` take an i… (objectui `6158e4c93`) -- **minor** — **BREAKING for authors of `object-grid` and `list-view` row rules, released as `minor`.** `conditionalFormatting` on `ObjectGridSchema` (and so on the `object-view` `table` slot b… (objectui `6f5719e1c`) -- **minor** — **BREAKING** — **A dashboard metric widget bound inline to an object now shows the retired-format prompt instead of its number (objectui#11525).** This follows the maintainer's ruling C on objec… (objectui `160c6c6ea`) -- **minor** — **BREAKING for authors of `object-kanban` card rules, released as `minor`.** `object-kanban`'s `conditionalFormatting` takes ONE rule dialect, the spec list view's `{ condition, s… (objectui `c73cdb569`) -- **minor** — `DashboardRenderer`'s `onWidgetsReorder` hands back the slot's own array type, `DashboardComponentSchema['widgets']`, instead of `DashboardWidgetSchema[]`; the dashboard's `object… (objectui `9d7419b91`) -- **minor** — `DashboardWidgetSchema['type']` names the widget vocabulary only, `DashboardWidgetTypeName`: it drops `DashboardComponentWidgetType` (objectui#11514). This narrows the TypeScript… (objectui `9d7419b91`) -- **minor** — fix(plugin-charts): an `object-chart` whose `specType` is a single-value or tabular spec family draws its routed form, not a silent bar chart; the renderer has no default family (… (objectui `06634afe7`) -- **minor** — **BREAKING** — `DrillDownConfig.report`'s `{ name }` reference arm is retired on the TypeScript face, the tolerant zod face and the strict authoring face, and both zod faces refuse it by name (o… (objectui `9ed8d0f1c`) -- **minor** — **BREAKING** — The drill-down drawer scopes a dataset-bound drill report by `runtimeFilter`, and the pre-9.0 object-bound drill report is retired (objectui#11506). (objectui `8366accd1`) -- **minor** — `@object-ui/types` declares TypeScript types for three node types its zod face already validates: `DetailSectionNodeSchema` (`detail-section`), `AppSchemaRendererNodeSchema` (`app… (objectui `fc7db059f`) -- **minor** — feat(types): `ObjectChartSchema.chartType` declares the `@objectstack/spec` chart families plugin-charts draws (objectui#11513) (objectui `95e58a3b2`) -- **minor** — **BREAKING** — A `grid` node sets a column count per breakpoint in one way: the breakpoint object of `columns`. The flat `smColumns`, `mdColumns`, `lgColumns` and `xlColumns` keys are retired, w… (objectui `2d576e46e`) -- **minor** — **BREAKING (`@object-ui/cli`):** four commands are retired: `objectui create`, `objectui lint`, `objectui test` and `objectui studio`. There is no alias window and no placeholder:… (objectui `37268aae9`) -- **minor** — **BREAKING** — chore(console)!: drop the lazy `spec-report` stub (objectui#11440) (objectui `9d9ed5495`) -- **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops `spec-report`, which objectui#11440 retired (objectui `9d9ed5495`) -- **minor** — feat(types): the `report` node declares the `report` member that wraps a spec report (objectui#11440) (objectui `9d9ed5495`) -- **minor** — **BREAKING** — refactor(plugin-report)!: retire the `spec-report` node type key; a spec report is embedded as `{ "type": "report", "report": { … } }` (objectui#11440) (objectui `9d9ed5495`) -- **minor** — **BREAKING** — An `app-schema-renderer` node draws the app document it carries under `schema` (objectui#11494, triage ruling A). (objectui `fcdc8ec91`) -- **minor** — `AppSchemaRendererNodeSchema` declares the `app-schema-renderer` node's `schema` input: it is the app document, `AppComponentSchema` itself, by reference, and optional (objectui#1… (objectui `fcdc8ec91`) -- **minor** — **BREAKING** — The `columns` of a `grid` node is one of the counts its renderer maps, 1 to 12, as the bare number and at every breakpoint of the object form. Any other count is refused at valida… (objectui `aea682a31`) -- **minor** — **BREAKING (`@object-ui/cli`):** the `objectui add` command is retired, and so are `objectui analyze`'s two flags, `--render-performance` and `--bundle-size`. There is no alias wi… (objectui `ea3914139`) -- **minor** — A `metric-card` in a dashboard's widget slot parses only with its `value` (objectui#11483). This narrows a published accept set on both validator faces, and it narrows the widget… (objectui `f68e0a080`) -- **minor** — **BREAKING (`@object-ui/cli`):** `objectui generate` no longer accepts `--from`. The flag is retired, with no alias window and no placeholder, and the CLI now refuses it as an unk… (objectui `1fe05ff37`) -- **minor** — **BREAKING** — feat(types): `ObjectGridSchema`'s zod mirror declares ten members its TypeScript twin declares (objectui#6152, round 6) (objectui `0d723a33f`) -- **minor** — `safeValidateSchema` — and so `objectui validate` — accepts seven more registered node types: the spec page kinds `record`, `home` and `utility`, and `app-schema-renderer`, `objec… (objectui `00ccdf742`) -- **minor** — The `object-pivot` registration publishes `drillDown` as an input (objectui#11440). (objectui `00ccdf742`) -- **minor** — **BREAKING** — The `gap` of a `stack`, a `flex` and a `grid` node is one of the steps its renderer maps. Any other number is refused at validation, with the set named (objectui#11474). (objectui `4abc0aafa`) -- **minor** — Dashboard percent faces read the storage they are told, never a storage guessed from the value (objectui#11475). (objectui `f560ded15`) -- **minor** — **BREAKING: a percentage is scaled at the storage its field declares, never at a storage guessed from the value (objectui#11475)** (objectui `f560ded15`) -- **minor** — **BREAKING (`@object-ui/cli`):** `objectui generate` no longer accepts `--output`. The flag is retired, with no alias window, and the CLI now refuses it as an unknown option (obje… (objectui `9de0b3483`) -- **minor** — A `metric-card` in a dashboard's widget slot is checked against the props `MetricCard` renders, on the TypeScript face and in the validator (objectui#11467). This narrows a publis… (objectui `401611b21`) -- **minor** — `AnySchema` now includes the node types this package declares and exported outside it, so `SchemaByType` and narrowing on `type` reach them (objectui#11478). (objectui `2b188faa3`) -- **minor** — **BREAKING** — feat(types)!: `SidebarSchema` declares what the `sidebar` node draws — nine unread keys are retired on both faces, and `variant` takes the registration's enum (objectui#11465) (objectui `ca3de7272`) -- **minor** — The authored `properties`-bag carriers outside the spec's public blocks have a TypeScript authoring type, and `AuthoringNode` includes them, so `SchemaRenderer`'s `schema` prop ac… (objectui `2c0ddf226`) -- **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the ten `sidebar-*` node type keys objectui#10859 batch 8 phase 2d retired (objectui `1c8403692`) -- **minor** — **BREAKING** — refactor(components)!: retire the ten `sidebar-*` node type keys; the `sidebar` node supplies its own provider (objectui#10859, batch 8 phase 2d) (objectui `1c8403692`) -- **minor** — feat(components): the `sidebar` node mounts a `SidebarProvider` only when none is above it, and honours the boolean `collapsible` (objectui#10859, batch 8 phase 2d) (objectui `1c8403692`) -- **minor** — `input-otp` draws the separator its docs page teaches (objectui#11365). The "With Separator" example on the `input-otp` docs page and two catalog entries (`with-visual-separator`,… (objectui `e46ee770b`) -- **minor** — **BREAKING** — A `container` node's `padding` is one of the twelve steps its renderer maps: 0 to 8, 10, 12 and 16. Any other number is refused at validation, with the set named (objectui#11424). (objectui `3f6efd640`) -- **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the two node type keys objectui#11441 retired (objectui `9d1c0bff9`) -- **minor** — **BREAKING** — refactor(layout)!: retire the `navigation-renderer` and `responsive-grid` node type keys; navigation is application metadata, and the breakpoint grid is `grid` (objectui#11441) (objectui `9d1c0bff9`) -- **minor** — **BREAKING** — The page and report designers, and all three canvases, draw the members their node declarations always carried and they never read. Every designer registration's `inputs` now list… (objectui `5988b6b53`) -- **minor** — **BREAKING (authoring)** — a report element's `properties.field` is refused by name on the zod face; the element's binding is its declared `dataBinding` (objectui#11434, ADR-0049). (objectui `5988b6b53`) -- **minor** — **BREAKING** — chore(console)!: drop the lazy `pie-chart`, `donut-chart` and `radar-chart` stubs (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) -- **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the four node type keys objectui#10859 batch 8 phase 2c retired (objectui `ad1785c1d`) -- **minor** — **BREAKING** — refactor(components)!: drop `page-header` from the opt-in protocol placeholders (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) -- **minor** — **BREAKING** — refactor(plugin-charts)!: retire the `pie-chart`, `donut-chart` and `radar-chart` node type keys; the families are `chart` + `chartType` (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) -- **minor** — **BREAKING** — refactor(layout)!: retire the `page-header` node type key; the header node is `page:header` (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) -- **minor** — **BREAKING (authoring)** — `DataModelRelationship.onDelete` is respelled `deleteBehavior`, in `@objectstack/spec`'s vocabulary, on both faces; and the process designer's last two… (objectui `0e9058b95`) -- **minor** — The data-model and process designers draw the members their node declarations always carried and they never read (objectui#11434). (objectui `0e9058b95`) -- **minor** — **BREAKING** — chore(console)!: drop the lazy `scatter-chart` and `dashboard-grid` stubs, and stop the `metric` / `metric-card` stubs claiming the bare key (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-dashboard)!: retire the `dashboard-grid` node type key, and register the `metric` / `metric-card` node keys without a bare fallback (objectui#10859, batch 8 phase… (objectui `37140f4f5`) -- **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the twelve node type keys objectui#10859 batch 8 phase 2b retired (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-designer)!: retire four builder-chrome node type keys (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-form)!: retire the `form-analytics` node type key (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-grid)!: retire the `import-wizard` node type key; the `ImportWizard` export stays (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-detail)!: retire the `related-list` node type key; the related-list block is `record:related_list` (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-charts)!: retire the `scatter-chart` node type key; scatter is `chart` + `chartType: 'scatter'` (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING** — refactor(plugin-view)!: retire the `shared-view-link` node type key (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) -- **minor** — **BREAKING (authoring)** — seven members of the `@object-ui/plugin-designer` node declarations and their record types are retired on both faces, and two element types leave the pa… (objectui `c4ab6d09a`) -- **minor** — **BREAKING (TypeScript props)** — `DataModelDesignerProps.autoLayout` and `ReportDesignerProps.previewMode` are removed, and the `data-model-designer` registration no longer publi… (objectui `c4ab6d09a`) -- **patch** — `record:details` edit mode edits a `textarea` field in a multi-line textarea, so saving it keeps its line breaks (objectui#11562). (objectui `ab1879721`) -- **patch** — The top-level `fields` input descriptions of `object-form`, `view:form`, `embeddable-form` and `object-master-detail-form`, and the `console.warn` a top-level `fields` member that… (objectui `dbd108166`) -- **patch** — A grouped `object-grid` labels its group headers from the object field's `options` only; a column's `options` no longer relabels them (objectui#11544). (objectui `b0bf413ca`) -- **patch** — `record:details` edit mode gives a `markdown` field an editor: a multi-line textarea (objectui#11541). (objectui `b34cc148e`) -- **patch** — The record dialog now draws a `form.sections[].group` section (objectui#11542). (objectui `584eecae8`) -- **patch** — fix(app-shell): create and edit no longer render a container's first named form when it declares no default form (objectui `9bfd0b36e`) -- **patch** — Raise `@object-ui/types`' declared `@objectstack/spec` floor from `^17.5.0` to `^17.6.0` (objectui#11227). The package's published types now read `EmptyState` from `@objectstack/s… (objectui `6158e4c93`) -- **patch** — objectui now resolves `@objectstack/*` 17.6.0 (objectui#11438). One declared range moves: `@object-ui/types` raises its `@objectstack/spec` floor to `^17.6.0` in objectui#11227's… (objectui `6158e4c93`) -- **patch** — fix(plugin-charts): an `object-chart` whose family is on `specType` gets `compareTo` exactly as the same family on `chartType` does, so a `specType: scatter` chart with `compareTo… (objectui `6837bfa85`) -- **patch** — fix(plugin-dashboard): an `object-metric` with a structured `groupBy` and a rule-list `filter` draws its number (objectui `8bfc0012e`) -- **patch** — The metadata-admin dashboard preview's reorder handler takes `DashboardComponentSchema['widgets']`, the array `DashboardRenderer`'s `onWidgetsReorder` now hands back (objectui#115… (objectui `9d7419b91`) -- **patch** — The dashboard editor reads a `widgets[]` entry by the slot's element type, `DashboardComponentSchema['widgets'][number]`, in its widget card, its property panel, its preview and i… (objectui `9d7419b91`) -- **patch** — The README's Activity tab authors `record:activity`'s declared inputs in `properties` (`{ limit: 20, showCompleted: false }`) instead of a host feed in `items` (objectui#11515). (objectui `fc7db059f`) -- **patch** — The VS Code extension's **Export to React** command types the `schema` constant it emits as `SchemaRendererProps['schema']`, imported type-only from `@object-ui/react` beside `Sch… (objectui `fc7db059f`) -- **patch** — The generated plugin's `jsdom` devDependency is now `^29.1.1` instead of `^30.0.1` (objectui#11366). (objectui `059bf1b59`) -- **patch** — The drill-down drawer renders a `drillDown.report` as a `report` node, `{ type: 'report', report }` (objectui#11440). (objectui `9d9ed5495`) -- **patch** — `toRenderableSchema` declares its parameter as `SchemaNode` (objectui#11479). (objectui `b654d4ed5`) -- **patch** — The record header's percent summary chip, text and bar, scales at the storage its field declares (`percentCellScale`, the spec's `percentScaleOf`: a fraction unless the field decl… (objectui `f560ded15`) -- **patch** — The gantt tooltip's `percent` row scales at the storage the field declares (`percentCellScale`, the spec's `percentScaleOf`), the answer the list cell reads. A fraction-stored `1`… (objectui `f560ded15`) -- **patch** — Grid percent faces read the field's declared storage (objectui#11475). (objectui `f560ded15`) -- **patch** — The gallery card's field bag now carries the field's `max`, the storage statement the percent cell reads through the spec's `percentScaleOf` (objectui#11475). Without it, a whole-… (objectui `f560ded15`) -- **patch** — fix(components): an expanded `offcanvas` sidebar is drawn inside the viewport (objectui#11464) (objectui `50c73fed0`) -- **patch** — `objectui generate page NAME` writes a page that draws its title (objectui#11450). (objectui `5429e6c74`) -- **patch** — The runner drops a fallback welcome page that could never render (objectui#11450). (objectui `5429e6c74`) -- **patch** — fix(fields): a read-only percent or currency field shows its value the way its table cell does, and a whole currency amount keeps its minor units (objectui#11444) (objectui `6007dd4e4`) -- **patch** — fix(approvals): the console names a position approver `position:manager`, the spelling the server stores — not `role:manager` (objectui#11455) (objectui `d93e53f5d`) -- **patch** — The search results line and the object view's record-count footer read the right noun form in every language at every count (objectui#11445). Each passes `count` to one i18next co… (objectui `4a1adb76e`) -- **patch** — The comment thread's header, its reaction tooltip and the presence stack's labels read the right noun form in every language at every count (objectui#11445). Each passes `count` t… (objectui `4a1adb76e`) -- **patch** — The data table's "N rows modified" line and the `page:tabs` count badge's accessible name read the right noun form in every language at every count (objectui#11445): both are i18n… (objectui `4a1adb76e`) -- …and 11 more releasing changesets in this range (list capped at 100; see the objectui range below). - -⚠️ 43 of these carry a breaking change: 43 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 1 commit in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared it, so it appears in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ docs(plugin-dashboard): the charts example authors a line widget bound to a dataset; the card widget with nested children retires (objectui#11484) (#11523) (objectui `6903eafbc`) - - - -objectui range: `89cad75d5570...ab1879721595` diff --git a/.changeset/objectui-pin-citations-0abd4f9f8769.md b/.changeset/objectui-pin-citations-0abd4f9f8769.md deleted file mode 100644 index db4688e9052..00000000000 --- a/.changeset/objectui-pin-citations-0abd4f9f8769.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `0abd4f9f8769`. - -Clause-②: no - -Every anchor was mapped through the objectui diff `9dfaca654311..0abd4f9f8769`, 50 paths over five commits. None of those paths is an objectui file that an asserting record cites, so every cited file is byte-identical across the hop (`git diff --quiet`) and every anchor held unmoved. The seven quoted anchor lines verify against objectui at the new pin. Three records carry a count, and each count was re-taken by its record's own method with the same reading: the `keyboardNavigation` hit lines (15, against 3 for the `schema.editable` control), `ObjectKanban.tsx`'s `quickAdd` / `onQuickAdd` (2 each, against 11 for `onCardClick`), and the `ElementDataSourceGate` occurrences in five `src/index.tsx` shells (0, 3, 3, 3 and 4). - -The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that first reproduced every `9dfaca654311` number. The corpus is now 7650 tracked files. All 98 checked tokens read as before: every zero still reads zero, and `Span` / `SpanSchema` still read 508 / 57. - -No key, default, enum member or export moves. diff --git a/.changeset/objectui-pin-citations-2e818d0b51ec.md b/.changeset/objectui-pin-citations-2e818d0b51ec.md deleted file mode 100644 index 53db8a30350..00000000000 --- a/.changeset/objectui-pin-citations-2e818d0b51ec.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `2e818d0b51ec`. - -Clause-②: no - -Every anchor was mapped through the objectui diff `ab1879721595..2e818d0b51ec`. Every file a current anchor cites is byte-identical across the hop except four, and in those the cited text is byte-identical too: - -- `plugin-dashboard/src/index.tsx`: objectui#11466 added lines above the `object-metric` registration, so the `object-metric` icon input record moves from `:269` to `:281`. It is still `{ name: 'icon', type: 'string' }`. -- `packages/types/src/objectql.ts`: objectui#11216 declared `grouping` on `ObjectKanbanSchema` below the cited `limit` member. -- `plugin-kanban.mdx`: objectui#11216 added a `grouping` row below the cited `limit` row. -- `SchemaRenderer.tsx`: objectui#11466 changed the type of `schema`. Its `properties.*` hoist and `createElement` spread are unchanged. - -The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that reproduces the previous pin's numbers. objectui's 17.7.0 release removed 2726 consumed changesets, so the corpus is now 7579 tracked files. Every zero still reads zero. - -No key, default, enum member or export moves. diff --git a/.changeset/objectui-pin-citations-89cad75d5570.md b/.changeset/objectui-pin-citations-89cad75d5570.md deleted file mode 100644 index 84955df50f2..00000000000 --- a/.changeset/objectui-pin-citations-89cad75d5570.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `89cad75d5570`. - -Clause-②: no - -Several records were corrected rather than moved, because objectui changed what they describe on this hop: `object-map` now reads `mapStyle` ahead of `map.style` on the declared-block path as well (objectui#11168 slice 3), so the `getMapConfig` return quote is rewritten; `object-tree`'s `navigation` read and `@object-ui/types`' `ObjectTreeSchema` mirror now carry the spec row's keys with no cast, and objectui's record-source table no longer lists the retired bare `tree` / `view:tree` keys (objectui#10859 batch 8); `object-map`, `object-gantt` and `object-timeline` publish the further keys their rows declare (objectui#11168 slices 3–5). Two stale `object-timeline` anchors (`filter` and `variant`) that were already one line off at the previous pin are corrected. No key, default, enum member or export moves. diff --git a/.changeset/objectui-pin-citations-9dfaca654311.md b/.changeset/objectui-pin-citations-9dfaca654311.md deleted file mode 100644 index 003051c1e93..00000000000 --- a/.changeset/objectui-pin-citations-9dfaca654311.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `9dfaca654311`. - -Clause-②: no - -Every anchor was mapped through the objectui diff `2e818d0b51ec..9dfaca654311` and re-read at the new pin. One cited line changed: `ObjectKanban.tsx:10`, the type import, gained `SortConfig` beside the `ObjectKanbanSchema` the record cites. Every other change in a cited file sits outside the cited lines, and the anchors moved with their text byte-identical: - -- `plugin-grid/src/ObjectGrid.tsx`, `plugin-tree/src/ObjectTree.tsx`, `plugin-gantt/src/ObjectGantt.tsx`, `plugin-calendar/src/ObjectCalendar.tsx`, `react/src/SchemaRenderer.tsx`, `plugin-map/src/index.tsx`, `plugin-gantt/src/index.tsx` and `plugin-view/src/ObjectView.tsx`: objectui#8347 re-worded docblocks that described `BaseSchema`'s index signature. Anchors below those docblocks moved by at most three lines. -- `plugin-kanban/src/ObjectKanban.tsx`: objectui#8347 added a private `GateBoundKanbanSchema` read type above the board's fetch, so the fetch, the navigation reads and the spread into `KanbanBoardCore` moved by 30 lines. The board still reads `limit` and `navigation` as `ObjectKanbanSchema` declares them. -- `plugin-kanban/src/index.tsx`, `plugin-dashboard/src/index.tsx` and `react/src/element-data-source/ElementDataSourceGate.tsx`: objectui#11605 made `objectName` a non-required input and added the "no object named" hint. The `object-metric` icon input moved from `:281` to `:299`, and it is still `{ name: 'icon', type: 'string' }`. -- `components/src/renderers/layout/containers.tsx`: objectui#11619 added the record picture to the record chrome. The `page:tabs` and `page:accordion` icon anchors moved by three lines. -- `packages/types/src/objectql.ts` and `packages/types/src/zod/objectql.zod.ts`: objectui#11615, objectui#11266 and objectui#8347 grew declarations above the cited members. `ObjectKanbanSchema.limit`, `ObjectMapConfigSchema` and `LIST_VIEW_LOCAL_OVERRIDES` moved with their text byte-identical. - -The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that first reproduced every `2e818d0b51ec` number. The corpus is now 7632 tracked files. Every zero still reads zero; the three new `Span` hits are a `colSpan` in an objectui test. - -No key, default, enum member or export moves. diff --git a/.changeset/objectui-pin-citations-ab1879721595.md b/.changeset/objectui-pin-citations-ab1879721595.md deleted file mode 100644 index af4572da4a6..00000000000 --- a/.changeset/objectui-pin-citations-ab1879721595.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `ab1879721595`. - -Clause-②: no - -Several records were corrected rather than moved, because objectui changed what they describe on this hop. - -- **`object-grid` `keyboardNavigation`.** The grid now reads the key (objectui#11068), so its describe drops the `[EXPERIMENTAL — not enforced]` marker and the sentence that said no renderer reads it and authoring it changes nothing. The describe now says what the grid does: the data cells become one Tab stop that the arrow keys, Home / End and Ctrl+Home / Ctrl+End move between. It is on by default when the grid renders editable, `true` turns it on for a read-only grid, and `false` turns it off on an editable one. -- **`object-grid` `emptyState`.** Its record now says the grid resolves `title` and `message` against the display locale (objectui#11227), so an inline locale map draws. -- **`ActionSchema.outcomeMessages`.** The console reader landed (objectui#11344). The key's liveness row is now `live`, and authoring it no longer draws the "the console does not show outcome copy yet" author warning. The `successMessage` row records that `${result.*}` is interpolated. -- **The four `action:*` rows.** The action renderers forward `outcomeMessages` to the action runner. The `action:button` and `action:icon` rows record it as a key the renderer forwards and the row does not declare. The `action:group` and `action:menu` rows record that each member's own `outcomeMessages` rides the member forward. - -Every other anchor either held on a byte-identical file or moved with its cited text byte-identical. The corpus counts in the six migration entries were re-taken with the method that reproduces the previous pin's numbers. No key, default, enum member or export moves. diff --git a/.changeset/public-form-withdrawal-kill-switch.md b/.changeset/public-form-withdrawal-kill-switch.md deleted file mode 100644 index b9fe5f02836..00000000000 --- a/.changeset/public-form-withdrawal-kill-switch.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/rest': patch -'@objectstack/metadata-protocol': patch -'@objectstack/metadata-core': minor ---- - -A public form's explicit intake withdrawal at any metadata layer now holds: layering can only narrow anonymous intake, never re-open it - -Clause-②: yes (widening) - -- **What counts as a withdrawal.** A withdrawal keeps the form's `publicLink` and sets `sharing.enabled: false` or `sharing.allowAnonymous: false`. Only an explicit `false` counts: a switch that is absent is not a withdrawal. Removing the `sharing` block, clearing the `publicLink`, or deleting the view at one layer is not a withdrawal either. A sharing that names no public link withdraws nothing. -- **Organization-scoped saves and publishes.** A `view` save or draft promotion in the organization the anonymous form doors read is refused with `403 NOT_OVERRIDABLE` if it would leave open a form that the environment-wide definition withdraws. This check judges by the stored row: the organization's body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package's artifact), and also with the env-wide view list the way the doors read it (a container-shaped body is expanded the way the list read expands it). Inside the row, a withdrawn form matches by its place (`form`, the same `formViews` entry, or `config`) or by its public slug, and either match is enough. So a renamed `formViews` key, a `form.name`, a move to another place, a listViews collision rename in the expansion, and a new or re-cased slug are all judged as the same form. A form that differs from every withdrawn form in both place and slug, such as a sibling in the same container, stays independent. The check also covers an organization copy that was already open before the withdrawal, the next time it is saved. The message names the remedies: save the overlay withdrawn, or publish the form from its environment-wide definition. An organization-scoped save that keeps the form withdrawn is still accepted. -- **Anonymous form doors.** `GET /forms/:slug` and `POST /forms/:slug/submit` judge by the name of the view item they serve. Beneath the organization's read they read the env-wide view list, and they serve a form only when the env-wide item of the same name does not explicitly withdraw a form in the same place or with the same slug. A withdrawn form answers `404 FORM_NOT_FOUND` on both doors and creates no record. A form that is open at every layer is served as before. A form that only an organization carries is still served there. A different view that uses the same slug is a different form, and the two never close each other. -- **Package-shipped forms.** A package's form is part of the env-wide definition, not a separate layer beneath it. A package artifact that was parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal (fail closed). An artifact that reached the runtime without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: there a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch: an env-wide save may open a form the package ships closed. -- **Known limit: packages and names.** A withdrawal of a view name closes that name in every package. When two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name: it may over-close, never under-close. Per-package precision is tracked in #21934. A publish judges the draft it promotes under the same package key: with two packages holding a draft of the same view in one organization, each draft is judged on its own publish. -- **Known limit.** The doors match by served item name, and the save check runs only on an organization-scoped save or publish. An organization overlay that was stored before the env-wide withdrawal, or that a rollback or commit-revert restores, can still be served if it keeps the form open under a different key or place than the env-wide definition. Withdraw the form in that overlay to close it. Rollback and commit-revert restores are not gated by the save check. -- **Behaviour change.** Between 17.6.0 and this fix, an organization overlay that published a form the environment-wide (package) definition withdrew was honoured: the doors served the organization's copy. That behaviour never shipped in a release, and it is reversed on purpose. The environment-wide withdrawal now wins. -- **`@objectstack/metadata-core`** exports the shared judgement `anonymousFormIntakeWithdrawnIn` (a new, additive public export). Both the doors and the save path read it. diff --git a/.changeset/public-form-withdrawal-one-rule.md b/.changeset/public-form-withdrawal-one-rule.md deleted file mode 100644 index 5c7cadc15f0..00000000000 --- a/.changeset/public-form-withdrawal-one-rule.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/metadata-core': minor -'@objectstack/metadata-protocol': patch -'@objectstack/rest': patch ---- - -Public forms: every declared means of withdrawing a form from anonymous intake is now honoured by every anonymous form door. Which forms a `view` opens to anonymous intake is now decided by one rule, `anonymousFormIntakeCandidates` (new in `@objectstack/metadata-core`, alongside `anonymousFormIntakeSlugs`, `anonymousFormIntakeSlug` and `publicFormSlug`), read by both the anonymous form endpoints in `@objectstack/rest` and the organization-scoped `view` write check in `@objectstack/metadata-protocol`, so the two can no longer disagree. A form is served anonymously only when its `sharing` config declares public sharing as `SharingConfigSchema` defines it: `sharing.enabled: true`, `sharing.allowAnonymous: true` and a `sharing.publicLink` slug. `enabled` defaults to `false`, so a form that set only `allowAnonymous` and `publicLink` is no longer served on the anonymous endpoints (`404 FORM_NOT_FOUND`). Migration: add `enabled: true` to the form's `sharing` block (and to any stored overlay of it) to keep it public; see the public forms guide. diff --git a/.changeset/record-change-payload-credential-mask.md b/.changeset/record-change-payload-credential-mask.md deleted file mode 100644 index 1ca54a8783c..00000000000 --- a/.changeset/record-change-payload-credential-mask.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/objectql': patch -'@objectstack/plugin-approvals': patch -'@objectstack/plugin-webhooks': patch -'@objectstack/service-knowledge': minor ---- - -Record-change payloads apply the same credential mask and internal-field omission as write responses. - -Clause-②: yes (widening) - -- **`data.record.created` / `data.record.updated` events.** The engine projects the event's `after` and `changes` bodies through `omitInternalFieldsFromWriteResponse` (`@objectstack/core`), the helper every external write response already uses: credential-class fields (`secret`, and `password` outside the exempt `managedBy` buckets) carry `SECRET_MASK` (or `null` when unset), and `internal: true` fields are omitted. The engine's own write result is unchanged, so a privileged in-process caller that reads the stored value back off `insert` / `update` still sees it. -- **Approval request snapshot.** The record snapshot an approval request stores (`payload_json`) applies the same rule when the request is opened. -- **Outbound webhook body.** The delivered body, and the delivery row that stores it, apply the same rule to `before`, `after` and `changes`. -- **Knowledge index documents.** `recordToDocument` takes the object definition as an optional fourth argument and skips credential-class and `internal` fields, under `'*'` and when a source names one explicitly. `KnowledgeService` passes the definition from the bound engine. -- **New public surface of `@objectstack/service-knowledge` (additive):** `recordToDocument` accepts the object definition as an optional fourth argument; existing three-argument calls behave as before. -- **Receivers see masked values.** Webhook receivers and realtime clients now get `SECRET_MASK` (or `null` when unset) for credential-class fields and no key for `internal` fields. -- **Existing rows are not rewritten.** Approval snapshots, webhook delivery rows and knowledge documents written before this change keep their stored bodies; reindexing a knowledge source refreshes its documents. -- The audit trail already masked these fields and is unchanged. No other accept set or public schema changes. diff --git a/.changeset/spec-last-six-dead-citation-anchors.md b/.changeset/spec-last-six-dead-citation-anchors.md deleted file mode 100644 index dc951df11e7..00000000000 --- a/.changeset/spec-last-six-dead-citation-anchors.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Six provenance comments in `src/data/` and `src/ui/` were re-anchored - -Clause-②: no - -Six comment and docblock lines in `src/data/datasource.zod.ts`, `src/data/filter.zod.ts`, -`src/data/value-roundtrip-conformance.ts` and `src/ui/component.zod.ts` cited tracker numbers -that no longer resolve on GitHub. Each now cites the commit in this repository's history that -decided the matter, and the datasource comment also says in words which refusal it means: a -credential written into mongo's `options` passthrough. The live numbers beside them stay. -Comments only: no type, schema, export or runtime behaviour changes. diff --git a/.changeset/spec-migration-registry-rationale-decisions-in-words.md b/.changeset/spec-migration-registry-rationale-decisions-in-words.md deleted file mode 100644 index 783452a11b1..00000000000 --- a/.changeset/spec-migration-registry-rationale-decisions-in-words.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Nine migration-step rationale passages state their decisions in words instead of tracker numbers, and two registry comments cite the commit that decided them - -Clause-②: no - -The protocol 17 and protocol 18 step rationales are what `os migrate meta` shows per hop -and what the protocol upgrade guide prints. Nine of their passages named GitHub issues -that no longer exist, so an upgrading author met a number with nothing behind it. Each of -those passages now carries no number at all and says what was decided: why `mongo` and -`mongodb` are both accepted, why the form-view option `default` and `connector.errorMapping` -were retired, which earlier cleanup the import mapping `lookup` params finish, what the -memory driver's placeholder refusal extends, how the plugin manifest's `contributes` -members and `routes` were retired, and why the stack `themes` carrier and the -component-translation `submitLabel` key went. Two source comments of the migration -registry now cite the commit behind them. Text only: no migration step, entry, retired key -or def, conversion, schema, export or runtime behaviour changes. diff --git a/.changeset/spec-test-surface-dead-citation-anchors.md b/.changeset/spec-test-surface-dead-citation-anchors.md deleted file mode 100644 index ca08b9af305..00000000000 --- a/.changeset/spec-test-surface-dead-citation-anchors.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Two provenance comments that tests read literally were re-anchored - -Clause-②: no - -The removal note on `DATA_ACTION_TO_API_OPERATION` in `src/data/api-derivation.ts` and the -explanatory block about `ApiKeySchema` in `src/identity/identity.zod.ts` cited tracker numbers -that no longer resolve on GitHub. Each now opens with the commit in this repository's history -that decided the matter: 6968885ef removed the producer-less `batch: 'bulk'` alias row, and -2c86fe3ea deleted `ApiKeySchema` on the maintainer's ruling. The unit tests that read those two -comments moved with them, and the same re-anchoring was applied to the comments in the -package's test files, which do not ship. Comments only: no type, schema, export or runtime -behaviour changes. diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index 145b5bcbeb2..4c0ac7e5345 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -75,7 +75,7 @@ docker run -p 8080:8080 \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET \ -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.6.0 + ghcr.io/objectstack-ai/objectstack:17.7.0 ``` (`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -93,7 +93,7 @@ docker run -p 8080:8080 \ -e OS_ARTIFACT_URL="https://releases.example.com/hotcrm-2.2.2.json#sha256=<64 hex chars>" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.6.0 + ghcr.io/objectstack-ai/objectstack:17.7.0 ``` Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly @@ -144,7 +144,7 @@ COPY . . RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── -FROM ghcr.io/objectstack-ai/objectstack:17.6.0 +FROM ghcr.io/objectstack-ai/objectstack:17.7.0 COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json ``` @@ -162,7 +162,7 @@ image)? The official image is nothing more than: ```dockerfile title="Dockerfile (self-built runtime, equivalent)" FROM node:22-slim -RUN npm install -g @objectstack/cli@17.6.0 +RUN npm install -g @objectstack/cli@17.7.0 WORKDIR /srv/app RUN chown node:node /srv/app diff --git a/content/docs/releases/index.mdx b/content/docs/releases/index.mdx index 011f08e1ea9..4946b43bce2 100644 --- a/content/docs/releases/index.mdx +++ b/content/docs/releases/index.mdx @@ -18,7 +18,7 @@ migration steps, then covers new capabilities and notable fixes. ## Versions -- [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (current series: 17.6.0, released 2026-10-02). +- [v17.0.0](/docs/releases/v17) — Files become owned `sys_file` records with server-enforced `accept`/`maxSize` and a governed download path, bulk export becomes its own opt-in privilege, the SDK is reconciled against the routes the server actually mounts (21 dead methods out, 40+ real ones in), approval nodes route approvers dynamically via CEL expressions and decision outputs, a datasource that cannot connect fails the boot, and Node 22 becomes the supported floor; 17.1 adds partial field masking, record-view auditing on `sys_audit_log`, and a per-object read-only approval visibility tier — and makes a deactivated permission set or position actually stop granting access, withdraws the bulk-export wildcard from the shipped admin sets, and gives all three flow doors one honest HTTP status table; 17.2 tightens by-id `update`/`delete` against a silently-dropped `where` predicate or a mismatched id, retires `sys_position.permissions` and other dead ADR-0049 surfaces, and stops analytics from answering the wrong number on a cross-object filter (current series: 17.7.0, released 2026-10-06). - [v16.0.0](/docs/releases/v16) — One org identifier (`organizationId`) across hooks and actions, quorum + per-group sign-off (会签) approvals with metadata-declared decision actions, time-relative automations, filtered roll-ups, strict dashboard widgets, an identity-scoped MCP stdio transport, and a platform-wide enforce-or-remove sweep that makes dead metadata loud; 16.1 adds a `requires` capability-provider preflight, two more dashboard build gates, and `runAs:'user'` automations that run with the triggering user's real grants (final release: 16.1.0). - [v15.0.0](/docs/releases/v15) — Explain record access layer by layer, a docked AI workspace in the Console, project-ready Gantt charts, and phone sign-in; 15.1 adds permission-following attachments, no-code third-party connectors, dashboard-wide filters, pinyin search, and whole-record inline editing — with materially safer multi-tenant and write-path defaults (final release: 15.1.1). - [v14.0.0](/docs/releases/v14) — ADR-0090 vocabulary convergence completed, object `enable.*` flags become real gates, admin user management, phone/SMS auth, book-audience enforcement, data-lifecycle contract, and effective-dated grants (final release: 14.8.0). diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index 9d343e7154b..e760feaa53c 100644 --- a/content/docs/upgrading.mdx +++ b/content/docs/upgrading.mdx @@ -52,7 +52,7 @@ The official image is `ghcr.io/objectstack-ai/objectstack`, and its tags mirror ```bash # docker-compose.yml, or your orchestrator's manifest -image: ghcr.io/objectstack-ai/objectstack:17.6.0 +image: ghcr.io/objectstack-ai/objectstack:17.7.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 79b5bc6ec26..c5dfc558e5d 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,7 +29,7 @@ Multi-arch: `linux/amd64` + `linux/arm64`. [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-hosting)): ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:17.6.0 +FROM ghcr.io/objectstack-ai/objectstack:17.7.0 COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json ``` @@ -40,7 +40,7 @@ docker run -p 8080:8080 \ -v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.6.0 + ghcr.io/objectstack-ai/objectstack:17.7.0 ``` `OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -72,7 +72,7 @@ for a `file:…` path — one box only, wrong for multi-node) and MongoDB (`libsql://…` / Turso). Add one by extending the image: ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:17.6.0 +FROM ghcr.io/objectstack-ai/objectstack:17.7.0 USER root RUN npm install -g tedious USER node @@ -100,5 +100,5 @@ reverse-proxy / multi-node guidance: ## Local build of this image ```bash -docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.6.0 docker/ +docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.7.0 docker/ ``` diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 0f414c2adb0..e806482a1fc 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,137 @@ # @objectstack/example-crm +## 4.0.99 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [bd70706] +- Updated dependencies [aa0d4b9] +- Updated dependencies [5d0e4e2] +- Updated dependencies [9e9d693] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [025008a] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [088428f] +- Updated dependencies [cab6396] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [131b937] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/service-i18n@17.7.0 + ## 4.0.98 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index 85ff8289a80..144e13b5ff2 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.98", + "version": "4.0.99", "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 7965921a458..931c0e1c5cd 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/example-multi-package +## 0.0.6 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 0.0.5 ### Patch Changes diff --git a/examples/app-multi-package/package.json b/examples/app-multi-package/package.json index 86cb6e580e2..9624a7ae77b 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.5", + "version": "0.0.6", "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 cb972f7ab8b..e81d88ed103 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,166 @@ # @objectstack/example-showcase +## 0.3.21 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [13a24ec] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [f9bcd08] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [ab52182] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [440cd32] +- Updated dependencies [6c5697d] +- Updated dependencies [74281a8] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [bd70706] +- Updated dependencies [aa0d4b9] +- Updated dependencies [5d0e4e2] +- Updated dependencies [9e9d693] +- Updated dependencies [901e7cf] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [5d095a0] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [d7fff21] +- Updated dependencies [025008a] +- Updated dependencies [da40a5f] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [e09f1ac] +- Updated dependencies [c4d5713] +- Updated dependencies [93f51f1] +- Updated dependencies [a0176ef] +- Updated dependencies [07e933b] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [088428f] +- Updated dependencies [48297ad] +- Updated dependencies [cab6396] +- Updated dependencies [9f9510f] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [25eb7de] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bc7747c] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [131b937] +- Updated dependencies [76fec88] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/driver-sql@17.7.0 + - @objectstack/connector-mcp@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/cloud-connection@17.7.0 + - @objectstack/connector-openapi@17.7.0 + - @objectstack/connector-rest@17.7.0 + - @objectstack/connector-slack@17.7.0 + - @objectstack/service-i18n@17.7.0 + ## 0.3.20 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 518180969a3..571d3dc5a41 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.20", + "version": "0.3.21", "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 ecf452780e3..48292ae4f43 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,172 @@ # @objectstack/example-todo +## 4.0.99 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [8598614] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [44defd4] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [bd70706] +- Updated dependencies [aa0d4b9] +- Updated dependencies [6cf1154] +- Updated dependencies [5d0e4e2] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [a1ca156] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [be55fd2] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [025008a] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [088428f] +- Updated dependencies [0fe0a59] +- Updated dependencies [cab6396] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [13a22d0] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/service-knowledge@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/mcp@17.7.0 + - @objectstack/client@17.7.0 + - @objectstack/driver-sqlite-wasm@17.7.0 + - @objectstack/knowledge-memory@17.7.0 + - @objectstack/service-i18n@17.7.0 + ## 4.0.98 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 16e67190488..b59cbddcb3a 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.98", + "version": "4.0.99", "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 b7f3dea8d71..272a2c5ded7 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,139 @@ # @objectstack/example-embed-objectql +## 0.0.39 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [db0cf22] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [6ec54f0] +- Updated dependencies [a1ca156] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [be55fd2] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [fe10172] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [0728cbf] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [13a22d0] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/driver-memory@17.7.0 + - @objectstack/objectql@17.7.0 + ## 0.0.38 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 7b68084c37b..f761b2a9574 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.38", + "version": "0.0.39", "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 9a1b59fcc7d..0882184915b 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,54 @@ # @objectstack/hono +## 17.7.0 + +### Patch Changes + +- f5b8e29: Share-link passwords follow the platform's credential rules (#21839). + + - **The stored hash never leaves the server.** The share-link mint response (`POST /api/v1/share-links`, and `ShareLinkService.createLink`'s return value) no longer carries `password_hash`. The list and the redemption result are projected the same way. A client that reads a link's password state keeps reading it from the redemption route's `NEEDS_PASSWORD` answer, as before. + - **The stored form is the platform's slow password hash.** New passwords are hashed with scrypt at the parameters account passwords use, instead of one salted SHA-256. Links minted before this release keep working: a stored password in a legacy form still verifies, and it is re-hashed into the new form on its first successful redemption. Every comparison is constant-time. A deployment that injects its own `hashPassword` / `verifyPassword` pair is unaffected, and its stored forms are left alone. + - **The password travels in a header.** Both public share-link routes (`GET /api/v1/share-links/:token/resolve` and `/:token/messages`) accept the `x-share-password` request header, the preferred form, because a header is not part of the request URL. The `?password=` query parameter is still accepted for compatibility, so current consoles keep working until they move to the header. `/messages` accepted only the query parameter on this mount before. + - **Cross-origin clients can send the header.** `X-Share-Password` is in the default CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS` in `@objectstack/plugin-hono-server`, which the `@objectstack/hono` adapter also applies). A deployment that passes its own `allowHeaders` is unchanged; add the header to that list to let a cross-origin client use it. + - **Public share-link answers are not cached.** Both public routes answer with `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, on both mounts (the sharing plugin's routes and the runtime dispatcher's `/share-links` domain). The authenticated create, list and revoke routes are unchanged. + - **Hashing works in WebContainer.** On StackBlitz WebContainer, where `node:crypto.scrypt` is incomplete, the password is hashed with the pure-JS scrypt from `@noble/hashes` (now a dependency of `@objectstack/plugin-sharing`, as it already is of `@objectstack/plugin-auth`), at the same parameters and in the same stored form. A hash made on either runtime verifies on the other. +- Updated dependencies [909229e] +- Updated dependencies [96a9719] +- Updated dependencies [748b240] +- Updated dependencies [50e1c65] +- Updated dependencies [1fd5664] +- Updated dependencies [1d0600b] +- Updated dependencies [6d728b8] +- Updated dependencies [b206403] +- Updated dependencies [85e29b8] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [bd70706] +- Updated dependencies [aa0d4b9] +- Updated dependencies [5d0e4e2] +- Updated dependencies [045b946] +- Updated dependencies [316be32] +- Updated dependencies [6946f2f] +- Updated dependencies [83e2fee] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [025008a] +- Updated dependencies [75ddcd1] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [088428f] +- Updated dependencies [f5b8e29] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [131b937] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] + - @objectstack/runtime@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/plugin-hono-server@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index 8b1b492f6a6..1b37f4b2b8e 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index 6727c023522..f5a62f910ee 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,119 @@ # @objectstack/account +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index 8cd4bd4a926..64dc5c33f74 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "ObjectStack Account — the end-user account/self-service console app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index 2ba07a3f1be..b7d45ed6cb0 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,119 @@ # @objectstack/setup +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index c800df240f8..f1ef331e465 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index 73241a63108..0e7fff77818 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,119 @@ # @objectstack/studio +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 9e24c3ce6d2..bfeda6eb73e 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 34db7f065b7..cb1f79f1b34 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,772 @@ # @objectstack/cli +## 17.7.0 + +### Minor Changes + +- bcd68a2: feat(cli): `objectstack generate picklist NAME` scaffolds a shared option list, and the metadata summary counts picklists + + Clause-②: yes (widening) + + - **`objectstack generate picklist NAME`** (alias `os g picklist`) writes `src/picklists/NAME.picklist.ts`, a list declared with `definePicklist({ name, label, options })`, and adds its export line to `src/picklists/index.ts`. The list is collected under the `picklists` stack key. A select field takes its options from the list by naming it, `Field.select({ picklist: 'NAME' })`, in place of options of its own. The server serves that field with the list's options resolved onto it, together with any options other packages add through `picklistExtensions`, and judges writes against them. `objectstack validate` and `objectstack build` refuse a field whose `picklist` names no list the stack declares, and so does the boot. + - **`objectstack init`** wires the new `src/picklists` barrel in the `app` and `plugin` templates, the same way it wires every other directory `objectstack generate` writes into: an empty `src/picklists/index.ts` and a `picklists: exportsOf(picklists)` key in `objectstack.config.ts`. A project scaffolded by an earlier release keeps its config. `objectstack generate picklist` then reports the list as not wired and prints the import line and the `defineStack` key to add. + - **The metadata summary** that `objectstack validate`, `objectstack build` and `objectstack info` print counts the picklists a stack declares, in the `Data:` row: `Data: 1 Objects 3 Fields 1 Picklists`. A stack that declares none prints the row it printed before. The `stats` object in the `--json` output of the same three commands gains a `picklists` count. A `picklistExtensions` entry is not counted as a list. +- 713b0fa: fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **Three things change for callers and operators.** + + 1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". + 2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. + 3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. + + **What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. +- f397608: fix(cli)!: `os verify` runs the author-time rules first, and a stack they refuse fails `verify` with the findings `os validate` reports (#21323) + + Clause-②: yes (narrowing) + + + + **BREAKING** — `os verify` narrows what it passes. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What was accepted before.** `os verify` booted the app and exercised CRUD round-trip fidelity and, with `--rls`, the RLS invariant — and nothing else. A stack carrying a lookup to an object that does not exist, an action `visible` expression naming a field without `record.`, or a list column naming no field booted, round-tripped its records and printed `✓ verify passed` at exit 0, while `os validate`, `os build` and `os lint` all refused it. The documented done-bar ("`objectstack verify` is green") was green on a stack the build refuses to ship. + + **What is refused now.** `os verify` runs two stages. The first is the author-time rule registry `os validate` runs, over the stack prepared the way `os validate` prepares it: normalized, inline handlers lowered, parsed against the protocol schema, the SDUI manifest read beside the config, judged whole and then once per package of a multi-package artifact. A gating finding, or a stack that does not parse, exits 1 with those findings and the runtime stage never starts: + + - text face: `✗ Author-time rules failed (N issues) — the runtime stage did not run`, then each finding with its rule and location (the per-package and schema refusals have their own sentence); + - `--json`: the command's failure envelope, `error` (the sentence), plus a new key, `errors`, carrying the findings in the shape `os validate --json` carries them under `errors` — rule findings (with `package` on a per-package one), or the schema issues. + + Advisories never fail the stage; the text face counts them and points at `os validate`. On a passing stack the text face prints one step line and `✓ Author-time rules passed (N rules)` before the runtime stage, and the `--json` report of a run that reaches the runtime stage is unchanged. + + **Who is affected.** Only a stack `os build` already refuses: the first stage runs the same gating rules over the same prepared stack, so every stack it refuses, `os build` refuses too. The remedy is the one `os validate` prints for each finding. Measured with this branch's CLI over the examples at `222ecc27f9` (unchanged on this branch): `os validate` exits 0 on `examples/app-todo`, `examples/app-crm`, `examples/app-showcase` and `examples/app-multi-package`, so none of the four is refused by the new stage. +- 11905a4: fix(cli)!: `objectstack generate` binds a view, flow, action or app to an object (and an action to a flow) that you name or that the stack declares, never to one derived from the new item's name, and every scaffold passes `objectstack validate`, `objectstack build` and `objectstack lint` with zero findings + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows which `objectstack generate` invocations write a file. It ships as `minor` under the launch-window convention for narrowings. No export or published type changes. + + **Why.** `view`, `flow`, `action` and `app` scaffolds took the object they bind from their own name, and an action took its flow the same way. On a fresh `npm create objectstack` project holding `project` and `task`, `objectstack generate flow task_done` wrote a flow triggered by an object called `task_done` that nothing declares (a flow that never fires) and reported success, while `objectstack generate action complete_task` and `objectstack generate app tasks` were refused, because no object was called `complete_task` or `tasks`. Nothing let the author name the object they meant. + + **New options.** + + - `--object ` names the object a `flow`, `action` or `app` binds, as the stack declares it or without the namespace prefix (`--object task` binds `tasks_app_task` under `namespace: 'tasks_app'`). Without it, the scaffold binds the stack's only object. + - `--flow ` names the flow an `action` runs. Without it, the action runs the stack's only flow. + + **What is now refused, with nothing written.** In each case the command names what the stack declares and the command to run instead. + + - A `flow`, `action` or `app` with no `--object` in a stack that declares no object, or several. + - `--object` or `--flow` naming nothing the stack declares. + - An `action` with no `--flow` in a stack that declares no flow, or several. + - A `view` whose name is not an object the stack declares. A view is still named after the object it binds: `objectstack generate view task` writes the views of `tasks_app_task`. + - Any of these four outside a project, where there is no config and so no stack to check the binding against. + - `--object` or `--flow` on a type that takes neither (`object`, `dashboard`, `skill`, `picklist`, and the `types`, `client` and `migration` routes), instead of reading as honoured. + + **What the scaffolds now write.** Each was measured adding at least one finding to `os validate`, `os build` or `os lint`, and now adds none. + + - `object`: the record's title field (`name`) and no `description` field. Nothing read the `description` field, so `field-no-consumers` reported it on every generated object as soon as the project held any view, flow, action, app, dashboard or skill. + - `view`: no container `name` or `label`. The container is registered under its `object`, so `name` could only restate that key or contradict it, and no reader reaches a container's `label`. Both were `liveness-dead-property` warnings. The list now carries the `label` that `os lint` requires (`required/label` was an error). Its columns are every field the bound object declares, and it is sorted by the object's title field. It used to show a fixed `name` column, which an object without a `name` field refused. + - `flow`: `status: 'active'` in place of `'draft'`. A draft flow already fires its trigger (only `obsolete` and `invalid` disable one), so the runtime behaviour is unchanged. `flow-draft-status-ambiguous` warned on every scaffold. + - `action`: `locations: ['record_header']`. With no placement, `action-no-placement` warned that the button renders nowhere. + - `app`: its navigation entry opens the bound object and is labelled with that object's plural label. + + **What to write instead.** Name the object a flow, action or app binds, for example `objectstack generate flow task_done --object task`. Name the flow an action runs when the stack has more than one, for example `objectstack generate action complete_task --object task --flow task_done_flow`. Run the command in the project's directory. Generate a view under the name of an object the stack declares. + + **Unchanged.** `objectstack generate object`, `dashboard`, `skill` and `picklist`, and every name, namespace, parse and import check in front of the bindings. Files generated by earlier releases are not touched. +- 0557c2f: feat(cli): `os secret rewrap` re-wraps version-1 `sys_secret` ciphertext under the current AAD derivation, each row under its holder's producer scope (ADR-0128 §4.2, #21326 stage 2) + + Clause-②: yes (widening) + + A ciphertext sealed before ADR-0128 D1–D3 carries the older binding over + `(namespace, key)` alone, and still opens in this release. `os secret rewrap` moves + the stored values to the current binding through `rotateKey`, the seam ADR-0128 §4 + names. It is an operator command: a dry run by default, `--apply` to write, and + nothing on any boot or upgrade path invokes it. It has no HTTP surface. + + - **The scope comes from the holder.** `sys_secret` records no producer, and a + version-1 ciphertext binds no scope, so each row is re-sealed under the scope of + the producer whose holder references it: `settings` for a `sys_setting.value_enc` + handle, `object_secret_field` for a `secret:` ref on a business row, + `datasource_credential` for a `sys_secret:` `credentialsRef`. The holders come from + the same cross-producer reference union `os secret orphans` reads. A row nothing + references, a row whose holders belong to different producers, and every row while + a holder family could not be read are left as they are and counted, never re-sealed + under a guessed scope. `--apply` refuses an incomplete union and names the family. + - **Resumable.** A row already sealed under the current derivation is skipped as + done, so a stopped run finishes the rest when re-run and a finished run writes + nothing. + - **Safe against a live deployment.** Each row is written by one conditional update, + keyed on its id and the ciphertext the run read. A row a producer changed in + between is not overwritten, and a re-run picks it up. A driver with no + `updateMany` is refused before any row is opened. + - **Fails closed.** A row that does not open, or whose re-seal does not open to the + same plaintext under the same scope, is not written. The run finishes the rest and + exits 1. The check happens before the write. + - **Output is classes and counts only.** It never prints a plaintext, a ciphertext + or a row id. + + The command resolves its data key from `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the + persisted key file, in the strict posture: it never mints a key, and it hands the + settings service it boots the same provider so that service does not mint one + either. With no key it refuses before opening any row. + + `@objectstack/service-settings` publishes `ciphertextDerivationStatus` (and its + `CiphertextDerivationStatus` type). It is `LocalCryptoProvider`'s own reading of + which derivation sealed a stored ciphertext, read off its marker without opening it: + `current`, `superseded` or `unknown`. The re-wrap classifies rows with it rather than + restating the marker grammar. +- 3b4efa7: `os migrate meta --stored` and `os migrate audit-metadata-bodies` without `--apply` no longer write to the database they preview. Both now boot the stack the way `os migrate plan` does: schema DDL is held back, the app's inline seed loader does not run, and a SQLite file that does not exist is not created. + + Clause-②: yes (narrowing) + + + + **BREAKING** — a preview of either command at a database that lacks the table it reads now exits 1, where it used to exit 0. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What was wrong.** Both previews booted the full data stack before reading, and that boot ran schema sync and the app's seed loader. The seed loader upserts every seeded row, so a preview bumped `updated_at`, stamped `organization_id` on seeded rows that had none, put an operator's edit to a seeded row back to the seed's value, and re-evaluated relative-date seed values. On a database that was behind the app's schema, the boot also added the missing columns and created the missing tables. The 17.6.0 upgrade checklist runs both previews before their `--apply` runs, so the safety step changed the data. + + **What changes for an operator.** A preview leaves the schema and every row byte-identical, and its report is the same as before. `--apply` boots and writes exactly as before. One edge changes: a preview pointed at a database that lacks the table it reads (a SQLite file that does not exist, an unbooted database, or the wrong `--database-url`) now fails and exits 1 instead of creating the table and reporting nothing to examine. Point `--database-url` at the deployment's database, or boot the deployment once first. +- 4b20c84: `os environments list | show | create | bind | switch` run on the `os cloud login` session + + Clause-②: yes (widening) + + The documented hosted flow is `os cloud login`, then `os environments create`. The five + `os environments` subcommands read only `~/.objectstack/credentials.json` (the `os login` + session), so with only `~/.objectstack/cloud.json` they exited 1 with + `Authentication required` before sending any request, while `os login --help` sends hosted + users to `os cloud login`. + + All five now choose their session in one shared resolver: + + - With no `--url` / `OS_CLOUD_URL`, they use the `os login` session when there is one, which + is the same behaviour as before. Otherwise they use the `os cloud login` session and the URL + it recorded. + - With a `--url`, they use the session whose file names that server, `credentials.json` first. + When neither file names it, they use `credentials.json`'s session as before. The cloud token + is never sent to a URL other than its own. + - The active environment sent with each request comes from the chosen session's file. + `os environments switch` and `create --activate` no longer write a cloud environment id into + `credentials.json` when they ran on the cloud session. + + With no session at all, the `Authentication required` message now names `os cloud login` as + well as `os login`. `os package publish` is unchanged: it still reads only `cloud.json`. +- b206403: The CLI's one-shot commands no longer write to the database as a side effect of booting. No `os migrate *`, `os meta resync`, `os secret orphans` or `os storage orphans` run loads the app's inline seed data, apply and delete modes included, and every mode that writes nothing now boots read-only. + + Clause-②: yes (narrowing) + + + + **BREAKING** — a no-write run of `os migrate value-shapes`, `os migrate recorded-by`, `os migrate resume`, `os secret orphans` or `os storage orphans` at a database that lacks a table it reads now exits 1, where it used to exit 0. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What was wrong.** Eight commands booted the full data stack in a mode their documentation says writes nothing: `os migrate value-shapes` (scan), `summary-nulls`, `files-to-references` and `recorded-by` (dry run), `os migrate resume` (list), `os secret orphans` and `os storage orphans` (report), and `os meta resync` without `--yes`. That boot ran schema sync and the app's inline seed loader. The seed loader upserts every seeded row, so each run bumped `updated_at`, stamped `organization_id` on seeded rows that had none, and put an operator's edit to a seeded row back to the seed's value. On `examples/app-crm` that was all 28 seeded rows on every run. On a database behind the app's schema, the boot also added columns and created tables. The apply and delete modes ran the same seed loader alongside the write the operator confirmed. + + **What changes for an operator.** + + - Every mode that writes nothing boots the way `os migrate plan` does: the schema sync is held back, no seed rows are written, and a SQLite file that does not exist is not created. The database is left byte-identical, and the report is the same as before. + - No one-shot CLI boot loads the app's inline seed data. `--apply`, `--delete`, `os migrate resume --run` and `os meta resync --yes` write what they report and nothing else. Seeding stays with `os dev` and `os serve`. + - The deferred schema sync now covers every SQL datasource the boot connects, not only the default one. `os migrate plan` lists a second datasource's pending tables, and `os migrate apply` creates them after you confirm. + - One edge changes: a no-write run pointed at a database that lacks a table it reads (a SQLite file that does not exist, a database that was never booted, or the wrong `--database-url`) refuses and exits 1 instead of creating the table and reporting nothing. Point `--database-url` at the deployment's database, or boot the deployment once first. `os secret orphans --json` answers that refusal with `"error": "scan_failed"`. + - `os migrate value-shapes --json` prints one JSON document when the scan fails its gate. It used to print a second one, `{"error":"EEXIT: 1"}`. + + **For embedders of `@objectstack/runtime`.** `createStandaloneStack` accepts `armLifecycleSweep` (default `true`). With `false`, the ADR-0057 lifecycle sweep (rotation, retention reaping, archiving and the dangling-reference audit that rides its clock) is never armed on that boot, and an explicit `sweep()` call on it returns an empty report. The CLI passes `false` on every one-shot boot. +- 6c5697d: fix(runtime,cloud-connection)!: a job's sandboxed `body` is scheduled on every door that brings an artifact in, and install-local refuses an enabled job with no `body` (#21489) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package that declares an **enabled job with no `body`**. Such a job names its code only through `handler` — a `defineStack({ functions })` entry, which travels in the artifact's runtime module and never in the package JSON this door installs — so it used to install with a 200 and never run, hot or after a restart, with nothing saying so. + + - **Job bodies run.** A job's sandboxed `body` (`JobSchema.body`, the hook body shape) is now scheduled on every door that brings an artifact in: the boot (`os start --artifact`, a `defineStack` config) and install-local, on install and on every rehydrate after a restart. One binder does it for all of them. With both `body` and `handler` declared, the `body` wins. The body runs in the QuickJS sandbox with `ctx.api` (as system: a job has no caller), `ctx.log` and `ctx.crypto` behind its declared `capabilities`. The job's `timeoutMs` is its one time limit; with none, a job body gets a 5000 ms CPU budget. A body may return `{ outcome: 'degraded', reason }` to report a run that did not do its work. + - **A package's jobs stop with it.** Re-scheduling a package's jobs replaces its set: a reinstall whose new version drops, disables or can no longer run a job cancels that job, and a version with no jobs cancels them all. Uninstalling a package cancels its scheduled jobs through a new uninstall cleanup, `runtime.package-jobs`, on the protocol's uninstall-cleanup registry, so install-local's `DELETE` and the protocol's package uninstall both stop them and report it in `cleanups`. Another package's jobs are never touched. + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, names each refused job and the function its `handler` declares, and installs nothing: nothing is registered, persisted or scheduled. A disabled job (`enabled: false`) is not judged. A package installed by an earlier version keeps rehydrating; its handler-only job is reported at `warn` and does not run. + - **CLI.** `os package install` prints a refusal's code beside its status (`Install failed (422 VALIDATION_ERROR): …`), for every refusal alike. + - **Spec.** The shipped liveness ledger records `job.body` (`language`, `source`, `capabilities`, `memoryMb`) as live, so `os validate` / `os build` no longer warn that a job's `body` is planned and not read yet. `body.timeoutMs` stays refused on a job. `JobSchema.body`'s description and the `defineJob` example no longer say to keep a `handler` until the runtime runs job bodies. + - **Unchanged:** a `handler` job on a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) still runs its `functions` entry; a package without jobs installs exactly as before. + + The route for a refused package: give each enabled job a `body` (sandboxed JS that reaches data through `ctx.api`), or boot the artifact with `os start --artifact`, which loads its runtime module. It ships as `minor` under the launch-window convention for accept-set narrowings. +- 9a4182a: fix(spec,runtime,cli)!: the in-memory (mingo) engine is no longer a boot store — every boot door refuses it and names SQLite instead (#21492, #21572) + + Clause-②: yes (narrowing) + + + + **BREAKING**: the in-memory (mingo) engine can no longer be selected as the store a server, a migration or an embedded stack boots on. It refuses every tenant-scoped read by design, so a boot on it signed a user in and then answered `503` to every data request; there was nothing working to keep. The retirement is made at the declaration: `@objectstack/spec`'s driver table withdrew `memory`, `mingo` and `in-memory` from its selection face (they stay on the config-contract face beside `inmemory`), and every boot door refuses the engine with one sentence that names the replacement. + + - **`@objectstack/spec`** — `DATABASE_DRIVER_SELECTION_ALIASES` no longer lists `memory`, `mingo` or `in-memory`; `DATABASE_DRIVER_SELECTION_IDS` no longer lists `memory`; `resolveDatabaseDriverId` answers `undefined` for all four spellings. `resolveDriverId`, `DRIVER_ID_ALIASES`, `BUILTIN_DRIVER_IDS` and the `memory` config contract are unchanged. + - **`@objectstack/cli`** — `--database-driver memory` is refused while the flags parse (`os dev`, `os start`); `OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory` is refused before `os dev` or `os start` prints its Database row; `os serve`'s legacy path refuses the spellings and the `memory://` / `mingo://` schemes as a fatal boot error. The help no longer offers `memory://`. + - **`@objectstack/runtime`** — `createStandaloneStack`, `createDefaultHostConfig` and `resolveStandaloneDatabase` (every ordinary `os dev` / `os start` / `os serve` boot and every `os migrate` subcommand) refuse the spellings, the `memory://` and `mingo://` schemes, and a project whose default datasource is declared with `driver: 'memory'`. `resolveProjectDatabaseUrl` refuses a retired driver selection ahead of every rung, and its `ProjectDatabaseUrlSource` type no longer has the `'memory-driver'` member. `ResolvedStandaloneDatabase.driver` never names `memory`. Two exports are added for hosts that refuse the engine themselves: `namesRetiredMemoryEngine` and `retiredMemoryEngineMessage`. + - **Unchanged:** the `@objectstack/driver-memory` package; a declared non-default datasource with `driver: 'memory'` and a directly constructed `InMemoryDriver`, both still built; SQLite's dev step-down, whose last rung is still this driver. + + Migration — one flag change: + + - FROM `os dev --database-driver memory` (or `OS_DATABASE_DRIVER=memory`) TO `os dev --fresh` for a throwaway database deleted on exit. + - FROM `OS_DATABASE_URL=memory://…` / `--database memory://…` / `databaseUrl: 'memory://…'` TO `:memory:` (SQLite's own in-memory database), e.g. `OS_DATABASE_URL=:memory:`. + - FROM a default datasource declared `{ driver: 'memory' }` TO a SQLite one, e.g. `{ driver: 'sqlite', config: { filename: ':memory:' } }`. + + No shipped example selects the engine. It ships as `minor` under the launch-window convention for accept-set narrowings. +- 759dbe9: feat(cli): `os migrate unmapped-columns --object NAME` reads the values of a retired field's columns, keyed by record id, for a conversion before `os migrate apply --allow-destructive` drops them (#21573) + + Clause-②: yes (widening) + + - **What it reads.** The columns `os migrate plan` reports as `unmapped_column` for one object's table: a column that is still in the table and that no metadata declares, typically one a retired field left behind. The column set is the plan's own findings, from the same differ on the same read-only boot, so the command never reads a column the plan does not report. Each record is emitted as `{ id, values }`. `--json` prints one document, `{ database, object, table, columns, count, records, duration }`. The text face lists the columns and each record's values. + - **Why it exists.** A read or a write through the engine now serves an object's declared fields only, and naming an undeclared column is refused. An app that moves a retired field's values into the field that replaced it reads them once with this command, writes them with its own script, and then drops the columns with `os migrate apply --allow-destructive`. That is the route the read and write narrowing in `@objectstack/objectql` names for this case. + - **Operator-only and read-only.** It runs under the database credentials you pass (`--database-url`, else `OS_DATABASE_URL`, else the project database), and it reads every organization's rows. No REST route, API flag or per-request option serves these values, and the runtime doors are unchanged. It boots the way `os migrate plan` does: no schema DDL, no seed data, and no database file created. + - **Values as stored.** An unmapped column has no declared type, so each value is emitted as the database client returns it, with no field-type decoding; a PostgreSQL `timestamp` arrives as a date and is emitted as its ISO 8601 text. A value JSON cannot carry as stored (binary bytes, a `bigint`, or a non-finite number) is refused in both faces with exit 1, naming the column and the record id, and no record is emitted: read that column with the database's own client. No column the platform creates for a field type answers with one of these, on SQLite or on PostgreSQL. + - **Answers.** An object with no unmapped column, or with no table yet: empty work, exit 0. No SQL driver: `os migrate plan`'s own `no_sql_driver` answer, exit 0. An undeclared object name: `OBJECT_NOT_FOUND`, exit 1. An object the plan does not diff (federated, or bound to another datasource): refused, exit 1. A read that cannot be complete, such as one stopped by `--max-records`: refused, exit 1, and no partial set is emitted. A value JSON cannot carry as stored: refused, exit 1, as above. + - `MigrateUnmappedColumnsCommand` is exported from `@objectstack/cli` beside the other `os migrate` commands. + + Nothing that ran before changes. This is a new command. + +### Patch Changes + +- aead296: `os lint` and `os i18n extract` ask for a flow's `flows..label` translation only when the flow has a screen node at any depth, the only kind of flow the console's screen-flow runner opens and names, so a scheduled, record-triggered or API flow with no screen no longer draws an `i18n/missing-flow` demand for a label no surface shows. + + Clause-②: no +- dabd1c5: The published `package.json` no longer declares `oclif.plugins`, and the package no longer lists `@oclif/plugin-help` or `@oclif/plugin-plugins` as devDependencies. The array named both plugins, but they were only devDependencies, and oclif loads an `oclif.plugins` entry only when the same name is in `dependencies`. Neither plugin ever loaded. + + Clause-②: no + + **What changes for an operator.** Nothing. `os --help`, every command and topic, and the output of `os help` and `os plugins` read byte-identical before and after the change. `os help` and `os plugins …` were never commands, and each still exits 2 with `command … not found`. Use `os --help` or `os --help` for help. + + **What the README now says.** It said `os plugins install`, `uninstall` and `update` came from `@oclif/plugin-plugins` and installed CLI extensions. That was never true. This CLI ships no plugin manager. To add commands to it, build an `os` distribution: a package whose own `package.json` lists the extension in both `oclif.plugins` and `dependencies`. +- 37a0148: The published README now describes the `os` that ships. Five things it said were false. + + Clause-②: no + + - **Short flags.** The README listed `-v, --version` and `-h, --help` as global options. `os -v` and `os -h` exit 2 with `command -v not found` / `command -h not found`, because only `--version` and `--help` are registered. It now lists `--version` and `--help` alone and says there is no short form. `-v` already belongs to commands of their own: it is `--verbose` on `os dev`, `os serve`, `os start` and `os doctor`, and `--version` on `os package publish` and `os package install`. + - **The `os plugin` group.** The README said there is no `os plugin` command group. `os plugin build`, `os plugin sign` and `os plugin publish` are registered, and the README now lists them. It also says the group has no `install`, and that `os plugin` is a different thing from `os plugins`, which is not a command. + - **Two command rows.** `os init [name]` creates a new directory of that name when a name is given, so it no longer says "in the current directory" for every case. `os dev` restarts the server after each rebuild, so it no longer says "with hot reload". + - **Cloud credentials and flags.** The README said every cloud command takes its credentials from `os cloud login` or from `--token` / `OS_CLOUD_API_KEY` and `--server` / `OS_CLOUD_URL`. That holds only for `os package publish` and `os plugin publish`. `os environments list`, `show`, `create`, `bind` and `switch` take `-u, --url` (env `OS_CLOUD_URL`) and `-t, --token` (env `OS_TOKEN`), and otherwise use the `os login` session in `~/.objectstack/credentials.json` — never the `os cloud login` session. With only `os cloud login` done they exit 1 with `Authentication required`. The README now has a per-command table, and its typical publish flow says so at the `os environments create` step. + - **`os serve --ui`.** The README said it enables "Studio UI". It enables the bundled Console portal at `/_console/` when `@object-ui/console` is installed, which is what `os serve --help` says. + + **What changes for an operator.** Nothing at runtime. No command, flag, environment variable, exit code or help page changes. +- 5155093: fix(cli): `os verify --json` writes exactly one JSON document to stdout; the booted stack's log lines move to stderr (#21324) + + Clause-②: no + + `os verify --json > report.json` used to exit 0 and leave a file no JSON parser accepts. On a two-object stack that reaches the runtime stage, 318 lines landed on stdout ahead of the report: the kernel logger's `INFO` and `WARN` records, the ObjectQL registry's `[Registry] …` lines and the HTTP server's stop line. `JSON.parse` failed at position 4. + + Under `--json`, stdout now carries the report and nothing else, and every other line the run writes goes to stderr. Nothing is dropped: the boot records, the warnings among them and the shutdown lines all still reach the operator, on stderr. The document is unchanged, and so is the shape of each of the three `--json` documents (the runtime report, the author-time refusal, and the could-not-run envelope). + + `os verify` without `--json` is unchanged: the log lines stay on stdout beside the text report. + + A script that read those log lines from `os verify --json`'s stdout now reads them from stderr. +- fa7b565: fix: a fresh project no longer warns about its own starter fields after the first `objectstack generate` + + Clause-②: no + + The blank starter's `note` object (`npm create objectstack`) and the item object of the `app` template (`objectstack init -t app`) now declare one field group, `fieldGroups: [{ key: 'details', label: 'Details' }]`, and place every field in it with `group: 'details'`. Before this, the first view, flow, dashboard or other metadata that can read a field made `objectstack validate` and `objectstack lint` report `field-no-consumers` on a field the author never wrote: the note's `body`, or the item's `description` and `status`. That held whether the author generated it or wrote it by hand. Both commands still exited 0. A field placed in a declared group is drawn by the object's form and detail page, and the rule counts that as displayed, so a fresh project now reports nothing. The `plugin` and `empty` templates are unchanged: the plugin's one field is the record's title, which the rule never reports, and the empty template declares no object. + + **What changes for an author.** In a new project, the object's form and detail page show the starter fields in one section labelled Details instead of a flat list. A field you add joins a section the same way, by naming its `key` in `group`. A project scaffolded by an earlier release keeps its files. To clear the warning there, add the same `fieldGroups` entry to the object and `group: 'details'` to each field the warning names, or give each field another consumer, such as a view column. +- 2ee8383: fix(cli): `os migrate recorded-by`, `resume` and `account-issuer` print exactly one `--json` document, and a completed run exits 0 (#21434) + + Clause-②: no + + `os migrate recorded-by --apply --yes --json` converted the rows, printed its result, then printed a second document, `{"error":"EEXIT: 0","duration":…}`, and exited 1. A script that read the exit status took the completed run for a failure, and a parser that read stdout failed on the second document. The cause was the command's own `catch`: the `this.exit(…)` inside its `try` throws oclif's exit signal, and the `catch` reported the signal as an error. + + The same `catch` sat in three more commands: + + - **`os migrate resume --run --json`.** A run that was already concluded printed a second `{"error":"EEXIT: 0"}` and exited 1 instead of 0. A resumed run did the same. Every refusal inside the command (unknown run id, plan not loaded, confirmation required) printed a second `{"error":"EEXIT: 1"}` under its own document. + - **`os migrate account-issuer --json`.** A refused pre-flight printed a second `{"error":"EEXIT: 1"}` under its report. Without `--json`, it printed an extra `EEXIT: 1` error line. + - **`os migrate apply`** (text output). A `sys_account.issuer` pre-flight refusal printed an extra `EEXIT: 1` error line. + + Each command now prints one document and exits with the status it computes. A completed `recorded-by --apply` and an already-concluded or resumed `resume --run` exit 0. Refusals and failed runs still exit 1. A script that worked around the second document or the exit status 1 can drop that workaround. +- 25797a1: `os secret orphans`, `os storage orphans` and `os migrate files-to-references` no longer create a data key file in the key home. A one-shot command never mints key material (#21471) + + Clause-②: no + + Each of these commands composes the settings service. Given no crypto provider, the service builds its own default one. In a development posture with no `OS_SECRET_KEY`, no `OS_DEV_CRYPTO_KEY` and no key file, that default writes a new key file into the key home. So a report that promises to write nothing left key material behind, and the next development-posture process on that host adopted the minted key. A minted key opens nothing that is stored, so the run gained nothing from it. + + - **What these commands hand the settings service now.** They pass the provider `os secret rewrap` already passed: the one over a data key that already exists, resolved the way every host resolves it, in the strict posture and with the auto-key opt-in withheld, so it never mints. With no key, the service gets a provider that refuses every call and says why. A stored setting that cannot be opened reads as it did with a freshly minted key: empty, with a warning. + - **One composition.** The settings service is composed in one place in `@objectstack/cli` (`utils/one-shot-settings.ts`), shared by `secret orphans`, `secret rewrap` and the storage arm of the data-migration plugins. `os serve` still takes the service's default: persisting a key in a development posture so restarts reuse it is that host's documented behaviour. + - **Visible difference.** On a host whose key lives only in the key file, these commands now print the strict posture's one-line note on stderr ("using the persisted key at …"), as `os secret rewrap` already did. stdout and `--json` output are unchanged. +- 5895119: fix(cli): `os package install`, `os package publish` and `os plugin sign` print one error line per refusal (#21496) + + Clause-②: no + + `os package install ./does-not-exist.json` printed `✗ Cannot read artifact: ENOENT …` and then a second line, `✗ EEXIT: 1`. The exit status, 1, was right. The extra line came from the command's own `catch`: the `this.exit(1)` inside its `try` throws oclif's exit signal, and the `catch` reported the signal as an error. + + The same `catch` sat in two more commands: + + - **`os package publish`.** Every refusal it makes printed the extra `✗ EEXIT: 1` line. Examples are an unreadable artifact, an invalid manifest id, no cloud login, a failed package registration and a failed version publish. An `--icon-file` whose image type it cannot infer printed three error lines: the refusal, then `✗ Cannot read --icon-file '…': EEXIT: 1`, then `✗ EEXIT: 1`. + - **`os plugin sign`.** A signature that failed its self-verification printed `✗ Self-verification error: EEXIT: 1` under the refusal. + + Each refusal is now one error line, and every exit status is unchanged. A script that filtered out the `EEXIT` line can drop that filter. +- 550f4cc: `os migrate resume --run --yes` can resume an interrupted `os migrate recorded-by` run, and `os serve` reports interrupted migration runs at boot (#21498) + + Clause-②: no + + `MigrationRecoveryPlugin` owns two things: the `migration-plans` registry, where a journal-backed migration's code is looked up, and the boot scan that reports runs which started and never finished. No CLI boot composed it. So `os migrate resume` found no plan for any run. It refused with "no loaded package registers" the plan, even though the plan's package was loaded in that process. And no `os serve`, `os start` or `os dev` boot ever scanned the migration journal. + + - **The `os migrate` data commands** (`recorded-by`, `resume`, `value-shapes`, `summary-nulls`, `files-to-references`, `meta --stored`, `audit-metadata-bodies`, `os storage orphans`) now boot with the plugin. A run interrupted before any of its chunks committed now resumes to completion. A command booted over an interrupted run also warns about that run on stderr first. + - **Every `os serve` boot** (and so `os start` and `os dev`, which spawn it) composes the plugin beside `PlatformObjectsPlugin`, which registers the journal the scan reads. An interrupted run is reported once at boot, with the `os migrate resume --run ` command that resumes it. Nothing is resumed automatically. A database with no interrupted run prints nothing. A config that composes its own `new MigrationRecoveryPlugin()` keeps that instance. + - **A run that had committed a chunk, or that was started with a non-default `--chunk-size`,** reaches the runner too. The runner fix that lets it resume is in the `@objectstack/core` entry for #21528. +- e909aa0: `os dev -a PATH` and `os start --artifact PATH` now serve the artifact they name, also from a directory that holds an `objectstack.config.ts` (#21501). + + Clause-②: no + + - **One precedence, written once.** The order is `--artifact` > `OS_ARTIFACT_URL` > `OS_ARTIFACT_PATH` > `/dist/objectstack.json` > `/dist/objectstack.json` (`os start` only) > a cwd `objectstack.config.ts`, except that a cwd config joins the boot when the resolved artifact is its own compiled output. It is the order the `os start` reference already published. `os start` and `os dev` both resolve through one module, and the `serve` child they spawn boots exactly their answer. + - **Beside a config.** The child used to read the supervisor's answer only when the working directory held no config. So `os dev -a X` and `os start --artifact X` printed `Artifact: X` and served the config's `dist/objectstack.json`, or the config itself. A named artifact now boots alone, exactly as it boots from a directory with no config. The config takes part only when the artifact is its own compiled output: `/dist/objectstack.json`, or the path the command compiled it to. A bare `os dev`, a bare `os start` in a project, and `os start --artifact ./dist/objectstack.json` take that path, and are unchanged. A host config (its `plugins` hold code) boots its own module there, because its compiled output cannot carry that code. + - **`OS_ARTIFACT_PATH` beside a config** follows the same rule: `OS_ARTIFACT_PATH=Y os start` serves `Y` without loading the config. Under `os start --artifact ./dist/objectstack.json` the flag now also wins over an exported `OS_ARTIFACT_PATH` inside the config boot. + - **`os dev` under a local `OS_ARTIFACT_PATH`** compiles the cwd config into that path, so the file there is the config's own compiled output. The config takes part in the boot that serves it, and a host config compiled there keeps its plugins. + - **`os dev` gains the `OS_ARTIFACT_URL` rung.** `--artifact` outranks it. Before, the reference stayed in the child's environment and won. Without the flag the reference drives the boot, as under `os start`. The `Artifact:` row names it (redacted), and nothing is compiled into, watched for or judged stale against it. + - **Banner rows.** `os start` and `os dev` print `Config:` only when the config takes part in the boot. The child says it is not loading a config that sits beside a named artifact, instead of `No objectstack.config.ts found`. + - **The ready banner names what loaded.** On a config boot, a non-host config whose app was served from its compiled artifact gets `Artifact: dist/objectstack.json` in the ready banner, and a host config keeps `Config: objectstack.config.ts`. No ready-banner row names a file the boot did not load. + + Upgrading: a project that ran `os dev -a`, `os start --artifact` or `OS_ARTIFACT_PATH` beside its config, and relied on that config being loaded, should drop the override or point it at `./dist/objectstack.json`. +- 24dc7c1: fix(cli): `os init` prints its dependency-install and scaffold-validation refusals once (#21523) + + Clause-②: no + + `os init demo -p npm` with an unreachable package registry printed `✗ Project scaffolded, but dependency installation failed.`, then a second `✗ Dependency installation failed`, then oclif's `Error: Dependency installation failed`, and exited 2. The second `✗` line came from the command's outer `catch`: the `this.error(…)` inside its `try` throws oclif's exit signal, and the `catch` reported it again. A scaffold that failed its own validation got a second `✗ Scaffold validation failed` line under its refusal the same way. + + The `catch` now lets the signal through. Each refusal prints its `✗` line once, followed by oclif's `Error:` line as before, and the exit status is still 2. +- aa0d4b9: `os migrate resume`, `os migrate recorded-by` and `os migrate value-shapes` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 with "The database refused to run this query" (#21529) + + Clause-②: no + + Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing. On a never-booted database (or a `--database-url` that points at one), every default run failed: + + - `os migrate resume` exited 1, naming `sys_migration_journal`; + - `os migrate recorded-by` exited 1, naming `sys_metadata_history`; + - `os migrate value-shapes` reported every scanned object as unreadable, kept the gate closed and exited 1, over data that does not exist. + + Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so: + + - `os migrate resume` lists no interrupted runs (`{"interrupted": [], "count": 0}`), exit 0; + - `os migrate recorded-by` reports `pending: 0`, nothing to convert, exit 0; + - `os migrate value-shapes` completes a clean scan of zero records, exit 0, and names the objects it did not read because they have no table yet (on stderr under `--json`). + + Human mode says the table is not there yet, instead of implying the command looked through one. `--json` documents have the same shape as on a booted database with nothing to do. The write modes (`--run`, `--apply`) are unchanged: they boot with the schema sync, so their tables exist before they read. + + `MigrationRecoveryPlugin` (`@objectstack/runtime`), which every one of these boots composes, scans the migration journal at boot. On such a database it logged "Migration journal scan failed; interrupted migrations (if any) were NOT detected" on every run. It now treats a missing journal table as "no runs" and says nothing. It recognises that case only with the shared `isMissingTableError` predicate, asked about `sys_migration_journal` itself. Any other failure of the scan still warns. + + There is nothing to migrate. +- bf36edd: fix(cli): `os init` and `os compile` render each refusal once, not once on stdout and again as oclif's `Error:` block on stderr (#21542) + + Clause-②: no + + `os init demo -t bogus` printed `✗ Unknown template: bogus` on stdout, then the same sentence as oclif's `Error:` block on stderr, and exited 2. Ten refusals did it: the five `os init` makes before it writes anything (an unknown template, a project name that is not valid, a target directory that is not empty, a current directory whose name is not a valid project name, an `objectstack.config.ts` that already exists), its scaffold self-test and dependency install, its catch-all, and `os compile`'s runtime-bundle refusal and catch-all (`os build` inherits both). Each printed its own `✗` line and then handed the sentence to `this.error`, which has oclif's entry point render it again. + + Each now prints its `✗` line and the hint under it once, and ends in `this.exit(2)`: the status `this.error` raised, with nothing rendered by the entry point. Stdout carries the same lines as before; stderr no longer repeats them. Exit statuses are unchanged: 2 for all ten. + + A script that read the sentence from stderr, from the `Error:` block, now finds it on stdout, on the `✗` line, which is where the full wording and the hint always were. +- 1777a9b: `os migrate account-issuer`, `os migrate audit-metadata-bodies`, `os migrate meta --stored`, `os secret orphans`, `os secret rewrap` and `os storage orphans` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 on a refused read (#21552) + + Clause-②: no + + Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing, and the database refused the read. On a never-booted database (or a `--database-url` that points at one) every default run exited 1: + + - `os migrate account-issuer` refused, naming `sys_account`; + - `os migrate audit-metadata-bodies` counted `failures: 3` for `sys_audit_log`, `sys_activity` and `sys_metadata_audit`; + - `os migrate meta --stored` refused, naming `sys_metadata`; + - `os secret orphans` and `os secret rewrap` answered `"error": "scan_failed"`, naming `sys_secret`; + - `os storage orphans` refused, naming `sys_file`. + + Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so: + + - `os migrate account-issuer` reports no account and no collision (`ok: true`), exit 0; + - `os migrate audit-metadata-bodies` reports nothing to rewrite, with `failures: 0`, exit 0; + - `os migrate meta --stored` reports no stored metadata to examine (`scanned: 0`, `clean: true`), exit 0; + - `os secret orphans` and `os secret rewrap` report no secret to act on, with every holder family enumerated rather than a gap, exit 0; + - `os storage orphans` reports no stranded file, exit 0. + + Each names the tables it did not read: on stdout in human mode, on stderr under `--json`, where stdout stays one document. `os migrate account-issuer` is the one that recognises the refusal instead of asking the boot: its boot composes no auth plugin, so `sys_account` is never listed as a table to create. It recognises only the missing-table refusal for `sys_account`, with the shared `isMissingTableError` predicate. + + A table that exists but lacks a column, and any other read that is refused, is still read and still refuses with exit 1. The write modes (`--apply`, `--delete`) are unchanged: they boot with the schema sync, so their tables exist before they read. + + There is nothing to migrate. +- 417443e: `os migrate value-shapes` and `os migrate files-to-references` record the deployment-level ADR-0104 flag only from a run over every object, and every command in the `os migrate` data-migration family refuses an `--object` name the deployment does not declare (#21644). + + Clause-②: no + + - **A narrowed `--apply` records no deployment flag.** The flag attests the stored data of every object and turns strict enforcement on, but a run narrowed by `--object` reads only the named objects. Such a run still applies its fixes: `files-to-references` converts the named objects' values. It records no flag, whether it passes or fails, and leaves a flag that an earlier full-scope run recorded exactly as it was. Its output says why and names the run that records the flag: the same command without `--object`. The `--json` document carries `filter: { objects }`, which is `null` on a full-scope run, so a narrowed run is never mistaken for a full one. Any `--object` narrows, even a list that names every object. A full-scope `--apply` records the flag as before. + - **`runFilesToReferencesMigration`** (`@objectstack/service-storage`) skips the flag write when it is given `objects`. That includes `[]`, which walks nothing. Its `flag` result is `null` on a narrowed run. + - **The column step of `files-to-references` does not run on a narrowed run.** It retypes every single-value media column in the database on the authority of the gate, and a narrowed gate vouches only for the named objects. Before this change, a narrowed `--apply` or a misspelled one moved those columns and stamped `columns_moved_at`. + - **An unknown `--object` is an error.** This applies to `value-shapes`, `files-to-references`, `summary-nulls` and `duplicates`. A name the booted registry does not declare exits 1 with `OBJECT_NOT_FOUND`, and the error names that name and the declared objects. The check runs before anything is read or written. Until now, such a name was filtered out of the scan without a word, so a typo scanned nothing and read as a clean run. `duplicates` reports the refusal as `{ error: 'report_failed', detail, code }`. A declared object that the command has nothing to check on is still accepted. +- 1c3a4d9: A TOTP enrollment names the deployment, not the auth library. `/two-factor/enable` and `/two-factor/get-totp-uri` answered an otpauth URI whose issuer and label prefix were `Better Auth`, so every authenticator app listed the account under that name. They now carry the deployment's app name: `OS_APP_NAME`, else the configured `appName`, else `ObjectStack`. An explicitly set `branding.workspace_name` setting still outranks it. + + Clause-②: no + + - **Existing enrollments keep working.** The issuer is a display label. The stored enrollment holds only the encrypted secret, the backup codes and the confirmation flag, and the codes depend only on the secret, digits and period. An authenticator app enrolled under `Better Auth` keeps producing codes that verify. It keeps its old label until the user re-enrolls. + - **`@objectstack/plugin-auth`.** `AuthManager` passes its app name to better-auth as `appName`. In better-auth 1.7.3 that key names only these two otpauth URIs. No cookie name or stored value derives from it. + - **`@objectstack/cli`.** `objectstack serve` now passes the deployment app name to `AuthPlugin`. It is resolved by the same chain the email service's template context uses: `OS_APP_NAME` > `config.email.appName` > `config.email.defaultTemplateContext.appName` > `config.appName` > `ObjectStack`. Before, `serve` built `AuthPlugin` with no app name, so auth answered `ObjectStack` whatever `OS_APP_NAME` said. Auth emails were affected too: under `serve` they now name the deployment the way every other email already did. + - The issuer is read when the auth instance is built. A `branding.workspace_name` change made after that reaches new enrollments at the next restart or auth-settings change, while auth emails pick it up on their next send. +- 6afb1b5: `os migrate plan` and `os migrate apply` boot a config whose connector plugins depend on a service that only `requires` supplies (#21732). Before this fix, both commands exited 1 on a fresh `create-objectstack -t blank` app and on `examples/app-showcase` with `[Kernel] Dependency 'com.objectstack.service-automation' not found for plugin 'com.objectstack.connector.rest'`. + + Clause-②: no + + - **Why it failed.** The connectors (`@objectstack/connector-rest`, `-openapi`, `-mcp`, `-slack`) declare a hard dependency on the automation service. The blank template and the showcase ask for automation only through `requires: ['automation', …]`. `os serve` turns that token into the provider, but the schema-migration composition read `config.plugins` and never read `requires`. + - **What it composes now.** It uses the same token lookup `os serve` uses (`Serve.CAPABILITY_PROVIDERS`, with exact identity matching, and an explicit instance in `plugins` still wins). It composes a provider only when a plugin it already composed hard-depends on that provider and the config's `requires` (or the always-on slate) supplies it. + - **Automation is taken inert** (`armRuntime: false`). The engine and node registry come up. No flow is registered, no trigger or job is bound, no connector is materialized and no suspended run is resumed. Its `init()` declares `sys_automation_run`, `sys_flow_dispatch` and `sys_flow_credential`, so the plan now covers the tables `os serve` creates for this capability. + - **A provider with no measured declaration posture is refused by name.** The refusal names the plugin, its dependency and the token, instead of booting that provider's `start()` inside a dry run. A dependency that no token supplies is still refused by the kernel, as `os serve` refuses it. + - A config that lists no plugin with such a dependency composes exactly what it did before. +- 025008a: fix(runtime,cli): a plain `os dev` now self-heals safe schema drift on restart and provisions the `telemetry` sibling database, as `content/docs/deployment/cli.mdx` already says (#21733) + + Clause-②: yes (widening) + + - **What was broken.** A config with no instantiated `plugins[]` (every fresh scaffold) boots through the standalone stack. Its `default` datasource was built without `autoMigrate: 'safe'`: only the config-load fallback that a host config or `OS_MODE=off` takes carried it. So safe drift was never applied on restart. An example is a per-organization unique index that an older release left non-NULL-safe. Meanwhile the driver's drift line and `os migrate plan` both said the change was "auto-applied at boot under dev autoMigrate: 'safe'". The same boot never provisioned the `.telemetry.` sibling either. + - **The fix.** The dev self-heal decision now lives in one place, `devAutoMigrateConfig` in `@objectstack/runtime`. That is the driver kinds whose connection contract declares `autoMigrate` (sqlite, postgres, mysql), on a dev boot. The standalone stack, the CLI's config-load fallback and the telemetry sibling all read it, so no kind gains or loses the self-heal relative to the host path. The telemetry provision is one helper (`provisionTelemetryDatasource`) that both serving paths call, under the same `resolveTelemetryDbPath` rule: dev default-on for a file-backed SQLite primary, `OS_TELEMETRY_DB=0` to opt out, `OS_TELEMETRY_DB=` to opt in anywhere. + - **Only a serving boot self-heals.** The standalone stack arms the self-heal on an explicit `dev: true`. That is what `os dev` passes. It does not arm it on the `NODE_ENV=development` default that its sqlite step-down still takes. A one-shot command (`os migrate *`, `os meta resync`, …) passes no `dev`, so it never applies drift its operator did not confirm, whatever `NODE_ENV` says. Production boots are unchanged: the definition carries no `autoMigrate`, and the SQL driver refuses it under `NODE_ENV=production` anyway. + - **Why minor.** `@objectstack/runtime` gains two exports on its only entry, `devAutoMigrateConfig` and its `DevAutoMigrateConfig` type. That is the widening: the existing decision moved out of the CLI so that the CLI reads it rather than keep a second copy. No config key, schema or accept set moves. `@objectstack/cli` is a `patch`: its fix restores documented behaviour and adds no public surface. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [f97660c] +- Updated dependencies [13a24ec] +- Updated dependencies [fd5a1cd] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [48fa7a3] +- Updated dependencies [7e7e64b] +- Updated dependencies [15b29d3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [f9bcd08] +- Updated dependencies [cc07862] +- Updated dependencies [e3ad492] +- Updated dependencies [4916168] +- Updated dependencies [f9f9f91] +- Updated dependencies [44072fc] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [0fc8087] +- Updated dependencies [99589f9] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [bcd68a2] +- Updated dependencies [39a912e] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [7aab759] +- Updated dependencies [0e10be6] +- Updated dependencies [1c52a5e] +- Updated dependencies [97239c3] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [69a12a0] +- Updated dependencies [222ecc2] +- Updated dependencies [1371dc9] +- Updated dependencies [1caa603] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [fbe2deb] +- Updated dependencies [ee75aae] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [ab52182] +- Updated dependencies [57cc695] +- Updated dependencies [0557c2f] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [49524f6] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [535d1d2] +- Updated dependencies [d956910] +- Updated dependencies [6d487d2] +- Updated dependencies [6d67ad5] +- Updated dependencies [d7d5b4f] +- Updated dependencies [fa7b565] +- Updated dependencies [ca0dfb6] +- Updated dependencies [8b123c0] +- Updated dependencies [5e58193] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [3bddd4a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [520f66f] +- Updated dependencies [b793010] +- Updated dependencies [6f17d1d] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [81e69ca] +- Updated dependencies [85e29b8] +- Updated dependencies [d70353f] +- Updated dependencies [086ad0a] +- Updated dependencies [0b82391] +- Updated dependencies [6210f88] +- Updated dependencies [35dfb81] +- Updated dependencies [e9dec3d] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [88fb5e8] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [ce53218] +- Updated dependencies [44defd4] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [a7ab047] +- Updated dependencies [440cd32] +- Updated dependencies [f9a8eb8] +- Updated dependencies [6c5697d] +- Updated dependencies [74281a8] +- Updated dependencies [9a4182a] +- Updated dependencies [550f4cc] +- Updated dependencies [2df621a] +- Updated dependencies [41b1333] +- Updated dependencies [1ca1eb0] +- Updated dependencies [bee8d1c] +- Updated dependencies [5dbcee8] +- Updated dependencies [ec390ec] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [a4f0cb0] +- Updated dependencies [bd70706] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [aa0d4b9] +- Updated dependencies [6cf1154] +- Updated dependencies [5d0e4e2] +- Updated dependencies [e367002] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [901e7cf] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [5d095a0] +- Updated dependencies [a1ca156] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [7b07749] +- Updated dependencies [96b0e31] +- Updated dependencies [f40bb32] +- Updated dependencies [5ac2ba1] +- Updated dependencies [1968d5e] +- Updated dependencies [eea82af] +- Updated dependencies [417443e] +- Updated dependencies [be55fd2] +- Updated dependencies [31e3e00] +- Updated dependencies [a2aadab] +- Updated dependencies [ced217c] +- Updated dependencies [8843505] +- Updated dependencies [ff16740] +- Updated dependencies [234d1d8] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [5259a35] +- Updated dependencies [7fd2c34] +- Updated dependencies [c43a8ae] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [cf60dbc] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [73b2246] +- Updated dependencies [c7a60e1] +- Updated dependencies [e83c9f6] +- Updated dependencies [d7fff21] +- Updated dependencies [3eb38ae] +- Updated dependencies [33f9791] +- Updated dependencies [1c3a4d9] +- Updated dependencies [025008a] +- Updated dependencies [da40a5f] +- Updated dependencies [b7a13c7] +- Updated dependencies [50b5e03] +- Updated dependencies [045f764] +- Updated dependencies [18c2ddc] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [e09f1ac] +- Updated dependencies [c4d5713] +- Updated dependencies [93f51f1] +- Updated dependencies [26d710e] +- Updated dependencies [08adfea] +- Updated dependencies [a0176ef] +- Updated dependencies [07e933b] +- Updated dependencies [c9be1f1] +- Updated dependencies [e1790fd] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [833d57c] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [18fe681] +- Updated dependencies [3237b4a] +- Updated dependencies [088428f] +- Updated dependencies [2e78046] +- Updated dependencies [48297ad] +- Updated dependencies [0fe0a59] +- Updated dependencies [cab6396] +- Updated dependencies [9f9510f] +- Updated dependencies [87712ab] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [25eb7de] +- Updated dependencies [41a1135] +- Updated dependencies [255a777] +- Updated dependencies [54fb60a] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [5e0b489] +- Updated dependencies [07c842d] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [bc7747c] +- Updated dependencies [b238856] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [9cc2c79] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [76fec88] +- Updated dependencies [13a22d0] +- Updated dependencies [753e7a1] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [0d8ea5e] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/service-automation@17.7.0 + - @objectstack/lint@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/driver-memory@17.7.0 + - @objectstack/driver-mongodb@17.7.0 + - @objectstack/driver-sql@17.7.0 + - @objectstack/driver-turso@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/plugin-email@17.7.0 + - @objectstack/service-queue@17.7.0 + - @objectstack/service-sms@17.7.0 + - @objectstack/service-storage@17.7.0 + - @objectstack/trigger-record-change@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/plugin-approvals@17.7.0 + - @objectstack/plugin-audit@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/plugin-sharing@17.7.0 + - @objectstack/service-analytics@17.7.0 + - @objectstack/trigger-api@17.7.0 + - @objectstack/objectql@17.7.0 + - create-objectstack@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/trigger-schedule@17.7.0 + - @objectstack/plugin-auth@17.7.0 + - @objectstack/mcp@17.7.0 + - @objectstack/service-package@17.7.0 + - @objectstack/service-settings@17.7.0 + - @objectstack/cloud-connection@17.7.0 + - @objectstack/rest@17.7.0 + - @objectstack/verify@17.7.0 + - @objectstack/plugin-hono-server@17.7.0 + - @objectstack/service-messaging@17.7.0 + - @objectstack/plugin-webhooks@17.7.0 + - @objectstack/console@17.7.0 + - @objectstack/account@17.7.0 + - @objectstack/setup@17.7.0 + - @objectstack/client@17.7.0 + - @objectstack/driver-sqlite-wasm@17.7.0 + - @objectstack/observability@17.7.0 + - @objectstack/service-cache@17.7.0 + - @objectstack/service-job@17.7.0 + - @objectstack/service-realtime@17.7.0 + - @objectstack/plugin-pinyin-search@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index fceeeeebf65..6c3190acd5a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.6.0", + "version": "17.7.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 9a75e217b4c..9645096ff13 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,121 @@ # @objectstack/client-react +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/client@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index 1bb9ec8b20b..1057c8b4adf 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "main": "dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 3e6f9251f9b..bc5a0fd5a13 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/client +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index 89017710a29..73c617326c2 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "main": "dist/index.js", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index c80a157c086..dc0f5166ecb 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,334 @@ # @objectstack/cloud-connection +## 17.7.0 + +### Minor Changes + +- 6c5697d: fix(runtime,cloud-connection)!: a job's sandboxed `body` is scheduled on every door that brings an artifact in, and install-local refuses an enabled job with no `body` (#21489) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package that declares an **enabled job with no `body`**. Such a job names its code only through `handler` — a `defineStack({ functions })` entry, which travels in the artifact's runtime module and never in the package JSON this door installs — so it used to install with a 200 and never run, hot or after a restart, with nothing saying so. + + - **Job bodies run.** A job's sandboxed `body` (`JobSchema.body`, the hook body shape) is now scheduled on every door that brings an artifact in: the boot (`os start --artifact`, a `defineStack` config) and install-local, on install and on every rehydrate after a restart. One binder does it for all of them. With both `body` and `handler` declared, the `body` wins. The body runs in the QuickJS sandbox with `ctx.api` (as system: a job has no caller), `ctx.log` and `ctx.crypto` behind its declared `capabilities`. The job's `timeoutMs` is its one time limit; with none, a job body gets a 5000 ms CPU budget. A body may return `{ outcome: 'degraded', reason }` to report a run that did not do its work. + - **A package's jobs stop with it.** Re-scheduling a package's jobs replaces its set: a reinstall whose new version drops, disables or can no longer run a job cancels that job, and a version with no jobs cancels them all. Uninstalling a package cancels its scheduled jobs through a new uninstall cleanup, `runtime.package-jobs`, on the protocol's uninstall-cleanup registry, so install-local's `DELETE` and the protocol's package uninstall both stop them and report it in `cleanups`. Another package's jobs are never touched. + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, names each refused job and the function its `handler` declares, and installs nothing: nothing is registered, persisted or scheduled. A disabled job (`enabled: false`) is not judged. A package installed by an earlier version keeps rehydrating; its handler-only job is reported at `warn` and does not run. + - **CLI.** `os package install` prints a refusal's code beside its status (`Install failed (422 VALIDATION_ERROR): …`), for every refusal alike. + - **Spec.** The shipped liveness ledger records `job.body` (`language`, `source`, `capabilities`, `memoryMb`) as live, so `os validate` / `os build` no longer warn that a job's `body` is planned and not read yet. `body.timeoutMs` stays refused on a job. `JobSchema.body`'s description and the `defineJob` example no longer say to keep a `handler` until the runtime runs job bodies. + - **Unchanged:** a `handler` job on a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) still runs its `functions` entry; a package without jobs installs exactly as before. + + The route for a refused package: give each enabled job a `body` (sandboxed JS that reaches data through `ctx.api`), or boot the artifact with `os start --artifact`, which loads its runtime module. It ships as `minor` under the launch-window convention for accept-set narrowings. +- 045b946: fix(runtime,cloud-connection)!: install-local refuses a hook with no `body` and a job `body` that does not bind, and withholds such a hook on rehydrate (#21585) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses two more kinds of package it used to install with a 200: + + - **A hook with no `body`.** A hook in the deprecated function-name `handler` form names code that travels only in an artifact's runtime module, never in the package JSON this door installs. Such a hook used to install and then either never fire or bind by name to a function the package does not ship. Every hook is judged, since a hook has no on/off switch. A hook that carries both a `body` and a `handler` installs as before: its `body` wins. + - **An enabled job whose `body` does not bind.** The door used to judge only that a job `body` was present. It now judges that the body binds, by the declaration's own parse of `JobSchema.body`, the same parse the scheduler binds by. So a job whose `body` is an expression (L1) body, or carries `body.timeoutMs`, is refused instead of installed and never scheduled. + + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, the answer the door already gives an enabled job with no `body`. One answer names everything the door cannot run: each hook and the function its `handler` names, each job and its handler, and each refused job `body` with the key the declaration refuses. Nothing is installed: nothing is registered, persisted, bound or scheduled. `os package install` exits non-zero and prints the code beside the status. + - **Rehydrate.** A package installed by an earlier version keeps rehydrating after a restart. Its body hooks bind as before. A hook of it with no `body` is reported at `warn` by name and is **not bound**: this door carries no runtime module, so the hook's `handler` can never name the package's own code. Its job with no runnable `body` is reported and not run, as before. + - **Runtime.** The binder exports the two judgements the door reads: `collectHooksWithoutBody`, and `collectJobsWithoutBody`, which also names a job whose `body` does not bind. `bindAppArtifactHandlers` takes `withholdHooksWithoutBody`, which a door that carries no runtime module sets, and reports the hooks it withheld as `withheldHooks`. + - **Unchanged:** a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) binds an app's handler hooks to its own functions exactly as before. Hooks authored through the metadata API are unchanged too. A package whose hooks carry a `body` and whose enabled jobs carry a valid `body` installs exactly as before. + + The route for a refused package: give each hook a `body` (sandboxed JS, the form actions and jobs use), and correct each job `body` to the declared shape. That shape is a sandboxed JS body whose time limit is the job's own `timeoutMs`, and `os validate` reports the same refusal. Alternatively, boot the artifact with `os start --artifact`, which loads its runtime module. This ships as `minor`, under the launch-window convention for narrowings of an accept set. +- 83e2fee: fix(runtime,cloud-connection)!: install-local refuses an enabled job whose `pull` does not bind, as it refuses a job `body` that does not bind (#21672) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package whose enabled job declares a `pull` that does not bind. It used to install such a package with a 200, and the job was never scheduled; only a server warn said so. + + - **What does not bind.** The `pull` names a mapping the package does not declare, or a mapping with no `connectorSource`, or the job declares `body` or `handler` beside its `pull`. The door judges this with the scheduler's own judgement, so the door and the scheduler cannot disagree. `defineStack` and `os validate` already refuse the same `pull`, so only a hand-edited package reaches the door with one. + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, the answer the door already gives an enabled job whose `body` does not bind. One answer names everything the door cannot run, and gives each such job the reason its `pull` does not bind, prefixed with the key it names (`pull.mapping: …`). Nothing is installed: nothing is registered, persisted or scheduled. `os package install` exits non-zero and prints the code beside the status. + - **Unchanged.** A pull job naming a declared mapping with a `connectorSource` installs and is scheduled as before. A disabled pull job does not block its install. A package installed by an earlier version still rehydrates after a restart, and its pull job that does not bind is not scheduled, with a warn naming the job and the reason, as before. + - **Runtime.** `collectJobsWithoutBody` now names an enabled job whose `pull` does not bind, and `JobWithoutBody` gains an optional `pullRefusal`: the reason the scheduler gives when it does not schedule the job. Such a job carries no `bodyRefusal`. + + The route for a refused package: declare the mapping the job's `pull` names in the package, with a `connectorSource` naming the `rest` or `openapi` connector it reads from, or correct the `pull` as the refusal says. `os validate` refuses the same `pull`. This ships as `minor`, under the launch-window convention for narrowings of an accept set. +- 93f51f1: An install-local reseed over sample rows that are all still in place answers success, with the loader's `skipped` count, instead of a refusal naming a false cause (#21776). + + Clause-②: yes (widening) + + - **Intact baseline.** `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data`, run while every seed record the package declares is already present, answers `200 { success: true, data: { manifestId, inserted: 0, updated: 0, skipped: N, errors: 0, withSampleData: true } }`. Before, it answered `422 RESEED_NO_ROWS`, "Reseed wrote no rows. The package declares no seedable records for this runtime.", over a package that declares them. The reseed is idempotent, so a run that finds every row in place has reached its goal. The install's record of sample data is set the same way as when rows land. + - **`skipped` on every success.** A successful reseed now answers all four of the loader's counts: `inserted`, `updated`, `skipped` and `errors`. Before, `skipped` was not in the response. + - **Unchanged refusals.** `422 RESEED_NO_ROWS` still answers a run that wrote nothing because records failed, with the error count and the first error, and its `details` are still `{ inserted, updated, errors }`. It also still answers, with the same text, a run in which the loader had no record to process for this runtime: for example, every dataset is scoped to another environment (`Seed.env`). That text now states a true cause. A package with no seed dataset at all still answers `400 RESEED_SKIPPED` (`no-datasets`). + +### Patch Changes + +- 1d0600b: An app installed with `os package install ` now runs its `type: 'script'` action bodies and its body hooks, and MCP `list_actions` lists a script action only when `run_action` can run it (#21321). + + Clause-②: yes (widening) + + - **`@objectstack/runtime`.** New export `bindAppArtifactHandlers(ql, bundle, { appId, logger, source? })`. It binds every action `body` of an artifact through `ql.registerAction`, and every hook `body` and bundle function through `ql.bindHooks`, all under the owner `app:`. `appArtifactHandlerOwner(appId)` returns that owner key. Each call first removes the action handlers and hooks the same owner bound before. A reinstall therefore leaves one handler per action, and an action or hook that the new version dropped stops running. `AppPlugin.start` now binds through this function, with the same log lines and the same results for a boot artifact. + - **`@objectstack/runtime`, MCP `list_actions`.** A `script` action is listed only when the engine has a handler registered for it. The check reads `listRegisteredActions()` and uses the same object and key order as `run_action`. Before, a declared `target` or `body` was enough to be listed, so `list_actions` could list an action that `run_action` refused with "No handler registered". An engine without `listRegisteredActions` gets no script actions listed. Declarative update actions and `flow` actions are listed as before. + - **`@objectstack/cloud-connection`.** The install-local plugin calls `bindAppArtifactHandlers` on `POST /api/v1/marketplace/install-local` and when it rehydrates its ledger at `kernel:ready`. Before, an installed package's script actions answered REST `404 RESOURCE_NOT_FOUND` and MCP "No handler registered", before and after a restart, and its body hooks never ran. The same artifact booted with `os start --artifact` was not affected. +- ab52182: fix(cloud-connection,plugin-security): a package installed into a running runtime fires its record-change flows and has its permission sets in `sys_permission_set` right away, not after a restart + + Clause-②: no + + **Before**, `os package install ./dist/objectstack.json` into a running `os start` (the install-local route) registered the package, bound its script actions and body hooks, and stopped there. Two things the boot does for a package happen at `kernel:ready`, and that moment had already passed. The automation engine binds flows at `kernel:ready`, so the package's record-change flows never fired: a task updated to `done` wrote no note. The security plugin seeds declared permission sets at `kernel:ready`, so the package's set had no `sys_permission_set` row. `/meta/permission` listed the set, but an admin could not grant it. A restart fixed both, because the restart re-registers the package before those two steps run. Nothing in the CLI output or the install response said a restart was needed. + + **Now** the install route announces `metadata:reloaded` once the package is registered, bound, persisted and seeded. That is the same event a Studio package publish, a per-item publish and an artifact reload already announce. The automation engine already re-syncs its flows on it. The security plugin now re-runs its declared-permission seeding on it: the same function and organization passes as the boot, with the same provenance rules (`managed_by: 'package'`, `package_id`). Right after the install, the flow fires and the set's row exists, with the same state a restart gives. The seeding is idempotent and writes nothing when no permission set changed. It runs only after the boot's own pass has finished. A failed re-sync does not fail the install. It is logged at `warn` with the restart that repairs it. + + **Unchanged.** The restart path (the ledger rehydrate) announces nothing and behaves as before. The install response and the CLI output keep their fields and text. A package's `defineStack({ jobs })` are still not scheduled by install-local, on install or after a restart, because a job's handler is code from the artifact's runtime module and an inline install carries only the JSON. +- 74281a8: fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups, so the package's permission sets and their grants go with it + + Clause-②: yes + + `DELETE /api/v1/marketplace/install-local/:manifestId` removed the package's ledger entry and nothing else. After a restart the package's objects were gone, but its `managed_by: package` rows in `sys_permission_set`, and every grant of them, survived the uninstall. That broke ADR-0090's "No ghost grants" promise on this door. + + The door now runs the uninstall cleanups that domain plugins register with the protocol (`registerUninstallCleanup`) once the ledger entry is gone. It uses the same registry and the same runner as the protocol's own uninstall, so `plugin-security`'s `security.package-permissions` cleanup removes the package's sets with their position and user bindings, and any cleanup registered later fires here too. The cleanups run with the package's manifest id and no organization, because an install-local package is installed for the whole runtime. + + The response carries each outcome as `data.cleanups`, the way the protocol's uninstall reports them. A failed cleanup is reported there and named in the operator log with its remedy (install the package again, then uninstall it again). When the protocol cannot run the cleanups, the response says so as one failed `protocol.runUninstallCleanups` outcome. An uninstall that does not happen (a refused caller, an id this door never installed, a ledger write that fails) revokes nothing. + + `@objectstack/metadata-protocol`: `ObjectStackProtocolImplementation` gains `runUninstallCleanups({ packageId, organizationId?, actor? })`, the one runner of the uninstall-cleanup registry. It runs every registered cleanup for the package and answers one `UninstallCleanupOutcome` per cleanup. It never throws: a failed cleanup is an outcome, and a thrown fault's driver text goes to the operator log, not into the outcome. `deletePackage` now calls it as its last step in place of its own loop, and its `cleanups` are unchanged. The only visible difference there is the log tag of a failed cleanup's warning, now `[protocol.runUninstallCleanups]` instead of `[protocol.deletePackage]`. + + `@objectstack/cloud-connection` now declares its dependency on `@objectstack/metadata-protocol`, which it already received through `@objectstack/runtime`, for the cleanup outcome types. +- 901e7cf: An install-local uninstall (`DELETE /api/v1/marketplace/install-local/:manifestId`) now withdraws the package from the running kernel + + Clause-②: no + + - The DELETE used to remove the ledger entry and run the uninstall cleanups, but it left the package registered in the running kernel until the next restart. So another package's hot install re-ran the declared-permission seeding over the uninstalled package too. Its permission set came back as a package-managed row, and that row survived the restart as an orphan that an administrator could grant. + - After the ledger entry is removed, the door now calls `SchemaRegistry.uninstallPackage`, the same verb the protocol's own uninstall uses, on the same registry. It does this before the cleanups run. The package's objects answer 404 straight away, not only after a restart, and no reader of the registered packages counts it again. A reinstall of the same package in the same process registers it again. + - If the registry refuses the withdrawal, for example because another package extends an object this package owns, the uninstall still succeeds and the cleanups still run. The refusal is reported as a failed `registry.uninstallPackage` entry in `cleanups`. The operator log carries the cause and the remedy. + - The response `note` no longer says the kernel cannot unregister a package in place. The request and response keys are unchanged. +- d7fff21: `POST /api/v1/marketplace/install-local/:manifestId/purge-sample-data` now deletes an installed package's sample rows. Before, it answered `500 DRIVER_UNAVAILABLE` on every runtime. + + Clause-②: no + + - **What was wrong.** The purge looked up a bare `driver` service, a name no kernel registers (drivers register as `driver.`), so it refused everywhere. Behind that it matched seed records by `id`, which seed records rarely carry: the CRM example's 28 records key by `name`, `email` and `subject`. It also deleted through the driver, past every engine hook. + - **What it does now.** It deletes through the ObjectQL engine, so lifecycle hooks and the audit trail run, under the posture the seed was written with (record-change automation suppressed). Rows are matched by each dataset's `externalId`, the key the install and the reseed upsert by. A row whose key no seed record declares is never touched. Children are deleted before parents, in the reverse of the seed loader's own dependency order. + - **Scope.** Under an organization wall the purge removes only the seed rows of the caller's active organization, the scope the install and the reseed seed into. A session with no active organization is refused with `403 PERMISSION_DENIED` and a message naming the missing active organization, the way reseed refuses it. Without a wall the deployment is one tenant, and the match is table-wide, as the install's own match is. + - **The response keeps its shape**, `{ manifestId, deleted, skipped, errors, withSampleData }`. `skipped` counts seed records no row carries (already deleted). `errors` counts records that could not be purged, each with its reason in the server log: a delete the engine refused (for example, a user's row still requires the seed row as its parent), a key that more than one row carries, or a seed record with no key value. + - A runtime with no data engine or no metadata service still answers `500 DRIVER_UNAVAILABLE`, now naming what is missing. +- 75ddcd1: `POST /api/v1/marketplace/install-local` now runs the ADR-0087 D1 protocol handshake. A manifest whose declared range excludes this runtime's protocol major is refused with `422 OS_PROTOCOL_INCOMPATIBLE`, the answer `POST /api/v1/packages` already gives. It used to install with a `200` (#21762). + + Clause-②: yes (widening) + + - **Install.** The handshake runs after the manifest id is parsed and before anything is registered, written or synced. The range is read from `engines.protocol`, then `engines.platform`, then `engine.objectstack`. The refusal answers `422` with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'`, the handshake's own `error.message`, and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. It is the same on the inline-manifest branch and the cloud-snapshot branch. No ledger file is written, and an installed earlier version stays as it was. A manifest with no range, or a range the handshake cannot read, still installs, and the handshake's warning goes to the plugin's logger. + - **Restart.** On `kernel:ready`, a ledger entry whose range excludes this runtime's major is not loaded. Nothing is registered, synced, bound or seeded for it. One `error` line names the package, `OS_PROTOCOL_INCOMPATIBLE` and the replay command (`objectstack migrate meta --from N`). The boot continues with the other entries. The entry stays in the ledger, so `DELETE /api/v1/marketplace/install-local/{id}` still removes it, and installing a compatible version replaces it. Before, it was registered and its schemas synced, with no warning. + - **`@objectstack/metadata-core`:** a new export, `protocolIncompatibleAnswer(err)`, with its return type `ProtocolIncompatibleAnswer`. It turns a `ProtocolIncompatibleError` into the status, code, message and five-member `details` an HTTP door answers. Both install doors call it, so their answers are the same bytes. + - **`@objectstack/runtime`:** `POST /api/v1/packages` answers through that helper. Its response is unchanged. + + A client that relied on install-local accepting a package built for another protocol major gets `422` now. Install a version built for this runtime's protocol, or migrate the package with the `migrateCommand` in the refusal. +- e09f1ac: Under an organization wall, the install-local sample-data doors now refuse a session with no active organization, and the refusal names what is missing (ADR-0123 D2 / D4). Before, they skipped quietly. + + Clause-②: no + + - **Reseed and purge.** `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data` and `…/purge-sample-data` answer `403 PERMISSION_DENIED`, with a message saying the session has no active organization and that one must be joined or selected. Before, the reseed answered `400 RESEED_SKIPPED` (`multi-tenant-no-active-org`); the purge, which starts deleting in this same release, refuses the same way from the start. Reseed's other declines are unchanged and still answer `400 RESEED_SKIPPED`: a package with no seed datasets, a runtime with no data engine or metadata service, and a seed run that threw. + - **Install.** `POST /api/v1/marketplace/install-local` still installs the package, which is environment-wide. Its `seeded` block now reads `{ mode: "refused", reason: "…" }`, where `reason` names the missing active organization and says to select one and then reseed. Before, it read `{ mode: "skipped", reason: "multi-tenant-no-active-org" }`. + - **No organization is guessed.** The active-organization read no longer falls back to the user's first membership. ADR-0123 D1 makes "authenticated, with no active organization" a declared state. A guess would write into an organization the caller never chose. The fallback read an object no package defines, so it never resolved anything. + - **Unchanged.** A session with an active organization seeds, reseeds and purges in that organization, as before. Without a wall (`single` posture), no organization is read, and the three doors act table-wide. +- c4d5713: `GET /api/v1/marketplace/install-local` now answers each entry's `withSampleData` for the caller's own organization. Before, after a purge in organization A, the listing read as organization B answered `withSampleData: false` while B still held every one of its seed rows. + + Clause-②: no + + - **What was wrong.** The listing served the install ledger's `withSampleData`, one value per install. Under an organization wall, sample data is per organization: the install, the reseed and the purge each act in the caller's active organization. A purge in A flipped the one value for every organization, and a restart kept it. + - **What it does now.** The listing reads the rows. An entry answers `true` when at least one of the package's seed rows is in the caller's scope. Rows are matched the way the purge matches them, by each dataset's `externalId`. "At least one" is exactly when the purge has something to delete, and it decides whether the console labels its reseed action "Add sample data" or "Reseed again". The purge's matching is now a separate read-only step, and the purge deletes what it returns, with the same counts and log lines as before. + - **Scope.** Under a wall, the scope is the caller's active organization. A session with no active organization reads nothing, so every entry answers `false` with `200`, and no row is read. Without a wall, the match covers the whole table, and the answer is the one the ledger records after an install, a purge and a reseed. + - **When the rows cannot be read** (for example, a package the runtime did not load), the entry answers `false`, and the server log says why at `warn`, once per entry per request. + - **The response keeps its shape.** The ledger keeps its shape too. Its `withSampleData` and `sampleDataPurged` stay as install-time records, and their docs now say they are not per organization. +- 48297ad: `GET /api/v1/marketplace/install-local` now marks an installed package that this runtime refused to load. Before, after a restart whose rehydrate refused a package built for another protocol major, the listing served it like any loaded package, and the console's Installed Apps showed it as installed. + + Clause-②: no + + - **What was wrong.** On a restart, a ledger entry whose `engines.protocol` range excludes this runtime is not loaded, and the boot logs `OS_PROTOCOL_INCOMPATIBLE` at `error`. The entry stays in the ledger, so `DELETE` and a compatible re-install still act on it. The listing served it with the same fields as a loaded package. Each request also tried to read its seed rows from objects that were never registered, and logged a `warn` saying it could not. + - **What it does now.** That entry is listed with `"notLoaded": { "code": "OS_PROTOCOL_INCOMPATIBLE", "requiredRange": "^16" }` (the range the package declares) in place of `withSampleData`. No seed row is read for it, so the per-request `warn` is gone. `notLoaded` has exactly these two members, and every authenticated caller sees it. + - **Unchanged.** A loaded package's entry is exactly as before, with no `notLoaded` key. `DELETE /api/v1/marketplace/install-local/:manifestId` removes a marked entry as before, and once a compatible version is installed over it, the entry is listed as loaded. + - **Where the marker comes from.** The rehydrate records each entry it refuses, and the listing reads that record. The listing does not run the protocol check again. +- 9f9510f: `reseed-sample-data` and `purge-sample-data` on an installed package that this runtime refused to load now answer `422 OS_PROTOCOL_INCOMPATIBLE` before they change anything. Before, both acted on such a package anyway. + + Clause-②: no + + - **What was wrong.** On a restart, a ledger entry whose `engines.protocol` range excludes this runtime is not loaded: nothing is registered, synced, bound or seeded for it. `POST /api/v1/marketplace/install-local/:manifestId/reseed-sample-data` on that entry loaded the package's translations into the i18n service and merged its seed datasets into the kernel's shared `seed-datasets` list, and then failed with `400 RESEED_SKIPPED` because the package's objects were never registered. `POST …/:manifestId/purge-sample-data` answered `200` with every record counted in `errors`, and set the ledger's `withSampleData` to `false` with no row deleted. + - **What it does now.** Both doors run the protocol check on the ledger entry right after reading it. An entry whose declared range excludes this runtime gets the answer the install route gives the same manifest: `422`, `error.code` `OS_PROTOCOL_INCOMPATIBLE`, the check's own message, and `error.details` with `requiredRange`, `rangeSource`, `protocolVersion`, `targetMajor` and `migrateCommand`. No translation is loaded, no dataset is merged, no seed row is read or deleted, and the ledger is not written. The refusal comes before the organization check too, so a session with no active organization on a walled deployment also gets the `422` for such an entry. + - **Unchanged.** An entry this runtime loads is answered exactly as before. An entry that declares no range, or a range the check cannot read, is admitted as before, with no new warning. `DELETE /api/v1/marketplace/install-local/:manifestId` still removes a refused entry, and installing a compatible version over it makes both doors act on it again. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [0e10be6] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [535d1d2] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [e9dec3d] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [ce53218] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [74281a8] +- Updated dependencies [9a4182a] +- Updated dependencies [550f4cc] +- Updated dependencies [41b1333] +- Updated dependencies [5dbcee8] +- Updated dependencies [ec390ec] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [bd70706] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [aa0d4b9] +- Updated dependencies [5d0e4e2] +- Updated dependencies [e367002] +- Updated dependencies [9e9d693] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [7b07749] +- Updated dependencies [eea82af] +- Updated dependencies [a2aadab] +- Updated dependencies [ced217c] +- Updated dependencies [ff16740] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [7fd2c34] +- Updated dependencies [c43a8ae] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [cf60dbc] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [025008a] +- Updated dependencies [b7a13c7] +- Updated dependencies [045f764] +- Updated dependencies [18c2ddc] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [18fe681] +- Updated dependencies [3237b4a] +- Updated dependencies [088428f] +- Updated dependencies [2e78046] +- Updated dependencies [cab6396] +- Updated dependencies [87712ab] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [9cc2c79] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [753e7a1] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 39cc5a1f741..f2757402ae2 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Runtime-side client for an ObjectStack cloud control plane — marketplace browse proxy, install-local, device-code binding, org catalog and installed views, and the /api/v1/runtime/config discovery endpoint. Open mechanism (cloud ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index 3f349faf7e3..c9ff05c2a69 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,135 @@ # @objectstack/connector-mcp +## 17.7.0 + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index bf0b5fcc1f3..02a8cc55511 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).", "main": "dist/index.js", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index 6089c09110e..bba801f52b6 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/connector-openapi +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 00b69dd360d..60057fd40e8 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).", "main": "dist/index.js", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index 642aee072bd..67f42565966 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/connector-rest +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index e0d8715107a..32cfaf55ffb 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).", "main": "dist/index.js", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index e6ff67cf442..b2ab15e5b46 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/connector-slack +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 3c4df33a812..d2f961f2d60 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Slack Web API connector for ObjectStack — registers `chat.postMessage` / `chat.update` / `call` actions on the automation engine's connector registry (ADR-0018 §Addendum, ADR-0022).", "main": "dist/index.js", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index a5e17a78869..03d0c0b3fd9 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,433 @@ # @objectstack/console +## 17.7.0 + +### Minor Changes + +- 8832655: Console (objectui) refreshed to `0abd4f9f8769`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 3 releasing of 5 changesets added across 5 non-merge commits; omitted: 2 release-nothing changesets (they ship no package code). + + - **minor** — `@object-ui/types` declares each renderer's NODE SLOTS once (`NODE_SLOT_DECLARATIONS`, `nodeSlotsFor`), and `objectui check`, core `validateSchema`, the SDUI parser's `validateTree` and the `kind:'html'` page compile walk those slots as well as `children` (objectui#11170). Its changeset declares `Clause-②: yes (narrowing)`: a node under a slot that was never judged is judged now. (objectui `c4c506b9e`) + - **minor** — The screen-flow runner names the flow by its label, in the user's language (objectui#11092, the objectui half of objectstack#20318). (objectui `39a3e91fa`) + - **patch** — The External Datasource panel in Setup and Studio reads the `{ success, data }` envelope its routes answer (objectui#11628). On a federated datasource such as the showcase's `show… (objectui `0abd4f9f8`) + + No objectui commit in the range carries `!`, and no changeset in it declares `major` or carries the breaking annotation. objectui#11170's narrowing is objectui's own validation reach and changes no ObjectStack-authorable key. The manifest this repository ships is generated without `slotsFor`, so its entries carry no slot list. The release-nothing pair is objectui#11396, which derives `MasterDetailDetailConfig` from the spec's `details` entry by reference, member for member the same, and objectui#11095, a KPI-tile invalidation pin. + + objectui range: `9dfaca654311...0abd4f9f8769` +- 100f68b: Console (objectui) refreshed to `2e818d0b51ec`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 17 releasing of 18 changesets added across 22 non-merge commits; omitted: 1 release-nothing changeset, 6 commits carrying no changeset (they ship no package code). + + - **minor** — `FlexBlockNode`, the TypeScript type of an authored `flex` node, types its bag's child list as nodes: `properties.children` is `SchemaNode | SchemaNode[]`, where it was `unknown[]… (objectui `b10c68e58`) + - **minor** — The dashboards' metric card node, `plugin-dashboard:metric`, is a declared node type, and both dashboard surfaces hand `SchemaRenderer` declared nodes with no cast (objectui#11466… (objectui `83e3f8377`) + - **minor** — **BREAKING** — **An `object-metric` node in a dashboard widget's legacy `component` envelope now draws the retired-format prompt instead of its number (objectui#11466).** This follows the mainta… (objectui `83e3f8377`) + - **minor** — **BREAKING** — A node slot and `SchemaRenderer`'s `schema` prop take the union of the declared node types, `DeclaredNode` (objectui#11466). (objectui `83e3f8377`) + - **minor** — `safeValidateSchema`, and so `objectui validate`, accepts `record:line_items`, the last ADR-0080 public block it refused at `type` (objectui#10872). `@objectstack/spec` 17.6.0 car… (objectui `8d0ca9183`) + - **minor** — `ObjectKanbanSchema.grouping` is declared on both faces, as `@objectstack/spec`'s `GroupingConfig`, by reference (objectui#11216). (objectui `a7557a7d4`) + - **patch** — The dashboard surfaces read every `widgets[]` entry without leaning on `BaseSchema`'s index signature: a widget key is read on the widget arm alone, and the `chart` node is built… (objectui `2e818d0b5`) + - **patch** — A refused save, pin, reorder, view setting, report save, publish or discard in the console is now said to the user, and the view-config panel no longer reports a refused save as s… (objectui `e8c0b9614`) + - **patch** — The `object-grid` summary footer reads `currency`, `defaultCurrency`, `precision` and `scale` from the object field only. A column that carries one of these keys no longer changes… (objectui `d768c3178`) + - **patch** — fix(types): `AnyComponentSchema`'s declaration prints every category union by name, so `@object-ui/types` no longer sits at the edge of TypeScript's serialization ceiling (objectu… (objectui `bdc9049ed`) + - **patch** — "Save as view" now saves a Kanban view the platform accepts, and both Create View doors save the same view from the same dialog choices (objectui#11581). (objectui `b65aa5e65`) + - **patch** — The console and runner stylesheets compile only from their declared `@source` lines. Tailwind's automatic source detection is now off (`source(none)`), as it already was for `@obj… (objectui `5a2ca6b12`) + - **patch** — A refused view save is now said to the user, and the Create View dialog no longer closes as if the view were saved (objectui#11578). (objectui `f1c937966`) + - **patch** — The console's Create View dialog now creates chart views the platform accepts (objectui#11576). (objectui `d0097af2e`) + - **patch** — fix(plugin-grid): switching a server-grouped grid's grouping field shows exactly the new field's groups (objectui `2f54dca53`) + - **patch** — fix(app-shell): an interface page relays its source view's `tree` and `chart` blocks, and its own `allowPrinting` (objectui#11572) (objectui `09036173c`) + - **patch** — A stored list view now renders the same on an interface page and on the object page when it carries a legacy `options` bag (objectui#10380). (objectui `068564691`) + + ⚠️ 2 of these carry a breaking change: 2 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 6 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ fix(release): the publish lane pushes the version's git tags and creates its GitHub Releases itself — changesets/action@v1 finds no `New tag:` line under CLI v3 (objectui#11596) (… (objectui `973fc20f3`) + - _(no changeset)_ docs(skills): `schema-expressions.md` states `{ condition, style }` as the only authorable conditional-formatting rule on every list carrier (objectui#11534) (#11595) (objectui `94985a92b`) + - _(no changeset)_ chore: release packages (#5400) (objectui `b493919c7`) + - _(no changeset)_ docs(plugin-detail): the reference-rail README says `entries` go in the `properties` bag, and that the validator refuses a flat one (objectui#10872) (#11584) (objectui `7a7660c65`) + - _(no changeset)_ docs(skills): the page-builder guide's object-form example names its fields; a per-form override goes on a section entry (objectui#11550) (#11561) (objectui `0a53c67f9`) + - _(no changeset)_ docs(skills): type three marked fences' schema as SchemaRendererProps['schema'] (objectui#11543) (#11558) (objectui `5cde2c6fe`) + + + + objectui range: `ab1879721595...2e818d0b51ec` +- 8963dbf: Console (objectui) refreshed to `89cad75d5570`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 74 releasing of 88 changesets added across 64 non-merge commits; omitted: 14 release-nothing changesets, 6 commits carrying no changeset (they ship no package code). + + - **minor** — **BREAKING** — chore(console)!: drop the lazy `tree` registration stub (objectui#10859, batch 8) (objectui `990a2d616`) + - **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the thirty node type keys objectui#10859 batch 8 retired (objectui `990a2d616`) + - **minor** — **BREAKING** — chore(core)!: the record-source `data` arm table drops `tree` and `view:tree` (objectui#10859, batch 8) (objectui `990a2d616`) + - **minor** — **BREAKING** — refactor(fields)!: the 28 field widgets that still registered a bare node-type fallback register `field:` only (objectui#10859, batch 8) (objectui `990a2d616`) + - **minor** — **BREAKING** — refactor(plugin-tree)!: retire the bare `tree` node type key; `object-tree` is the one spelling (objectui#10859, batch 8) (objectui `990a2d616`) + - **minor** — **BREAKING** — refactor(plugin-view)!: retire the bare `view` node type key; `object-view` is the one spelling (objectui#10859, batch 8) (objectui `990a2d616`) + - **minor** — Three more reader sites stop riding `BaseSchema`'s index signature (objectui#11355 round 2, part of the preparation for objectui#8347's removal of that signature). None changes ru… (objectui `31987bd50`) + - **minor** — **BREAKING** — feat(types): the six `@object-ui/plugin-designer` node types validate; `ProcessDesignerSchema.variables` and `ReportDesignerSchema.parameters` leave the TypeScript face (objectui#… (objectui `063832222`) + - **minor** — **BREAKING** — BREAKING (`@object-ui/core`): `mergeAuthoredPresentation` and `axisPresentation` are no longer exported (objectui#11372). (objectui `f9c8c4e45`) + - **minor** — **BREAKING** — A `page` node refuses `maxWidth` and `padding` by name, and the layout guide teaches the controls that work: `pageType` for the page's width, a `container` for a narrower column o… (objectui `a1a44d621`) + - **minor** — `object-timeline` and `view:timeline` publish the ten `@objectstack/spec` 17.5.0 row keys their renderer honours, and `objectName` is no longer required (objectui#11168 slice 5, u… (objectui `6cd5ae3ea`) + - **minor** — The console build now writes `dist/sdui.manifest.json`, the SDUI component manifest of the Console it built (objectui#11403). (objectui `f88a900e7`) + - **minor** — A bind-only `list` is accepted: `ListSchema.items` is optional on both faces, and the zod face requires at least one of `bind` / `items` (objectui#11405). (objectui `9547063da`) + - **minor** — **BREAKING** — feat(core): the `flex()` builder emits its props in the `properties` bag (objectui#11276) (objectui `138ad4554`) + - **minor** — **BREAKING** — feat(types): an authored `flex` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#11276) (objectui `138ad4554`) + - **minor** — **BREAKING** — feat(types): an authored `object-grid` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#11276) (objectui `6aa029b63`) + - **minor** — **BREAKING** — objectui's app document refuses `mobileNavMode` by name, the answer the platform already gives (objectui#11363). (objectui `e100589f3`) + - **minor** — fix: the widget width / height editors write a whole four-number `layout` (objectui#11388) (objectui `6e9c8d27e`) + - **minor** — **BREAKING** — A gate that is declared but cannot be evaluated is a fault, not "no gate" (objectui#11358) (objectui `063119f2b`) + - **minor** — A public block's prop written directly on the node, instead of inside its `properties` bag, is refused by name on both faces, with a message naming `properties.KEY` (objectui#1087… (objectui `b5696d344`) + - **minor** — Five label positions that `@objectstack/spec` types as `I18nLabel` now accept the per-locale map in `@object-ui/types` too, where they were typed `string` (objectui#10993, batch 4… (objectui `b4075c088`) + - **minor** — feat(plugin-gantt): `object-gantt` publishes the eleven `@objectstack/spec` row keys its renderer honours (objectui#11168 slice 4) (objectui `8673402a3`) + - **minor** — The strict authoring face accepts the `layout` that the editable dashboard grid's Save Layout writes onto a `metric-card` in a dashboard's widget slot (objectui#11070, round 11).… (objectui `0a78a20c8`) + - **minor** — The spec's page blocks, the `element:text_input` / `element:record_picker` rows and a stored page document under its page kind have a TypeScript authoring type, and `SchemaRendere… (objectui `304f61137`) + - **minor** — Small reader sites stop riding `BaseSchema`'s index signature (objectui#11355, part of the preparation for objectui#8347's removal of that signature). Each key was measured on its… (objectui `3c3ce15a7`) + - **minor** — Declare `pageSize` on `ObjectDataTableSchema`, on both faces (objectui#11348). (objectui `6c3da53ae`) + - **minor** — A form field of `type: 'grid'` declares the grid widget's field-level keys (objectui#11070, round 10). (objectui `edfcf5a5e`) + - **minor** — The grid field's `sort_field` is declared, and a master-detail detail's sort field is derived only (objectui#11070, round 9). (objectui `0a3e5409f`) + - **minor** — `formatMetadataError` and `formatMetadataIssue` are exported from `@object-ui/data-objectstack`: the one reader of a failed metadata save (objectui#11302). (objectui `d89329033`) + - **minor** — `object-map` publishes the three keys its `@objectstack/spec` 17.5.0 row declares and its registration left out: `mapStyle`, `navigation` and `enableClustering` (objectui#11168 sl… (objectui `20d23befe`) + - **minor** — `object-tree` publishes the keys its `@objectstack/spec` 17.5.0 row declares and its renderer honours (objectui#11168 slice 3, objectui#11111 decision 3 = B). Each key was measure… (objectui `20d23befe`) + - **minor** — `ObjectTreeSchema` mirrors the `object-tree` row of `@objectstack/spec` 17.5.0 (objectui#11168 slice 3). The change applies to both faces, TypeScript and zod. (objectui `20d23befe`) + - **minor** — `UIActionSchema.size` takes the `action:button` row's vocabulary by reference (objectui#11168 slice 3). Before this, the type was `'sm' | 'md' | 'lg'`. That made `size: 'default'`… (objectui `20d23befe`) + - **minor** — Eight renderers stop riding `BaseSchema`'s index signature for node keys their types did not declare (objectui#11347, the `@object-ui/components` preparation for objectui#8347's r… (objectui `c82ff391f`) + - **minor** — **BREAKING (rendering):** a dataset-bound dashboard widget no longer reads `chartConfig.series`, `chartConfig.xAxis` or `chartConfig.yAxis` (objectui#11315). (objectui `1a88ce22f`) + - **minor** — **BREAKING (authoring, TypeScript only):** on a dashboard widget, `chartConfig.type`, `chartConfig.xAxis`, `chartConfig.yAxis` and `chartConfig.series` are now compile errors, the… (objectui `1a88ce22f`) + - **minor** — The grid field reads each field-level key under the one spelling `GridFieldMetadata` declares (objectui#11070, round 8). (objectui `55a12a8e1`) + - **minor** — A region-tagged language code reaches the built-in catalogue of its base language (objectui#11326) (objectui `d0fba91aa`) + - **minor** — The grid field's `columns` is `@objectstack/spec`'s inline grid column list, by reference, and `object-chart` declares the per-element `dataSource` binding like the other gate-wra… (objectui `75dcc81c3`) + - **minor** — A custom page publishes the console's record navigator to the blocks placed on it (objectui#11293). (objectui `2124d0411`) + - **minor** — A standalone `object-calendar` honours `navigation: { mode: 'page' }`, and a `navigation` block written without `mode`, by opening the record page (objectui#11293). (objectui `2124d0411`) + - **minor** — A standalone `object-kanban` honours `navigation: { mode: 'page' }`, and a `navigation` block written without `mode`, by opening the record page (objectui#11293). (objectui `2124d0411`) + - **minor** — `useNavigationOverlay` hands an authored `page` click with no `onNavigate` to the record navigator the host publishes (objectui#11293). (objectui `2124d0411`) + - **patch** — fix(fields): a read-only number field shows its value the way its table cell does (objectui#11431) (objectui `52c95a166`) + - **patch** — fix(i18n): every count plural family carries every plural form its language uses (objectui#11432) (objectui `55d18c649`) + - **patch** — fix(plugin-dashboard): a dimensioned `pie` / `donut` / `funnel` / `treemap` / `sankey` widget with several measures now says which measures it drops (objectui#11417) (objectui `175df47ef`) + - **patch** — `AiUsageIndicator` renders the reset line for the rolling 5-hour pace window, `resetKind: 'fiveHour'` (objectui#11415, consumer of cloud#2059 / cloud#2574). (objectui `c681b9ff2`) + - **patch** — The `object-calendar` / `calendar` `navigation` input description said `openNewTab: true` "outranks the mode". That does not hold for `none`: `useNavigationOverlay` checks `mode =… (objectui `6cd5ae3ea`) + - **patch** — The `object-kanban` `navigation` input description said `openNewTab: true` "outranks the mode". That does not hold for `none`: `useNavigationOverlay` checks `mode === 'none'` befo… (objectui `6cd5ae3ea`) + - **patch** — fix(plugin-dashboard): a dimensionless `column` / `horizontal-bar` draws every measure; the dropped-measure warning speaks whenever the widget's own branch leaves a declared measu… (objectui `db0e9d3a0`) + - **patch** — fix(plugin-designer): the dashboard editor's type picker no longer turns a multi-measure widget into a type the widget door refuses (objectui#8894) (objectui `db0e9d3a0`) + - **patch** — A host feed slot written on a `record:activity` or `record:history` node is refused by name: `items` and `entries`, and the `loading` flag paired with each (objectui#11321). (objectui `e0a9c6760`) + - **patch** — fix(plugin-gantt): a number row in the gantt tooltip shows the field's declared decimals, and none when it declares none (objectui `c1763e50c`) + - **patch** — fix(fields): the number cell ignores a malformed `scale` instead of flooring it or crashing (objectui `c1763e50c`) + - **patch** — The dataset designer no longer writes `field: ''` for a row whose Field box is blank (objectui#11402). (objectui `0858267e4`) + - **patch** — fix(layout): the mobile tab bar draws its tabs in the sidebar's order, and shows an entry's badge (objectui `7728c67c8`) + - **patch** — docs(plugin-grid): authored `object-grid` examples write their props in the `properties` bag (objectui#11276) (objectui `6aa029b63`) + - **patch** — fix(app-shell): a refused metadata save shows the server's message and field path on every transport (objectui `d59f11c0d`) + - **patch** — fix(layout): the mobile tab bar draws only the entries its sidebar draws (objectui `5ad9f5dc8`) + - **patch** — fix(app-shell): a published html page that gains a plugin component can be published again from the Studio (objectui `3ae919307`) + - **patch** — fix(app-shell): a datasource created as External or Validate only, or switched to either from Managed, now saves without a credential (objectui#11368) (objectui `8001068b9`) + - **patch** — The Studio surfaces import `formatMetadataError` from `@object-ui/data-objectstack`, where the reader now lives (objectui#11302). What they show is unchanged; the publish-failure… (objectui `d89329033`) + - **patch** — `MetadataFieldsPage` shows the per-field prescription when the spec refuses a save, not only the refusal headline (objectui#11302). (objectui `d89329033`) + - **patch** — `object-map` reads `mapStyle` before `map.style`, as `@objectstack/spec`'s `object-map` row says in `mapStyle`'s own description ("Read before `map.style`"). This is objectui#1116… (objectui `20d23befe`) + - **patch** — The page-block inspector labelled the `object-form` `columns` field "Columns (grid layout)" in English and 「列数(网格布局)」 in Chinese. That pointed at the `grid` form layout, which obj… (objectui `20d23befe`) + - **patch** — The `object-timeline` / `view:timeline` `navigation` input description had three wording errors, and all three are corrected (objectui#11168 slice 3, from the contract record on o… (objectui `20d23befe`) + - **patch** — The README's "View tabs" section listed `form.layout` as `vertical | horizontal | inline | grid`. It now lists `vertical | horizontal`, the two values the form layout keeps after… (objectui `20d23befe`) + - **patch** — fix(plugin-gantt): a percent row in the gantt tooltip shows the field's declared decimals (objectui `b149617e6`) + - **patch** — fix(plugin-dashboard): the `object-metric` tile shows a percent or number aggregate at the field's declared width (objectui `b149617e6`) + - **patch** — fix(plugin-grid): the mobile card's percent value shows the field's declared decimals (objectui `b149617e6`) + - **patch** — fix(layout): the mobile tab bar opens the same page as the sidebar (objectui#11211) (objectui `c18a0754b`) + - **patch** — A bulk action whose `visible` is blank now shows on the grid's selection bar and runs over every selected record, as it already does on the row menu and the toolbars of the same g… (objectui `5638529e6`) + - **patch** — Docblock only: `BulkActionDef.visible` now says what the grid's selection bar does with an `ast`-only envelope (objectui#11322). (objectui `5638529e6`) + - **patch** — Docblock only, no behavior change: `partitionRowsByPredicate` now names its callers and says who decides "is a gate declared?" (objectui#11322). (objectui `5638529e6`) + + ⚠️ 16 of these carry a breaking change: 16 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 6 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ docs(fields): the number page and catalog teach `scale` for the decimal width (objectui#11413) (#11429) (objectui `01f99e31e`) + - _(no changeset)_ docs(fields): the percent page and catalog teach `scale` for the decimal width (objectui#11255) (#11411) (objectui `549aaa831`) + - _(no changeset)_ docs(skills): the page-builder guide authors object-grid and object-gantt in the properties bag (objectui#10859; objectui#11276 rider) (#11404) (objectui `64c173d70`) + - _(no changeset)_ docs(skills): the mobile guide teaches mobileNavMode where it is read, not on the app schema (objectui#11363) (#11397) (objectui `abca9867e`) + - _(no changeset)_ fix(site): move next 16.3.3 to 16.3.6 for GHSA-vcvr-r3jv-pc5j (critical) (#11361) (objectui `ad58cc159`) + - _(no changeset)_ docs(guide): slotted-pages header example keeps only PageHeaderProps keys; pin it (objectui#11165) (#11339) (objectui `743181a48`) + + + + objectui range: `31971ff1e28f...89cad75d5570` +- 1354e7b: Console (objectui) refreshed to `9dfaca654311`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 24 releasing of 24 changesets added across 17 non-merge commits. + + - **minor** — **BREAKING** — The default (`simple`) `object-form` draws a self-describing inline section entry, as the `tabbed`, `wizard`, `split`, `drawer` and `modal` forms already did (objectui#11615). Bef… (objectui `9dfaca654`) + - **minor** — The `record:related_list` registration no longer declares `columns` required, so the page compile accepts a related list that lists no columns of its own (objectui#11613). (objectui `4c127cdef`) + - **minor** — The record page header draws the record's picture beside its title, from the field the object names in its object-level `imageField` (`@objectstack/spec` 17.6.0, objectstack#21182… (objectui `c096f0327`) + - **minor** — **BREAKING** — A form view's `subforms[].columns` entry is judged by `@objectstack/spec`'s `InlineGridColumnSchema` now, by reference, so `objectui validate` and `os validate` give one verdict o… (objectui `9db9ff3f9`) + - **minor** — **BREAKING — `PartialSchema` is RETIRED from `@object-ui/types`** (objectui#11608, enforce-or-remove). The utility type leaves the `.` entry, the one entry that published it, w… (objectui `8b14aecbd`) + - **minor** — **BREAKING** — The `grid` field's eight field-level keys are camelCase now, and their snake_case spellings are retired and refused by name on every face (objectui#11610). (objectui `2abec3a96`) + - **minor** — **`BaseSchema` no longer declares `[key: string]: any`** (objectui#8347, executing the objectui#7927 ruling: the TypeScript face is a contract). Every node type extends `BaseSchem… (objectui `b403bb36f`) + - **minor** — All ten locale packs gain `view.noObject`, the hint an object-bound block shows when its node names its object in neither place (objectui#11605). (objectui `fd060f076`) + - **minor** — The `object-chart` and `view:chart` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object, and a ch… (objectui `fd060f076`) + - **minor** — The `object-metric` and `object-pivot` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object, and a… (objectui `fd060f076`) + - **minor** — The `object-form`, `view:form`, `embeddable-form` and `object-master-detail-form` registrations no longer declare `objectName` required, so the page compile accepts a node whose `… (objectui `fd060f076`) + - **minor** — The `object-grid` and `view:grid` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object (objectui#1… (objectui `fd060f076`) + - **minor** — The `object-kanban` registration no longer declares `objectName` required, so the page compile accepts a board whose `dataSource` binding names the object, and a board that names… (objectui `fd060f076`) + - **minor** — The `list-view` and `view:list` registrations no longer declare `objectName` required, so the page compile accepts a node whose `dataSource` binding names the object, and a list t… (objectui `fd060f076`) + - **minor** — `ElementDataSourceGate` takes a `requiresObject` prop: when a placement opts in and its node names its object in neither place, the gate renders a short "no object named" hint ins… (objectui `fd060f076`) + - **minor** — A Studio form field that declares the `ref:dataset` widget now renders a dataset picker instead of the JSON editor fallback (objectui#11601). (objectui `b508ac50d`) + - **minor** — The console asks `GET /api/v1/usage/storage` only when the runtime serves it (objectui#11002). On a self-hosted or open-source runtime, an environment admin's console used to requ… (objectui `d2e859936`) + - **minor** — The `record:line_items` registration no longer declares `childObject` required, so the page compile accepts a node whose `dataSource` binding names the child object (objectui#1156… (objectui `902ebab63`) + - **patch** — `deriveColumns`, the default columns of a master-detail inline grid whose author listed none, now takes which columns it draws, their order and which of them are `defaultHidden` f… (objectui `15f67025b`) + - **patch** — An object page's view tab, and the breadcrumb that names the open view, draw the label of a view the object document embeds as the server served it (objectui#11336). (objectui `6e9090c26`) + - **patch** — The docs portal's book sidebar now shows what the book resolver answers, with nothing narrowing the docs in front of it (objectui#11340, ADR-0046 §6.4). The resolver decides book… (objectui `7c9a6b194`) + - **patch** — Studio's "Organization flows" page no longer says its drafts publish atomically, and a deep link to a flow that is not on the page no longer says no metadata designers are registe… (objectui `b92329c89`) + - **patch** — On a read-only package, a click on a flow canvas node in Studio Automations selects the node and opens its inspector read-only again (objectui#11546). (objectui `278d2444e`) + - **patch** — fix(plugin-detail): `record:details` read mode shows a `textarea` value with its line breaks (objectui#11577) (objectui `b61c116b2`) + + ⚠️ 4 of these carry a breaking change: 4 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + + + objectui range: `2e818d0b51ec...9dfaca654311` +- 1cbe165: Console (objectui) refreshed to `ab1879721595`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 111 releasing of 119 changesets added across 65 non-merge commits; omitted: 8 release-nothing changesets, 1 commit carrying no changeset (they ship no package code). + + - **minor** — Studio reaches the organization's own flows that belong to no package (objectui#11553). (objectui `f624f278d`) + - **minor** — `record:line_items` now publishes every key of its `@objectstack/spec` 17.6.0 row (objectui#11536). Each key was decided by measuring it through `SchemaRenderer` and the block's r… (objectui `072b7e843`) + - **minor** — The import wizard's "Download template" button now downloads the server's import template, an Excel workbook, instead of building a CSV of every field (objectui#9600). (objectui `b253c4e28`) + - **minor** — The Studio dataset filter builder writes "Is empty" / "Is not empty" as the spec's `{ FIELD: { $empty: true } }` / `{ FIELD: { $empty: false } }` instead of `$exists` (objectui#10… (objectui `d0c0c7fe9`) + - **minor** — `FilterConditionField` writes "Is empty" / "Is not empty" as the spec's one 「is empty」 operator, `{ FIELD: { $empty: true } }` / `{ FIELD: { $empty: false } }` (objectui#10813). (objectui `d0c0c7fe9`) + - **minor** — The list view's live query sends "Is empty" / "Is not empty" as the spec's `isempty` / `isnotempty` instead of an equality to `null` (objectui#10813). `@objectstack/spec` 17.6.0 a… (objectui `d0c0c7fe9`) + - **minor** — An `object-grid` honours `keyboardNavigation`: arrow-key cell navigation on the WAI-ARIA grid pattern (objectui#11068). `@objectstack/spec` 17.6.0 declares the key on its `object-… (objectui `154075ab1`) + - **minor** — The action success toast is composed from the action's `outcomeMessages`, then its `successMessage`, then the runner's default text. A `message` in the server's answer is no longe… (objectui `c476be0e0`) + - **minor** — The drill `filter[...]` URL dialect can spell "is empty" (objectui#11547). (objectui `7121221fa`) + - **minor** — `ValueDataSource` executes the empty pair `is_empty` / `is_not_empty` and the `$empty` operator, and `convertFiltersToAST` lowers `$empty` (objectui#11094). (objectui `6158e4c93`) + - **minor** — An `object-grid` publishes `description` and `emptyState` now that `@objectstack/spec` 17.6.0 declares them on its `object-grid` row, and `emptyState.title` / `.message` take an i… (objectui `6158e4c93`) + - **minor** — **BREAKING for authors of `object-grid` and `list-view` row rules, released as `minor`.** `conditionalFormatting` on `ObjectGridSchema` (and so on the `object-view` `table` slot b… (objectui `6f5719e1c`) + - **minor** — **BREAKING** — **A dashboard metric widget bound inline to an object now shows the retired-format prompt instead of its number (objectui#11525).** This follows the maintainer's ruling C on objec… (objectui `160c6c6ea`) + - **minor** — **BREAKING for authors of `object-kanban` card rules, released as `minor`.** `object-kanban`'s `conditionalFormatting` takes ONE rule dialect, the spec list view's `{ condition, s… (objectui `c73cdb569`) + - **minor** — `DashboardRenderer`'s `onWidgetsReorder` hands back the slot's own array type, `DashboardComponentSchema['widgets']`, instead of `DashboardWidgetSchema[]`; the dashboard's `object… (objectui `9d7419b91`) + - **minor** — `DashboardWidgetSchema['type']` names the widget vocabulary only, `DashboardWidgetTypeName`: it drops `DashboardComponentWidgetType` (objectui#11514). This narrows the TypeScript… (objectui `9d7419b91`) + - **minor** — fix(plugin-charts): an `object-chart` whose `specType` is a single-value or tabular spec family draws its routed form, not a silent bar chart; the renderer has no default family (… (objectui `06634afe7`) + - **minor** — **BREAKING** — `DrillDownConfig.report`'s `{ name }` reference arm is retired on the TypeScript face, the tolerant zod face and the strict authoring face, and both zod faces refuse it by name (o… (objectui `9ed8d0f1c`) + - **minor** — **BREAKING** — The drill-down drawer scopes a dataset-bound drill report by `runtimeFilter`, and the pre-9.0 object-bound drill report is retired (objectui#11506). (objectui `8366accd1`) + - **minor** — `@object-ui/types` declares TypeScript types for three node types its zod face already validates: `DetailSectionNodeSchema` (`detail-section`), `AppSchemaRendererNodeSchema` (`app… (objectui `fc7db059f`) + - **minor** — feat(types): `ObjectChartSchema.chartType` declares the `@objectstack/spec` chart families plugin-charts draws (objectui#11513) (objectui `95e58a3b2`) + - **minor** — **BREAKING** — A `grid` node sets a column count per breakpoint in one way: the breakpoint object of `columns`. The flat `smColumns`, `mdColumns`, `lgColumns` and `xlColumns` keys are retired, w… (objectui `2d576e46e`) + - **minor** — **BREAKING (`@object-ui/cli`):** four commands are retired: `objectui create`, `objectui lint`, `objectui test` and `objectui studio`. There is no alias window and no placeholder:… (objectui `37268aae9`) + - **minor** — **BREAKING** — chore(console)!: drop the lazy `spec-report` stub (objectui#11440) (objectui `9d9ed5495`) + - **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops `spec-report`, which objectui#11440 retired (objectui `9d9ed5495`) + - **minor** — feat(types): the `report` node declares the `report` member that wraps a spec report (objectui#11440) (objectui `9d9ed5495`) + - **minor** — **BREAKING** — refactor(plugin-report)!: retire the `spec-report` node type key; a spec report is embedded as `{ "type": "report", "report": { … } }` (objectui#11440) (objectui `9d9ed5495`) + - **minor** — **BREAKING** — An `app-schema-renderer` node draws the app document it carries under `schema` (objectui#11494, triage ruling A). (objectui `fcdc8ec91`) + - **minor** — `AppSchemaRendererNodeSchema` declares the `app-schema-renderer` node's `schema` input: it is the app document, `AppComponentSchema` itself, by reference, and optional (objectui#1… (objectui `fcdc8ec91`) + - **minor** — **BREAKING** — The `columns` of a `grid` node is one of the counts its renderer maps, 1 to 12, as the bare number and at every breakpoint of the object form. Any other count is refused at valida… (objectui `aea682a31`) + - **minor** — **BREAKING (`@object-ui/cli`):** the `objectui add` command is retired, and so are `objectui analyze`'s two flags, `--render-performance` and `--bundle-size`. There is no alias wi… (objectui `ea3914139`) + - **minor** — A `metric-card` in a dashboard's widget slot parses only with its `value` (objectui#11483). This narrows a published accept set on both validator faces, and it narrows the widget… (objectui `f68e0a080`) + - **minor** — **BREAKING (`@object-ui/cli`):** `objectui generate` no longer accepts `--from`. The flag is retired, with no alias window and no placeholder, and the CLI now refuses it as an unk… (objectui `1fe05ff37`) + - **minor** — **BREAKING** — feat(types): `ObjectGridSchema`'s zod mirror declares ten members its TypeScript twin declares (objectui#6152, round 6) (objectui `0d723a33f`) + - **minor** — `safeValidateSchema` — and so `objectui validate` — accepts seven more registered node types: the spec page kinds `record`, `home` and `utility`, and `app-schema-renderer`, `objec… (objectui `00ccdf742`) + - **minor** — The `object-pivot` registration publishes `drillDown` as an input (objectui#11440). (objectui `00ccdf742`) + - **minor** — **BREAKING** — The `gap` of a `stack`, a `flex` and a `grid` node is one of the steps its renderer maps. Any other number is refused at validation, with the set named (objectui#11474). (objectui `4abc0aafa`) + - **minor** — Dashboard percent faces read the storage they are told, never a storage guessed from the value (objectui#11475). (objectui `f560ded15`) + - **minor** — **BREAKING: a percentage is scaled at the storage its field declares, never at a storage guessed from the value (objectui#11475)** (objectui `f560ded15`) + - **minor** — **BREAKING (`@object-ui/cli`):** `objectui generate` no longer accepts `--output`. The flag is retired, with no alias window, and the CLI now refuses it as an unknown option (obje… (objectui `9de0b3483`) + - **minor** — A `metric-card` in a dashboard's widget slot is checked against the props `MetricCard` renders, on the TypeScript face and in the validator (objectui#11467). This narrows a publis… (objectui `401611b21`) + - **minor** — `AnySchema` now includes the node types this package declares and exported outside it, so `SchemaByType` and narrowing on `type` reach them (objectui#11478). (objectui `2b188faa3`) + - **minor** — **BREAKING** — feat(types)!: `SidebarSchema` declares what the `sidebar` node draws — nine unread keys are retired on both faces, and `variant` takes the registration's enum (objectui#11465) (objectui `ca3de7272`) + - **minor** — The authored `properties`-bag carriers outside the spec's public blocks have a TypeScript authoring type, and `AuthoringNode` includes them, so `SchemaRenderer`'s `schema` prop ac… (objectui `2c0ddf226`) + - **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the ten `sidebar-*` node type keys objectui#10859 batch 8 phase 2d retired (objectui `1c8403692`) + - **minor** — **BREAKING** — refactor(components)!: retire the ten `sidebar-*` node type keys; the `sidebar` node supplies its own provider (objectui#10859, batch 8 phase 2d) (objectui `1c8403692`) + - **minor** — feat(components): the `sidebar` node mounts a `SidebarProvider` only when none is above it, and honours the boolean `collapsible` (objectui#10859, batch 8 phase 2d) (objectui `1c8403692`) + - **minor** — `input-otp` draws the separator its docs page teaches (objectui#11365). The "With Separator" example on the `input-otp` docs page and two catalog entries (`with-visual-separator`,… (objectui `e46ee770b`) + - **minor** — **BREAKING** — A `container` node's `padding` is one of the twelve steps its renderer maps: 0 to 8, 10, 12 and 16. Any other number is refused at validation, with the set named (objectui#11424). (objectui `3f6efd640`) + - **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the two node type keys objectui#11441 retired (objectui `9d1c0bff9`) + - **minor** — **BREAKING** — refactor(layout)!: retire the `navigation-renderer` and `responsive-grid` node type keys; navigation is application metadata, and the breakpoint grid is `grid` (objectui#11441) (objectui `9d1c0bff9`) + - **minor** — **BREAKING** — The page and report designers, and all three canvases, draw the members their node declarations always carried and they never read. Every designer registration's `inputs` now list… (objectui `5988b6b53`) + - **minor** — **BREAKING (authoring)** — a report element's `properties.field` is refused by name on the zod face; the element's binding is its declared `dataBinding` (objectui#11434, ADR-0049). (objectui `5988b6b53`) + - **minor** — **BREAKING** — chore(console)!: drop the lazy `pie-chart`, `donut-chart` and `radar-chart` stubs (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) + - **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the four node type keys objectui#10859 batch 8 phase 2c retired (objectui `ad1785c1d`) + - **minor** — **BREAKING** — refactor(components)!: drop `page-header` from the opt-in protocol placeholders (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) + - **minor** — **BREAKING** — refactor(plugin-charts)!: retire the `pie-chart`, `donut-chart` and `radar-chart` node type keys; the families are `chart` + `chartType` (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) + - **minor** — **BREAKING** — refactor(layout)!: retire the `page-header` node type key; the header node is `page:header` (objectui#10859, batch 8 phase 2c) (objectui `ad1785c1d`) + - **minor** — **BREAKING (authoring)** — `DataModelRelationship.onDelete` is respelled `deleteBehavior`, in `@objectstack/spec`'s vocabulary, on both faces; and the process designer's last two… (objectui `0e9058b95`) + - **minor** — The data-model and process designers draw the members their node declarations always carried and they never read (objectui#11434). (objectui `0e9058b95`) + - **minor** — **BREAKING** — chore(console)!: drop the lazy `scatter-chart` and `dashboard-grid` stubs, and stop the `metric` / `metric-card` stubs claiming the bare key (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-dashboard)!: retire the `dashboard-grid` node type key, and register the `metric` / `metric-card` node keys without a bare fallback (objectui#10859, batch 8 phase… (objectui `37140f4f5`) + - **minor** — **BREAKING** — chore(cli)!: the generated known-types list drops the twelve node type keys objectui#10859 batch 8 phase 2b retired (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-designer)!: retire four builder-chrome node type keys (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-form)!: retire the `form-analytics` node type key (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-grid)!: retire the `import-wizard` node type key; the `ImportWizard` export stays (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-detail)!: retire the `related-list` node type key; the related-list block is `record:related_list` (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-charts)!: retire the `scatter-chart` node type key; scatter is `chart` + `chartType: 'scatter'` (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING** — refactor(plugin-view)!: retire the `shared-view-link` node type key (objectui#10859, batch 8 phase 2b) (objectui `37140f4f5`) + - **minor** — **BREAKING (authoring)** — seven members of the `@object-ui/plugin-designer` node declarations and their record types are retired on both faces, and two element types leave the pa… (objectui `c4ab6d09a`) + - **minor** — **BREAKING (TypeScript props)** — `DataModelDesignerProps.autoLayout` and `ReportDesignerProps.previewMode` are removed, and the `data-model-designer` registration no longer publi… (objectui `c4ab6d09a`) + - **patch** — `record:details` edit mode edits a `textarea` field in a multi-line textarea, so saving it keeps its line breaks (objectui#11562). (objectui `ab1879721`) + - **patch** — The top-level `fields` input descriptions of `object-form`, `view:form`, `embeddable-form` and `object-master-detail-form`, and the `console.warn` a top-level `fields` member that… (objectui `dbd108166`) + - **patch** — A grouped `object-grid` labels its group headers from the object field's `options` only; a column's `options` no longer relabels them (objectui#11544). (objectui `b0bf413ca`) + - **patch** — `record:details` edit mode gives a `markdown` field an editor: a multi-line textarea (objectui#11541). (objectui `b34cc148e`) + - **patch** — The record dialog now draws a `form.sections[].group` section (objectui#11542). (objectui `584eecae8`) + - **patch** — fix(app-shell): create and edit no longer render a container's first named form when it declares no default form (objectui `9bfd0b36e`) + - **patch** — Raise `@object-ui/types`' declared `@objectstack/spec` floor from `^17.5.0` to `^17.6.0` (objectui#11227). The package's published types now read `EmptyState` from `@objectstack/s… (objectui `6158e4c93`) + - **patch** — objectui now resolves `@objectstack/*` 17.6.0 (objectui#11438). One declared range moves: `@object-ui/types` raises its `@objectstack/spec` floor to `^17.6.0` in objectui#11227's… (objectui `6158e4c93`) + - **patch** — fix(plugin-charts): an `object-chart` whose family is on `specType` gets `compareTo` exactly as the same family on `chartType` does, so a `specType: scatter` chart with `compareTo… (objectui `6837bfa85`) + - **patch** — fix(plugin-dashboard): an `object-metric` with a structured `groupBy` and a rule-list `filter` draws its number (objectui `8bfc0012e`) + - **patch** — The metadata-admin dashboard preview's reorder handler takes `DashboardComponentSchema['widgets']`, the array `DashboardRenderer`'s `onWidgetsReorder` now hands back (objectui#115… (objectui `9d7419b91`) + - **patch** — The dashboard editor reads a `widgets[]` entry by the slot's element type, `DashboardComponentSchema['widgets'][number]`, in its widget card, its property panel, its preview and i… (objectui `9d7419b91`) + - **patch** — The README's Activity tab authors `record:activity`'s declared inputs in `properties` (`{ limit: 20, showCompleted: false }`) instead of a host feed in `items` (objectui#11515). (objectui `fc7db059f`) + - **patch** — The VS Code extension's **Export to React** command types the `schema` constant it emits as `SchemaRendererProps['schema']`, imported type-only from `@object-ui/react` beside `Sch… (objectui `fc7db059f`) + - **patch** — The generated plugin's `jsdom` devDependency is now `^29.1.1` instead of `^30.0.1` (objectui#11366). (objectui `059bf1b59`) + - **patch** — The drill-down drawer renders a `drillDown.report` as a `report` node, `{ type: 'report', report }` (objectui#11440). (objectui `9d9ed5495`) + - **patch** — `toRenderableSchema` declares its parameter as `SchemaNode` (objectui#11479). (objectui `b654d4ed5`) + - **patch** — The record header's percent summary chip, text and bar, scales at the storage its field declares (`percentCellScale`, the spec's `percentScaleOf`: a fraction unless the field decl… (objectui `f560ded15`) + - **patch** — The gantt tooltip's `percent` row scales at the storage the field declares (`percentCellScale`, the spec's `percentScaleOf`), the answer the list cell reads. A fraction-stored `1`… (objectui `f560ded15`) + - **patch** — Grid percent faces read the field's declared storage (objectui#11475). (objectui `f560ded15`) + - **patch** — The gallery card's field bag now carries the field's `max`, the storage statement the percent cell reads through the spec's `percentScaleOf` (objectui#11475). Without it, a whole-… (objectui `f560ded15`) + - **patch** — fix(components): an expanded `offcanvas` sidebar is drawn inside the viewport (objectui#11464) (objectui `50c73fed0`) + - **patch** — `objectui generate page NAME` writes a page that draws its title (objectui#11450). (objectui `5429e6c74`) + - **patch** — The runner drops a fallback welcome page that could never render (objectui#11450). (objectui `5429e6c74`) + - **patch** — fix(fields): a read-only percent or currency field shows its value the way its table cell does, and a whole currency amount keeps its minor units (objectui#11444) (objectui `6007dd4e4`) + - **patch** — fix(approvals): the console names a position approver `position:manager`, the spelling the server stores — not `role:manager` (objectui#11455) (objectui `d93e53f5d`) + - **patch** — The search results line and the object view's record-count footer read the right noun form in every language at every count (objectui#11445). Each passes `count` to one i18next co… (objectui `4a1adb76e`) + - **patch** — The comment thread's header, its reaction tooltip and the presence stack's labels read the right noun form in every language at every count (objectui#11445). Each passes `count` t… (objectui `4a1adb76e`) + - **patch** — The data table's "N rows modified" line and the `page:tabs` count badge's accessible name read the right noun form in every language at every count (objectui#11445): both are i18n… (objectui `4a1adb76e`) + - …and 11 more releasing changesets in this range (list capped at 100; see the objectui range below). + + ⚠️ 43 of these carry a breaking change: 43 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 1 commit in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared it, so it appears in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ docs(plugin-dashboard): the charts example authors a line widget bound to a dataset; the card widget with nested children retires (objectui#11484) (#11523) (objectui `6903eafbc`) + + + + objectui range: `89cad75d5570...ab1879721595` + ## 17.6.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index 383160943ce..53220c69b79 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.6.0", + "version": "17.7.0", "description": "Prebuilt Console SPA pinned to this framework release, installed as a dependency of @objectstack/cli. Source of truth: @object-ui/console (https://github.com/objectstack-ai/objectui).", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/console", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 657b793fbc2..e828dab7928 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,198 @@ # @objectstack/core +## 17.7.0 + +### Minor Changes + +- eb9ef79: New export `objectNotFoundError(object)`: the one `OBJECT_NOT_FOUND` envelope the data door and the engine's in-process verbs refuse an unresolved object name with + + Clause-②: yes + + `@objectstack/core` exports `objectNotFoundError(object: string): Error`. The error it returns carries `code: 'OBJECT_NOT_FOUND'`, `status: 404`, the requested name on `object`, and the message `Object '' not found`. It lives here beside `recordNotFoundError`, and for the same reason: the engine cannot import `@objectstack/metadata-protocol`, where the data door first wrote this envelope (ADR-0076 D2). The data door's object-existence gate and `@objectstack/objectql`'s resolver both build their refusal from it, so the two answer one name space with one envelope. Additive: nothing that existed before changes. +- 1ac7308: fix(core)!: the plugin artifact signature contract refuses any key that is not Ed25519, so its `ed25519` label now holds (#21524) + + **BREAKING**: `signPayload` and `verifyPayload` (the plugin artifact signature contract in `@objectstack/core`) now refuse a key whose type is not Ed25519. Until now they accepted any asymmetric key. node's `sign(null, …)` and `verify(null, …)` follow the key they are handed, so an RSA, EC or Ed448 key signed under the `ed25519:KEYID:SIG` label and verified against its own public half. `os plugin sign --key` with an RSA private key exited 0, printed `Plugin signed`, and wrote an `ed25519:`-labelled sidecar over an RSA signature. + + What is refused now: + + - **`signPayload`** throws when the private key is not Ed25519. The error names the key type found (`rsa`, `ec`, `ed448`, and `secret` for a symmetric key). + - **`verifyPayload`** throws when the verifying key's type is not the algorithm the signature's label names. The label is checked against the key, not trusted, and the only label the contract parses is `ed25519`. The error names the key type found. + - **`verifyPublisherSignature`, `verifyPlatformSignature` and `verifyPluginArtifact`** verify through `verifyPayload`. So a publisher key registry entry or a platform key that is not Ed25519 makes them throw, or reject, with that same error. It is not folded into a `false` or an `ok: false` result, because a wrong key is the verifier's own configuration, not a verdict on the artifact. + - **`os plugin sign`** prints one `✗ Signing failed: signPayload: …` line naming the key type, exits 1, and writes no sidecar. + + Each refusal is a plain `Error`, the error style the module already used. + + **The fix:** sign with an Ed25519 key, generated with `openssl genpkey -algorithm ed25519` or `generateEd25519KeyPair()`. Configure Ed25519 public keys for the publisher key registry and the platform key. A signature made earlier with a non-Ed25519 key cannot be verified any more. Sign the artifact again with an Ed25519 key. + + **Unchanged:** an Ed25519 key signs and verifies exactly as before, with the same deterministic signature bytes. That holds for a PEM string, a `KeyObject`, and the PEM buffer, DER and JWK inputs node also accepts. A malformed signature string, a signature that does not verify, and a key that cannot be read still answer `false`. The signature string format and every export are unchanged. + + Clause-②: no (narrowing) + + + +### Patch Changes + +- c205b6c: Provenance comments in `@objectstack/core` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each now cites the commit in this repository's history that made the decision it describes, except + three source comments: one in `resolve-authz-context.ts` that quotes a maintainer ruling now cites + ADR-0131's 2026-09-17 amendment, which records that ruling verbatim, and two on the unpack-time + integrity re-verification leg, which pointed at a tracker for work that was never built, now say in + words that the leg is unbuilt. One test comment named a maintainer-ruling comment that also answers + 404; it now cites ADR-0025 §3.7, which records that ruling's effect. Some of these docblocks sit on + exported members, so the reworded text appears in the published declaration files (`index.d.ts` / + `index.d.cts`), and the comments esbuild keeps appear in the JavaScript output (`index.js` / + `index.cjs`). + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 30af17e: `jsonColumnOperatorRefusalText` takes an optional fourth argument: the class of JSON column the refused operator met, `JsonColumnFieldClass` (now exported). `'multi-value-or-json'` is the default, and its words are unchanged. `'single-value-media'` words the refusal for a single-value file-class field that a SQL deployment still stores as a JSON column. + + Clause-②: no + + A single-value file-class field (`file`, `image`, `avatar`, `video`, `audio`) is stored as a JSON column only on a deployment inside the ADR-0104 dual-encoding window, whose media columns have not moved. There it holds one JSON string, so `$contains` with the field's exact id answers no rows. That class's refusal no longer prescribes `$contains`. It says that the field answers these operators again once the deployment finishes the media-column move (the column step of `objectstack migrate files-to-references --apply`), and it still names `$null` / `$empty` for "no value". The message stays under the REST envelope's 500-character bound. Which operators are refused, and on which fields, does not change. +- 10454b3: fix(core): a resumed migration run is compared against the chunk plan it started over, so `os migrate resume` completes an interrupted `recorded-by` run that had committed a chunk or was started with a non-default `--chunk-size` (#21528) + + Clause-②: no + + `runMigrationJournal` recomputed a resumed run's chunk plan from the rows `load()` returned at resume time, at the plan's current chunk size, and refused `PLAN_CHANGED` when that plan's hash differed from the one `run_started` recorded. Two kinds of interrupted run could differ. A plan whose `load()` selects only the work still to do, which `recorded-by`'s plan does, returns fewer rows once a chunk has committed. And the plan handed back for a resume carries its own chunk size, not the one the run was started with. So `os migrate resume` listed such a run as `resumable: true`, and `os migrate resume --run --yes` then refused it. + + A resume now reads the chunk plan back from the journal's `run_started` record: + + - **Identity.** The plan's id and step names are hashed with the recorded chunk boundaries and compared with the recorded hash. A plan whose id or steps changed is still refused `PLAN_CHANGED`. The run resumes at the chunk size it started with. + - **Rows.** Each step's rows are bound to that chunk plan. If `load()` returns every row the run started over, each chunk's rows are where the journal put them, as before. If it returns exactly the rows of the chunks not yet committed, those rows go, in order, to those chunks. Any other row count is refused `PLAN_CHANGED`, and the message names the step. + - **Unwind.** If a chunk fails after a resume that bound its rows the second way, the runner compensates the chunks this process committed, newest first. It then stops at the newest chunk an earlier process committed and journals `run_failed`, because `load()` no longer returns that chunk's rows. It does not compensate other rows in their place, and the run ends `failed`. +- a0176ef: Credential-class field values are now masked on every write response, as on reads. + + Clause-②: no + + - A `secret` field, and a `password` field on an object that is not `managedBy: 'better-auth'`, already read back as `SECRET_MASK` (`null` when unset) on the generic read path (ADR-0100). Every write response that returns a record (REST, batch and MCP) now answers the same way. + - The shared write-response helper every write door already calls (`omitInternalFieldsFromWriteResponse`, `@objectstack/core`) now applies the credential mask before it omits `internal: true` fields. New exports beside it: `maskCredentialFieldsInWriteResponse` and `collectCredentialWriteResponseFields`, which read the same `isMaskedOnReadFieldType` declaration as the engine's read mask. + - `callData`'s fallback create and update arms (`@objectstack/runtime`, used when no protocol service is registered) now pass their response record through the same helper. + - Unchanged: the engine's own write results still return the stored row whole to privileged server-side callers, and the echoed-mask write guard still treats a `SECRET_MASK` value as "leave unchanged", so a client that saves back a write response does not overwrite the stored credential. +- d16b9fb: The platform's own `sys_metadata` reads and writes now carry the explicit system opt-in (`isSystem: true`) instead of reaching the data engine with no principal at all + + Clause-②: no + + - **What moved.** Each engine call in these functions now passes `context: { isSystem: true }`. Inside a repository transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along. + - `@objectstack/metadata-protocol`: the overlay reads (`findServedOverlayRow`, `overlayLockLayerAt`), the list read (`readActiveOverlayRows`, and `readFlattenedMetaItems`' draft preview), the authoring gate's stored-collection fold (`foldStoredCollection`), the audit and commit trail writes (`recordMetadataAudit`, `persistPackageCommitRow`), and the package verbs' store calls (`publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage`, `reassignOrphanedMetadata`). + - `SysMetadataRepository`: `get`, `put`, `delete`, `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage counters. + - `@objectstack/objectql`: `ObjectQLPlugin`'s authored action and hook reads, at boot and on resync. + - `@objectstack/core`: the authored-translation read (`readAuthoredTranslationLayer`). + - **Why.** plugin-security passes an engine operation whose context has no user, no position, no permission set and no `isSystem` straight on to the next handler (ADR-0096's principal-less hand-off). These calls worked only because of that pass-through. They are platform plumbing: any door in front of them has already authorized the caller, and the protocol scopes its own rows by organization. So they now say so with the opt-in that already exists. + - **No gate verdict moves.** A system context skips the six gates the middleware still runs before that pass-through: package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. Four of them only act on other objects. The engine-owned gate never fires on a context with no user. The delegated-administration gate only acts on the RBAC link tables. So none of the six applies to the `sys_metadata` family. An instrumented run of the dogfood suite and a booted dev composition recorded no gate firing on any of these calls before the change. After the change it recorded no principal-less call from these functions. + - **One engine check also stands down under `isSystem`.** That is the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The instrumented runs recorded no refusal from it on any of these calls. + - ⛔ No new API, no export change, and no change to what any door authorizes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 21f3587c79a..9ed424c7930 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.6.0", + "version": "17.7.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 92afe4de0e0..a28c884a093 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,27 @@ # create-objectstack +## 17.7.0 + +### Minor Changes + +- bcd68a2: feat(create-objectstack): the blank starter wires a `src/picklists` barrel for `objectstack generate picklist` + + Clause-②: yes (widening) + + A new blank project ships an empty `src/picklists/index.ts`, and its `objectstack.config.ts` imports it and hands its exports to `defineStack` under `picklists`, as it already does for every other directory `objectstack generate` writes into. `objectstack generate picklist NAME` then writes `src/picklists/NAME.picklist.ts` and its export line, and the list is part of the stack with no edit to the config. A select field takes its options from the list with `Field.select({ picklist: 'NAME' })`, and the server serves that field with the list's options. + + A project scaffolded by an earlier release keeps its config. There, `objectstack generate picklist NAME` writes the list, reports that it does not reach the stack, and prints the import line and the `defineStack` key that wire `src/picklists`. + +### Patch Changes + +- fa7b565: fix: a fresh project no longer warns about its own starter fields after the first `objectstack generate` + + Clause-②: no + + The blank starter's `note` object (`npm create objectstack`) and the item object of the `app` template (`objectstack init -t app`) now declare one field group, `fieldGroups: [{ key: 'details', label: 'Details' }]`, and place every field in it with `group: 'details'`. Before this, the first view, flow, dashboard or other metadata that can read a field made `objectstack validate` and `objectstack lint` report `field-no-consumers` on a field the author never wrote: the note's `body`, or the item's `description` and `status`. That held whether the author generated it or wrote it by hand. Both commands still exited 0. A field placed in a declared group is drawn by the object's form and detail page, and the rule counts that as displayed, so a fresh project now reports nothing. The `plugin` and `empty` templates are unchanged: the plugin's one field is the record's title, which the rule never reports, and the empty template declares no object. + + **What changes for an author.** In a new project, the object's form and detail page show the starter fields in one section labelled Details instead of a flat list. A field you add joins a section the same way, by naming its `key` in `group`. A project scaffolded by an earlier release keeps its files. To clear the warning there, add the same `fieldGroups` entry to the object and `group: 'details'` to each field the warning names, or give each field another consumer, such as a view column. + ## 17.6.0 ### Patch Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index a1075986947..30284d7034f 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "17.6.0", + "version": "17.7.0", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index de89a962eea..426064182fd 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,135 @@ # @objectstack/driver-memory +## 17.7.0 + +### Patch Changes + +- db0cf22: Provenance comments in `@objectstack/driver-memory` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes. Some of + these docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` / + `index.d.mts`, and comments that esbuild keeps appear in the JavaScript output. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index 615bd2ce7f4..f12fb1f6c9f 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index 6f9e6cc0628..c32cedc3d25 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,136 @@ # @objectstack/driver-mongodb +## 17.7.0 + +### Patch Changes + +- f97660c: Provenance comments in `@objectstack/driver-mongodb` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes. One of + these docblocks sits on an exported member (`MongoDBDriver.update()`), so the reworded text appears in + the published `index.d.ts` / `index.d.mts`; that docblock and one more comment esbuild keeps appear in + the JavaScript output (`index.js` / `index.mjs`); the sourcemaps do not change. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index 4fa7753a75a..6ab6b4b9ae9 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "main": "dist/index.js", diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index cc3472cd8d6..e97bf8b5b47 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,193 @@ # @objectstack/driver-sql +## 17.7.0 + +### Minor Changes + +- 35dfb81: fix(service-analytics): the ObjectQL face echoes a date-bucketed dimension in the bucket expression the driver itself groups by, so SQLite runs the statement it prints + + Clause-②: yes (widening) + + **Before**, the ObjectQL strategy printed every date-bucketed dimension as `date_trunc('', col)` in the `sql` it echoes and in the `POST /analytics/sql` body, on every dialect. The native strategy declines a granularity, so every bucketed query lands on this face. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` in the default composition: the rows were right. On SQLite the echo failed with `no such function: date_trunc` (month, quarter and week). On PostgreSQL 16.14 it ran but answered `2026-01-01T00:00:00.000Z` where the face answers `2026-01`. The driver groups by `strftime('%Y-%m', …)` on SQLite and `to_char((…)::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM')` on PostgreSQL. + + **Now** the echo prints the driver's own expression, so it runs on that dialect and answers the face's bucket keys. + + - **`@objectstack/driver-sql`**: `SqlDriver.dateBucketSql(objectName, field, granularity)` returns the expression `aggregate` groups by, rendered as SQL text: the existing `buildDateBucketExpr`, unchanged, with each identifier quoted by the dialect. It returns `null` for a granularity the dialect buckets in memory (`week` on SQLite). The MySQL arm (`date_format(convert_tz(…))`) is checked by code read only, because no MySQL server was available. + - **`@objectstack/service-analytics`**: the new optional `AnalyticsServiceConfig.dateBucketSql` hook carries the expression to the ObjectQL strategy. `AnalyticsServicePlugin` wires it from the driver that serves the object, as it wires `sqlDialect`. + - **`@objectstack/driver-turso`**: a comment that said `SqlDriver` buckets with `date_trunc` now names the SQLite `strftime` expression it emits. The inherited `dateBucketSql` answers on the remote face too: it renders the same SQLite expression with no connection, and libSQL runs it. + + **Unchanged.** The rows every face answers. The echo keeps `date_trunc(…)` where nothing answers: a host that wires no hook, a driver with no bucket expression (memory, MongoDB), a granularity the driver buckets in memory, and a query with a non-UTC `timezone`, which the engine buckets in memory on that zone's calendar. + +### Patch Changes + +- 13a24ec: Provenance comments in `@objectstack/driver-sql` cite the commits and ADR that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes, or the + ADR that records it (ADR-0104's 2026-09-05 addendum). Some of these docblocks sit on exported members, + so the reworded text appears in the published `index.d.ts` / `index.d.mts`, and comments that esbuild + keeps appear in the JavaScript output. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 30af17e: On a deployment whose media columns have not moved, the JSON-column filter refusal on a single-value file-class field (`file`, `image`, `avatar`, `video`, `audio`) now names the repair that works there: the media-column move, not `$contains`. + + Clause-②: no + + The filter is still refused with `INVALID_FILTER` / 400, for the same operators as before (`$eq`, `$in`, `$startsWith`, `$icontains`, the orderings and the rest of that set, and the bare `{ field: value }` spelling). Before, the refusal told the caller to use `$contains`, the membership repair for a multi-valued field. On a single-value file-class field `$contains` with the field's exact id answers no rows. The refusal now says that the field answers these operators again once the deployment finishes the media-column move (the column step of `objectstack migrate files-to-references --apply`), and it still names `$null` / `$empty`, which answer there. A multi-valued field keeps the `$contains` words, byte for byte. Once the media columns have moved, these filters are not refused, as before. +- 6d728b8: fix(driver-sql): the driver's own refusal log lines no longer write the statement or the values bound into it + + Clause-②: no + + Five warning lines wrote the dialect's message to the server log as it came back. That message opens with the statement, with its bound values inlined on SQLite and MySQL, and on PostgreSQL a value-bearing diagnostic carries the value itself. The lines are the read terminal, the raw-statement terminal, and the refusals for a WHERE, a groupBy or aggregation, and a listed-distinct column the backend could not resolve. Each line now writes the dialect's text through the driver-fault redaction in `@objectstack/types`, the cut the engine applies at its boundary. + + - **What stays on each line.** Its code, the class of fault it reports, the object and column it names, the dialect's error code where the line printed one, and the dialect's own diagnostic. + - **What goes.** The statement and the values bound or inlined into it, replaced by `[statement and bound values redacted]`, and the value slot of each diagnostic the redaction's templates own, replaced by `[value redacted]`. The raw-statement line no longer writes the statement it was sent, which also holds for `@objectstack/driver-turso`'s remote transport, whose refusals reach the same line. The two debug lines the read terminal writes inside a pre-DDL question, or for a table whose DDL the driver deferred, take the same cut. + - **The envelopes.** The code, status, `cause` and withheld text of every refusal are unchanged. Two composed messages, the read terminal's `DATABASE_ERROR` and the raw-statement terminal's, said the statement was written to the server log; they now say the diagnostic was written with the statement and its bound values cut. + - **What changes for an operator.** A log reader that took the statement or a bound value from these lines now finds the marker where the dialect's text carried them, and nothing where the raw-statement line wrote the sent statement on its own. The diagnostic, the codes and the named object and column are where they were. +- 440cd32: A `Field.date` grouped by `day`, `week`, `month`, `quarter` or `year` buckets as its own calendar day on PostgreSQL and MySQL, whatever zone the server or the session is in (#21485). + + Clause-②: no + + - **What was wrong.** The PostgreSQL bucket cast every column to `timestamptz` and the MySQL bucket passed every column through `convert_tz`. A `date` has no instant, so both invented midnight in the session's zone, and on a session east of UTC the conversion to UTC read the previous day. On PostgreSQL with the server at `Asia/Shanghai`, `2026-06-01` grouped into month `2026-05`, and `2026-01-01` into year `2025`. MySQL did the same once the session zone was `+08:00`; the driver pins its own sessions to UTC, so there it took a host `pool.afterCreate` that sets the session zone. + - **What it does now.** A declared `Field.date` buckets its calendar day with no zone conversion. A `Field.datetime`, and a column with no declaration, keep the UTC-instant expression, byte for byte. SQLite already bucketed a `date` as its calendar day and is unchanged. + - **Where it shows.** `aggregate()` with a `dateGranularity` group, and the expression `SqlDriver.dateBucketSql()` renders for the analytics SQL echo, which reads the same expression. +- 5d095a0: On SQLite, a `week` date bucket is grouped in SQL, and the analytics SQL echo never prints a bucket statement that SQLite refuses (#21595). + + Clause-②: no + + - **What was wrong.** `driver-sql` grouped `day`, `month`, `quarter` and `year` in SQL on SQLite, but not `week`. Its `supports.queryDateGranularity` said `week: false`, so the engine bucketed weeks in memory, and the ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` echoed the bucket as `date_trunc('week', col)`. SQLite has no `date_trunc`, so that echo could not run. A non-UTC `timezone` on SQLite gave the same echo for every granularity. + - **What it does now.** + - SQLite advertises all five granularities. `week` buckets as `YYYY-Www`, the ISO 8601 week that the PostgreSQL and MySQL arms answer. The expression does not use `strftime('%V')`, which needs SQLite 3.46: `@libsql/client` 0.18.0 bundles SQLite 3.45.1, where `%V` answers NULL. It runs on better-sqlite3, on libSQL (`driver-turso`) and on sql.js (`driver-sqlite-wasm`). A `Field.date` still buckets as its own calendar day. + - The echo prints that expression for a `week` bucket on SQLite, and the statement runs. + - With a non-UTC `timezone` on SQLite, `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. `POST /api/v1/analytics/query` still answers the rows, and its answer carries no `sql`. The engine buckets on that zone's calendar in memory, and SQLite has no time-zone database, so no SQLite statement produces those keys. + - **Where it shows.** `aggregate()` with a `week` group on SQLite, `SqlDriver.dateBucketSql()`, and the analytics SQL echo. A query sent with `timezone: 'UTC'`, or with no `timezone`, still echoes the driver's own expression. +- da40a5f: A connect to a SQLite file that is already `auto_vacuum=INCREMENTAL` no longer writes to it. `SqlDriver.connect()` now reads `PRAGMA auto_vacuum` first and runs `PRAGMA auto_vacuum = INCREMENTAL` only when the file answers something else. + + Clause-②: no + + - **What changed on disk.** On a file that already answered INCREMENTAL, the setter changed no mode, but it still stamped two header counters: the file change counter (bytes 24–27) and the version-valid-for number (bytes 92–95). So a read-only command such as `os migrate duplicates`, which the CLI docs say writes nothing at all, changed the file's md5 on every run. Reading the pragma changes no bytes, so such a file now comes through a connect, and a whole `os migrate duplicates` run, byte-identical. + - **Unchanged.** A fresh file and `:memory:` still come out INCREMENTAL. A legacy NONE file that already holds tables still gets the setter, which leaves it NONE until a `VACUUM` (`os db clean`), exactly as before. The journal-mode step already read first and set only on a difference, and it is untouched. Postgres and MySQL issue no PRAGMA. + - **The WASM SQLite driver** inherits the rule. A connect and disconnect no longer rewrites an already-INCREMENTAL image file, because the setter was what marked the image dirty. + - ⛔ No config key, export, error code or accepted input changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/observability@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index 7e5f409bc33..dfb89f0b533 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index f894b14953e..b84df8f6e3d 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,128 @@ # @objectstack/driver-sqlite-wasm +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [13a24ec] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [440cd32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [5d095a0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [da40a5f] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/driver-sql@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index 55613dd633a..bdcf524b2cb 100644 --- a/packages/drivers/driver-sqlite-wasm/package.json +++ b/packages/drivers/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "17.6.0", + "version": "17.7.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 3671850471b..842e60a9515 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,162 @@ # @objectstack/driver-turso +## 17.7.0 + +### Minor Changes + +- 30af17e: Both transports now word the JSON-column filter refusal on a single-value file-class field (`file`, `image`, `avatar`, `video`, `audio`) the way `@objectstack/driver-sql` does: the media-column move, not `$contains`, which answers no rows on that field. + + Clause-②: no + + The local transport inherits the new words from `SqlDriver`. The remote transport refuses in its own filter compiler, and now reads the same class from the driver, so one filter gets one message on both transports. The refused operators and fields do not change. Remote mode never moves its media columns, so a single-value file-class field is a JSON column there on every deployment; the remote transport refuses to plan the column step of `objectstack migrate files-to-references` (`NOT_IMPLEMENTED` / 501), as before, so on that transport the prescribed move is not yet available. + + `RemoteTransport.setJsonColumnResolver` now takes a resolver that answers the column's class (`JsonColumnFieldClass`, from `@objectstack/core`), or `undefined` for a column that is not JSON, in place of `true` / `false`. `TursoDriver` supplies it. A host that calls the method itself returns `'multi-value-or-json'` where it returned `true`, and `undefined` where it returned `false`. That replaces the setter's published parameter type, so a host resolver that returns a boolean no longer compiles: a host that injects its own resolver updates its signature, which is why this release is `minor`. + +### Patch Changes + +- fd5a1cd: Provenance comments in `@objectstack/driver-turso` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes. Some + of these docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` + / `index.d.mts`, and the comments esbuild keeps appear in the JavaScript output (`index.js` / + `index.mjs`); the sourcemaps do not change. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 35dfb81: fix(service-analytics): the ObjectQL face echoes a date-bucketed dimension in the bucket expression the driver itself groups by, so SQLite runs the statement it prints + + Clause-②: yes (widening) + + **Before**, the ObjectQL strategy printed every date-bucketed dimension as `date_trunc('', col)` in the `sql` it echoes and in the `POST /analytics/sql` body, on every dialect. The native strategy declines a granularity, so every bucketed query lands on this face. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` in the default composition: the rows were right. On SQLite the echo failed with `no such function: date_trunc` (month, quarter and week). On PostgreSQL 16.14 it ran but answered `2026-01-01T00:00:00.000Z` where the face answers `2026-01`. The driver groups by `strftime('%Y-%m', …)` on SQLite and `to_char((…)::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM')` on PostgreSQL. + + **Now** the echo prints the driver's own expression, so it runs on that dialect and answers the face's bucket keys. + + - **`@objectstack/driver-sql`**: `SqlDriver.dateBucketSql(objectName, field, granularity)` returns the expression `aggregate` groups by, rendered as SQL text: the existing `buildDateBucketExpr`, unchanged, with each identifier quoted by the dialect. It returns `null` for a granularity the dialect buckets in memory (`week` on SQLite). The MySQL arm (`date_format(convert_tz(…))`) is checked by code read only, because no MySQL server was available. + - **`@objectstack/service-analytics`**: the new optional `AnalyticsServiceConfig.dateBucketSql` hook carries the expression to the ObjectQL strategy. `AnalyticsServicePlugin` wires it from the driver that serves the object, as it wires `sqlDialect`. + - **`@objectstack/driver-turso`**: a comment that said `SqlDriver` buckets with `date_trunc` now names the SQLite `strftime` expression it emits. The inherited `dateBucketSql` answers on the remote face too: it renders the same SQLite expression with no connection, and libSQL runs it. + + **Unchanged.** The rows every face answers. The echo keeps `date_trunc(…)` where nothing answers: a host that wires no hook, a driver with no bucket expression (memory, MongoDB), a granularity the driver buckets in memory, and a query with a non-UTC `timezone`, which the engine buckets in memory on that zone's calendar. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [13a24ec] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [440cd32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [5d095a0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [da40a5f] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/driver-sql@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index d9b04a2876f..e042378aefd 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "17.6.0", + "version": "17.7.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 ed21def0a17..a228a7206d5 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,169 @@ # @objectstack/formula +## 17.7.0 + +### Minor Changes + +- 7aab759: fix(formula)!: `matchesFilterCondition` compares a bare-day upper bound as written; its own whole-day copy is deleted (ADR-0053 D-D1 items 5 and 9) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the RLS write check admits on columns that are not `datetime`, and moves a few `engine.aggregate` answers that no seam lowers. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What is deleted.** `matchesFilterCondition` no longer reads a bare `YYYY-MM-DD` `$lte`, or a `$between` maximum, as "through that whole day", and no longer drops the bound on `9999-12-31`. It compares the value as written, as every other ordering operator here does, and as `driver-sql` compares it on the read. The whole day is applied once, at the seams that feed this evaluator, by the shared `lowerFilterCondition` (`@objectstack/spec/data`): the RLS compile seam lowers every policy filter on the object's declared `datetime` columns, the engine lowers `having` and `aggregations[i].filter` the same way, and the RLS write check judges a declared `date`, `datetime` or `time` column in its stored form. So a `check` on a `date` or `datetime` column answers exactly as before. + + **The RLS write check now agrees with the read on other columns.** Measured through `ObjectQL.insert` and `SecurityPlugin` on `SqlDriver` (better-sqlite3), as a member whose policy has the same `using` and `check`: + + - a `text` column under `record.title <= '2026-01-05'`, written as `'2026-01-05T15:00:00Z'` or `'2026-01-05 noon'`: the write was admitted while the read hid the stored row. It is now refused `PERMISSION_DENIED` / 403, and the read still hides it; + - two `text` columns, `record.title <= record.code`, with `code` holding `'2026-01-05'`: the same, admitted before and 403 now, with the read hiding the row; + - a `number` column under `record.amount <= '9999-12-31'`: the write was admitted because an epoch number read as an instant on the last supported day. A number is not less than a day string, so it is now 403. The engine refuses the same comparison in a `where` (`INVALID_FILTER` / 400: a day string is not a number). + + The access explanation (`explain`) judges a stored row with this evaluator, so its row verdict moves the same way: for the two `text` cells it now says hidden, as the read does. + + **`engine.aggregate` answers that no seam lowers.** A `{ $field }` referent is per row, so no seam can lower it. These positions are now compared as written: + + - two declared `text` columns of one class, at a per-aggregation `filter` or between two `having` group columns: `'2026-01-05 noon'` against `'2026-01-05'` is no longer counted or kept, which is what the same comparison answers in a `where`; + - the pairs the class rule cannot judge because a side has no declaration: an object the registry does not declare, and an audit-opt-out object's row-carried `created_at` / `updated_at` against a `date`. An instant on the due day is no longer counted against that bare day; + - a direct `applyInMemoryAggregation` call, which applies no class rule. + + **The remedy.** Compare a `datetime` with a `datetime` and a `date` with a `date`. A `datetime` against a calendar day has no single answer across SQL and memory, and a declared pair of the two is already refused. A number compared with a day string has no answer at all: compare a number with a number. A caller that evaluates a filter on a `datetime` column without passing a seam lowers it first with `lowerFilterCondition(filter, { isDatetimeColumn })` to get the whole-day reading. + + **Unchanged.** A `check` on a declared `date`, `datetime` or `time` column, a `{ $field }` pair of two `date` or two `datetime` columns (with or without `addDays`), a full-ISO bound, `$gte` / `$gt` / `$lt` and `$eq`. + +### Patch Changes + +- 0a0debb: Provenance comments in `@objectstack/formula` cite the commit that decided them, not a tracker number that no longer resolves + + Clause-②: no + + Comments and docblocks in the package cited an issue-tracker number that now answers 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes. One of + these docblocks sits on an exported member (`SCOPE_ROOTS`), so the reworded text appears in the + published `index.d.ts` / `index.d.mts`; one comment esbuild keeps inside that list appears in the + JavaScript output (`index.js` / `index.mjs`); the sourcemaps do not change. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 41a3c8d: Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it + + Clause-②: no + + `driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships: + + - `@objectstack/spec`: + - The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it. + - `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`). + - `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did. + - A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it. + - `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too. + - `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`. + - `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index d897568b05a..3902ee4c076 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "main": "dist/index.js", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index dc1bb1f23a8..382ada55624 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,281 @@ # @objectstack/lint +## 17.7.0 + +### Minor Changes + +- 0fc8087: fix(lint)!: `action-name-undefined` resolves `record:related_list` action ids against the related object, and refuses an id the list cannot draw (#20936) + + Clause-②: no (narrowing) + + `action-name-undefined` is the authoring gate for "a surface names an action that renders nothing". It already walked list-view row and bulk menus, the `record:quick_actions` bar, the `record:alert` call-to-action, the `page:header` action ids and app navigation. One page surface that binds actions by id was never read: `record:related_list` → `properties.actions`. + + The console now reads that key. It resolves each id against the RELATED (child) object's own actions, never the page's object, and places it by that action's own `locations`: `list_toolbar` draws a header button, `list_item` and `record_related` draw a row-menu item. An id that names no action of the child object, or an action placed at none of those three, draws no button; the list shows a refusal notice naming it instead. The spec types the key as plain strings, so a misspelled id passed spec validation and lint and surfaced only at runtime. + + The rule now walks the key, scoped to `record:related_list`, and answers the same two questions the renderer asks: + + - each string id must name an action of the related object: one written on that object, or a `stack.actions` entry bound to it by `objectName`. An id defined only on the page's object, or only as a global action, is refused like a typo, and the message names where it is defined. The did-you-mean and the hint's action list are the related object's own; + - the action it names must declare at least one location a related list draws. The location set is read from the spec's `ACTION_LOCATIONS` vocabulary, classified per member, so a location added to the vocabulary has to be classified before this package compiles. + + The related object is the component's bound `dataSource.object` when one is set, otherwise `properties.objectName`. A related object this stack does not define is skipped: its actions belong to another package, and the rule does not guess. Inline-object elements are skipped, as on `page:header`, and every id is reported at its authored index. Every other walk of the rule is unchanged: it still asks only whether a name is defined anywhere in the stack. + + **What moves for consumers.** A stack whose related list names an id the list cannot draw built clean before and now fails `os validate` / `os lint` / `os build` with `action-name-undefined` (severity `error`). That id never rendered a button, so nothing that worked stops working. The rule still does not run at the runtime publish door for `page` writes. No related list in the platform's own pages or in the example apps authors `actions`, so none of them changes. + + +- 39a912e: fix(lint)!: `os validate`, `os build` and `os lint` refuse an `analyticsCubes` dimension over a JSON-stored column, and a cube `count_distinct` measure over one, which the analytics door already refuses at query time + + Clause-②: no (narrowing) + + + + **BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail. An authored analytics cube (`defineStack({ analyticsCubes })`) is queried through the same analytics door as a compiled dataset, and that door refuses a query that groups by a JSON-stored column, or counts its distinct values, with `400 INVALID_FIELD` before any SQL is built. So such a member could be declared but never served, and until now no authoring rule read `analyticsCubes` at all. The dataset rule's two ids now judge cube members as well: `dimension-json-stored-field-refused` and `measure-aggregate-field-type-refused` (gating, `error`). It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. + + **What is refused.** On a cube whose `sql` names an object the stack defines: a `dimensions` entry whose `sql` column is declared with a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) or a multi-value declaration (`multiselect`, `checkboxes`, `tags`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`); and a `measures` entry of `type: 'count_distinct'` whose `sql` column is either. The column is the member's `sql`: a column of the cube's object, or a relationship path read on the object its last hop reaches (the join the cube declares for that hop, else the lookup field's `reference`). The classes are `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, `isMultiValueField` and the `count_distinct` row of `AGGREGATE_FIELD_TYPE_COMPATIBILITY`, the predicates the door reads. + + **What an author sees now.** The finding names the cube, the member, the column, the object that declares it and its declaration, and says the analytics door refuses it with `400 INVALID_FIELD`. It names the route: group by, or count the distinct values of, a field that stores one scalar value; for a multi-value field, filter by one member with `$contains` in a record query. It is located at `analyticsCubes[N].dimensions.KEY.sql` or `analyticsCubes[N].measures.KEY.type`, where `KEY` is the member's key. + + **Unchanged.** Every dataset finding, word for word. A cube member over any other column, a single-value `select` or `lookup` included; a `count` measure, and a `sum`, `avg`, `min` or `max` measure, which this check does not judge; the row wildcard `'*'`; a member whose column does not resolve or declares no type; a cube whose `sql` names no object this stack defines. The runtime metadata write door: no authoring rule is dispatched for an `analytics_cube` save, and a `dataset` save's snapshot carries no cubes. +- 99e1912: The metric sub-caption is retired at both ends. A dashboard widget keeps one authored description, `widget.description`, which renders as the card-header subtitle and is translated by the widget's `description` translation key. The widget translation key `subCaption` is refused, and the server no longer writes a widget's `options.description`. + + Clause-②: no (narrowing) + + + + **What is retired.** `dashboards.DASHBOARD.widgets.WIDGET.subCaption` in a translation bundle (`defineTranslationBundle`, `stack.translations`, the platform bundle) and in a registered `translation` item. It overlaid a caption under a metric's value onto the widget's `options.description`. The dashboard schema never declared `options.description`, and no authored widget wrote it, so `translateDashboard`'s overlay was the key's only writer. That overlay is removed: `translateDashboard` now translates a widget's `title` and `description` and carries `options` through untouched. + + **BREAKING** — an accept-set narrowing, shipped as `minor` under the launch-window convention. + + ### FROM → TO + + | wrote | write instead | + | --- | --- | + | `dashboards.DASHBOARD.widgets.WIDGET.subCaption: 'TEXT'` | delete the entry. If the copy belongs on the card, put it in the widget's `description` and translate it under `dashboards.DASHBOARD.widgets.WIDGET.description`. | + | `dashboards.DASHBOARD.widgets.WIDGET.subtitle: 'TEXT'` | `subtitle` was only ever a rename suggestion for `subCaption`. Card-header copy goes under `description`; a caption under the value has nowhere to render, so delete it. | + + **The one-line fix: delete every `subCaption:` entry under `dashboards.*.widgets.*` in your translation bundles.** `os migrate meta --from 17` lists the mechanical edits for existing sources; stored `translation` items are converted when they are read. + + **What an author now sees.** Writing `subCaption` fails `tsc` (its input type is the retired-key mark) and fails the parse with a prescription naming the widget's `description`. Writing `subtitle` on a widget translation fails the parse with both readings named, instead of a rename suggestion onto a key that is refused next. `os validate`, `os build` and `os lint` now raise the `unconsumed-widget-option` warning on an authored widget `options.description`, like any other options key the dataset-bound render path does not read. It is a warning, so none of the three fails on it. + + **Measured producers: none.** Zero `subCaption` entries and zero authored widget `options.description` in the four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and in the bundles `@objectstack/platform-objects` ships, so no shipped exit code changes. + + ### The retirement kit + + - **Tombstone.** `subCaption` is a `retiredKey()` tombstone on the widget translation node, so the refusal carries the prescription on all three faces the node is spread into (per-app bundle entry, platform bundle entry, `translation` item). The node sits under two records (`dashboards`, `widgets`), below the authorable-surface walk, so it has no `RETIRED_KEYS_BY_MAJOR` row, the same as the `submitLabel` component-copy key before it. + - **The former alias.** The `subtitle` → `subCaption` rename suggestion moves to the node's `guidance` table. An alias whose target is a tombstone is the shape the alias-integrity audit refuses, and repointing it at `description` would silently change what the word is taken to mean. + - **Conversion.** `translation-widget-sub-caption-removed` (protocol 18) strips the key from bundle entries and bare translation items as a lossless delete. It is retired from the load path, so authors are refused at parse while stored rows and `os migrate meta` replay it. Its D3 record is the semantic entry `translation-widget-sub-caption-retired`. + - **`@objectstack/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `description`, its one undeclared member, which existed only because the overlay wrote it. `check:widget-option-census`'s `NON_DECLARED_MEMBERS` ledger is now empty, so the census asserts that nothing writes an undeclared key into `options`. +- 1371dc9: `os validate`, `os build` and `os lint` now check an action translation's result-dialog copy against whether the action declares a `resultDialog`, under both `objects.OBJECT._actions.ACTION` and `globalActions.ACTION`. + + Clause-②: no (narrowing) + + + + **What is refused.** `resultDialog.title`, `resultDialog.description` and `resultDialog.acknowledge` under an action that declares no `resultDialog` are now `translation-target-unknown` errors, one per key: the code and level an undeclared `params`, `outcomeMessages` or `resultDialog.fields` key already gets. `translateAction` returns no dialog for such an action, so the copy is never read. Before this, only `resultDialog.fields.PATH` was checked under the dialog, and these three keys passed. + + **What still passes.** The same three keys under an action that declares a `resultDialog` are read and pass, whether or not the dialog sets that text itself. + + **BREAKING** — an accept-set narrowing at the `os validate`, `os build` and `os lint` doors, shipped as `minor` under the launch-window convention. **What changes for a project.** A bundle that carries one of these keys under an action with no `resultDialog` now fails `os validate` with exit 1 instead of passing, and `os build` refuses it. The fix is to move the keys under the action that declares the dialog, declare the `resultDialog` on the action if it should show one, or delete the keys. The four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and the bundle shipped with `@objectstack/platform-objects` produce no finding on these keys, so none of their exit codes change. +- d70353f: fix(lint)!: `os validate`, `os build` and `os lint` refuse an `analyticsCubes` measure whose aggregate the aggregate × field-type table refuses for its column, which the analytics door already refuses at query time + + Clause-②: no (narrowing) + + + + **BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail. A cube measure's `type` is its aggregate, and the analytics door judges every cube measure against `AGGREGATE_FIELD_TYPE_COMPATIBILITY` before any SQL is built: a pair the table refuses is answered `400 INVALID_FIELD`. The authoring check judged only a cube's `count_distinct` measures, so a cube `sum` over a `text` column, for one, passed every command and was refused on its first query. The `measure-aggregate-field-type-refused` id (gating, `error`) now judges every cube measure exactly as it judges a dataset measure. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. + + **What is refused.** On a cube whose `sql` names an object the stack defines: a `measures` entry of `type` `sum`, `avg`, `min` or `max` whose `sql` column is declared with a type outside that aggregate's row of `AGGREGATE_FIELD_TYPE_COMPATIBILITY` (`@objectstack/spec/data`). The four rows accept the numeric and boolean types, `min` and `max` the temporal types too, and `sum` does not accept `percent`; so a text, option, reference, file or structured-JSON column, among others, is refused under all four, and a `date`, `datetime` or `time` column under `sum` and `avg`. The column is the measure's `sql`: a column of the cube's object, or a relationship path read on the object its last hop reaches (the join the cube declares for that hop, else the lookup field's `reference`). + + **What an author sees now.** The finding names the cube, the measure, the column, the object that declares it and its type, the types the aggregate accepts and the aggregates the column's type accepts, and says the analytics door refuses the pair with `400 INVALID_FIELD`. It is located at `analyticsCubes[N].measures.KEY.type`, where `KEY` is the measure's key. A quantity that must be added up, averaged or ordered has to be stored as a numeric or temporal field and aggregated as one; `count` accepts every column. + + **Unchanged.** Every dataset finding and every cube dimension finding; a cube `count` measure over any column; a cube `count_distinct` measure, judged as before; a measure of an expression type (`number`, `string`, `boolean`); the row wildcard `'*'`; a measure whose column does not resolve or declares no type; a cube whose `sql` names no object this stack defines. The runtime metadata write door: no authoring rule is dispatched for an `analytics_cube` save. + +### Patch Changes + +- bdd3654: The list-view field-reference rule no longer walks a list view's own `tabs[].filter` + + Clause-②: no + + The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape, and this rule judges the parsed stack, so the key could never reach the walk: the parse refuses it first, with its prescription. The dead branch is deleted. The rule still judges `filter` and `userFilters.tabs[].filter` exactly as before. + + No finding changes for any stack that `os validate`, `os lint` or `os build` accepts. +- 7e7e64b: Flow, hook, action, approval and expression rule findings no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some findings these rules show to authors through `os validate`, `os lint` and `os build`, and the startup-registry findings a plugin author reads, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - Flow patterns: the record-change date-equality hint and the date-equality filter hint name the declarative alternative, a `schedule` flow whose start node carries a `config.timeRelative` descriptor; the unscoped `runAs` hint says `runAs` is enforced, so a run with no trigger user has its data operations refused rather than run unscoped; the unbounded bulk-write hint says `multi: true` is how a flow declares bulk intent and that the engine admits a whole-object write declared that way; the revise-target hint says the run-resume route continues a pause on a service-owned node type only through the service that owns it; the two interpolation hints say a flow node value is a string template in which only single-brace tokens resolve. + - Startup-registry findings: the open-vocabulary notes say the engine judges node types only once the vocabulary is sealed at `kernel:bootstrapped`; the prescription describes the lazy cache resolution and the ADR-0104 attestation by what each does; the assertive-wording finding describes its two incidents, and how each was fixed, in words. + - Expression findings: the field-level `visibleWhen` consequence names the `current_user` binding ADR-0089 D1 gives every runtime record surface; the retired `script` keys finding says spec 17 made `script` a call to a registered function and nothing else. + - Trigger readiness: the array `triggerType` hint says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + - Body writes, readonly writes and approvals: the discarded `ctx.record` write says the snapshot stays read-only by design and an action writes through `ctx.api`; the `readonlyWhen` write finding says a bulk update strips the field from every matched row once any one of them is locked; the `queue` approver finding says the type was deprecated rather than built; the empty-slate hint says the admin override may act on any pending request, so that one nobody in its slate can decide never stays stuck. + - The other findings drop a citation the sentence already explained. + + Text only: no rule id, severity, condition or finding moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 15b29d3: Data-model, filter, predicate, search, sort, security, seed, view, widget and registry findings no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + The remaining `@objectstack/lint` findings that `os validate`, `os lint` and `os build` show to authors, plus the `surfaceReason` texts of the exported `AUTHORING_RULES` registry and one integrity error, pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - Data model: the bare declared `unique: true` warning says that protocol 18 rejects the spelling and that stored metadata still carrying it converts to `unique: 'global'`, which builds the same physical index. + - Empty filter combinators: the `$and: []`, `$or: []` and empty-node messages say every backend reduces an empty combinator to its boolean identity; the `$or: []` message says an empty disjunction never opens a read scope to the whole table. + - Null guards: the fail-closed outcome says a predicate that cannot evaluate refuses the write rather than being skipped. + - Visibility and metadata-form predicates: the fall-open consequence says failing open is the console's settled behaviour; the dotted right-hand-side message says the form evaluator keeps its right-hand side a literal by design and says why only in a development build. + - Component props: the advisory hint says props are judged at the authoring door as a warning before they become an error. + - Rule schema formats: the format hint says `rule-validator.ts` registers the default `ajv-formats` set so that a `format` is enforced on every write. + - Security posture: the unset-OWD message describes the leave_request incident (an object with no `sharingModel` let an ordinary read/write grant read and edit every other user's records); the `controlled_by_parent` message says the write is refused as a metadata defect rather than a permission denial. + - Seeds and views: the seed state-machine message says a seed records established facts rather than walking the lifecycle; the `views:` container message says the stack schema, the rule and the registration loop hold `views:` to one container-only contract. + - React pages: the absent-`groupBy` hint states the ruling directly. + - Liveness: the unrecognised-status integrity error says such a status fails loudly rather than being graded `dead`. + - `AUTHORING_RULES` `surfaceReason` texts: the full-snapshot, capability-reference and sharing-rule reasons name the runtime publish gate (the Studio, REST and MCP door that runs this registry) in place of a tracker number; the advisory-volume reason says the object door opened to the gating object rules alone; the component-types reason names the crossing discipline the gating object rules went through. + - The other findings (search fields, sort fields, nav servability, dashboard actions, widget bindings and the remaining predicate and combinator messages) drop a citation the sentence already explained. + + Text only: no rule id, severity, condition, finding or registry field moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling. +- cfa4d74: `page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5). + + Clause-②: no + + The key's description used to say the list is "validated at save and load" while the liveness ledger recorded it as not enforced yet. Both are now true and say so. On a server that has the deployment's SDUI component manifest, saving a `kind: 'html'` page compiles its source, refuses a written `requires` that disagrees with it (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`; a draft at its publish) and stores the derived list. At load, a stored page whose list names a plugin no manifest component carries is reported and still served. A server with no manifest checks neither and says so once at boot. Omit `requires`: it is derived from the source. The liveness row moves from `planned` to `live`, and the generated page reference carries the new description. + + `validateJsxPages`' reason for staying off the runtime publish gate no longer says it parses through `typescript`/`sucrase`. It parses with the dependency-free `@objectstack/sdui-parser`, and it stays CLI-only because the save door already runs that compiler on every html page. The `ui-html-page-div-refused` upgrade-guide entry now names that save door too: on a server with a manifest, a `div` page saved from Studio or through the metadata API is refused under the same rule ids. + + No schema accepts or refuses anything it did not before, and no runtime behaviour changes. +- dcc5ef4: `deriveInlineRowFormFields` and `isInlineRowFormOffered` (`@objectstack/spec/data`) state which fields an inline master-detail grid's per-row expand form draws and when that form is offered, and `field-no-consumers` stops calling four more kinds of in-use child field "inert" (#21091). + + Clause-②: yes (widening) + + - **`@objectstack/spec`.** Two new exports from `@objectstack/spec/data`, beside `deriveInlineGridColumns`: + - `deriveInlineRowFormFields(def, { relationshipField?, exclude? })` returns the child field names of the per-row expand form, in the child's field order. It skips the same system, audit, tenancy, ownership and sort-position names as the grid, the relationship field, `exclude`, `system` and `hidden` fields, and the computed types (`formula`, `summary`, `rollup`, `autonumber`, `auto_number`). Unlike the grid it keeps `readonly` fields and the rich types a cell cannot edit (`richtext`, `json`, `markdown`, …), so the derived grid's columns are always a subset of its fields. + - `isInlineRowFormOffered({ inlineMode?, formFields?, columns? })` is `true` when the form factor is `form`, or when the form has more fields than the grid has columns. + - Both are the renderer's current rule, reproduced exactly. No schema accepts anything new or refuses anything new. + - **`@objectstack/lint`.** `os validate` no longer warns that these fields are inert: + - a `lookup` field that sets `inlineEdit`: it is the inline grid's join key, read whatever columns the grid draws, as a `master_detail` field already was; + - a field a derived inline grid's per-row expand form draws, through `deriveInlineRowFormFields`, such as a `readonly`, `richtext` or `json` child field; + - a field named in an `object-master-detail-form` detail entry's `formFields`, now read against the entry's `childObject` instead of the block's object. When the form is never offered for the list, the list is reported as a carrier. That is judged on an entry that names both its `relationshipField` and its `columns` under its declared `inlineMode` or none. On any other entry it is judged under a declared `inlineMode` where the grid can be counted: authored `columns`, or the derived grid of a named `relationshipField`. Otherwise the list is credited as drawn; + - a field named in a `record:line_items` block's `columns`, `relationshipField`, `amountField`, `sort` or `filter`, now read against the block's `childObject`. + + A parent field that shares a name with one of those child fields was credited in the child's place, and is now reported if nothing else reads it. A child field nothing draws or names, such as a `hidden` one, is still reported. +- 6210f88: `field-no-consumers` no longer calls a field "inert" when a dataset or cube member reads it through a relationship path (#21439). + + Clause-②: no + + `os validate`, `os build` and `os lint` warned "Verdict: inert — no site of any kind names it" for every field an analytics member reached through a path such as `account.revenue`, so an author following the warning would delete a column a measure reads. The four slots that name a column are a dataset dimension's and measure's `field` and a cube dimension's and measure's `sql`. Each one now credits every field its path reads: the lookup on the base object, each intermediate lookup, and the column on the object the last hop reaches. + + - **Hops resolve the way the analytics door resolves them.** A cube hop goes through the join the cube declares for it, else the lookup's `reference`. A dataset hop goes through the `reference` its compiler joins through, and only where the dataset's `include` declares the join. + - **A bare cube column is credited too.** Before, a cube member's `sql: 'amount'` credited nothing, because a cube names its object in its own `sql`. + - **A path the door refuses reads nothing.** Examples: a join the dataset's `include` does not declare, a hop that names no relationship, a column the last object does not have. Each field such a path names is now listed as a carrier site that a removal must clean (`carrier-only`), not as a reader. + - **A path the object graph cannot judge** credits the fields it does resolve. An example is a lookup to an object this stack does not define. + + Nothing new is refused, and the rule stays a warning. One warning can appear where there was none: a path the door refuses through a lookup named after its target object (`account.revenue`, with `account` a lookup to the object `account`). The old text scan credited its column as read. It is now reported `carrier-only`, beside the error the refused path already carries. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [0a0debb] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [a4fd82a] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/sdui-parser@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 9fbe47d4657..8882f4b5d09 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.6.0", + "version": "17.7.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 fc442096970..5d90b9934ab 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/plugin-mcp-server +## 17.7.0 + +### Minor Changes + +- 713b0fa: fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **Three things change for callers and operators.** + + 1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". + 2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. + 3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. + + **What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. + +### Patch Changes + +- 6cf1154: The MCP server's `serverInfo.version` is the package version unless you set one, as `MCPServerPluginOptions.version` always documented ("Defaults to package version") (#21532). + + Clause-②: no + + - Before, `MCPServerPlugin` and `MCPServerRuntime` each defaulted to the literal `1.0.0`, so every deployment built without the option, `os serve`'s auto-registration included, answered `initialize` with `serverInfo.version` `1.0.0` whatever the installed `@objectstack/mcp` was. Both defaults now read the version from the package's own `package.json`, ESM and CJS alike. + - An explicit `version` option (`MCPServerPluginOptions.version`, `MCPServerRuntimeConfig.version`) is still answered as given. + - `new MCPServerPlugin().version`, the kernel plugin's own version, is the package version too, where it was `1.0.0`. Its declared type is now `string | undefined`: if the manifest cannot be read (a bundle with no `package.json` beside it), `serverInfo.version` says `unknown` and the plugin's own `version` is left unset, which both kernels accept, instead of a placeholder they would refuse. + - Pass `version` yourself to keep reporting a fixed string. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 2943fd02063..838fc7bb294 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.6.0", + "version": "17.7.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 6fc78ee20b1..dc9b432115f 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,221 @@ # @objectstack/metadata-core +## 17.7.0 + +### Minor Changes + +- 83b3d32: Public forms on a walled tenancy posture: saving or publishing a view whose public form cannot take anonymous intake now tells the author why, on the response. + + Clause-②: yes (widening) + + On a walled posture (`group` or `isolated` in force), an open public form whose object is walled by an organization column cannot take an anonymous submission: the submission carries no organization, and an insert without one into a walled object is refused. The two anonymous form endpoints already answer such a form as a withdrawn one (`404 FORM_NOT_FOUND`), and the administrator's read of the view (`GET /meta/view/:name`) already states why in `_diagnostics.warnings`. + + - **`@objectstack/metadata-protocol`**: saving the view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and a package's batch publish) now answers success with one `warning` advisory per such form, under `advisories`, with rule `public-form-intake-unavailable`. It is located at the form's `sharing` (for example `views[0].formViews.contact.sharing`), its `message` is the same text the administrator's read states, and its `hint` is the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. The write is never refused. The advisory reads the posture in force from the `tenancy` service, which is what the anonymous endpoints read: a single-posture deployment, a deployment whose walled posture is degraded to `single`, a deployment with no tenancy service, and a form bound to a tenancy-disabled object get no advisory, and a draft save is not judged. The publish refusal for an unstamped platform schedule flow still reads the requested posture, as before. + - **`@objectstack/metadata-core`**: the intake-availability rule moved here from `@objectstack/rest` and is exported, so the anonymous endpoints, the administrator's read and the publish advisory read one answer: `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` (`null` when the form can take intake, otherwise the object, the posture and the wall column; it judges the object's effective schema, with the injected `organization_id`), `anonymousFormIntakePosture(tenancy)` (the posture in force, as a tenancy service reports it), `anonymousFormIntakeUnavailableMessage` and `anonymousFormIntakeUnavailableRemedy` (the reason and its remedy), `anonymousFormSharingPath` and `anonymousFormObjectName`, and the type `AnonymousFormIntakeUnavailable`. + - **`@objectstack/rest`**: the anonymous form endpoints and the administrator's read import that rule instead of holding their own copy. Their answers are unchanged. +- a6a7547: The object-schema field mask (ADR-0106 D1) now removes a denied field's references from the rest of the served object document, not only its `fields` entry. + + Clause-②: no + + A caller who cannot read a field was served a document without that field's definition, but other parts of the document could still name the field. Those parts are now projected too, on every exit that serves an object schema to a restricted caller: the by-name read, the list read and the layered view. What happens to each kind of position: + + - **Object-level rule entries** (`validations`, `indexes`, `activityMilestones`). An entry that names or reads a denied field is dropped whole. The platform still evaluates the stored rule on every write. + - **Role pointers** (`nameField`, `displayNameField`, `imageField`, `stageField`, `tenancy.tenantField`, `lifecycle.ttl.field`). A pointer to a denied field is deleted, so the role falls back to its default. + - **Name lists** (`highlightFields`, `searchableFields`, `publicSharing.redactFields`, list-view column lists, `external.columnMap` entries, and a readable field's `relatedListColumns` / `dependsOn`). The denied entries are filtered out. A list-view column in object form is dropped when any of its facets names a denied field: its own `field`, its `prefix.field` or its `summary.field`. A `dependsOn` entry's `param` is the lookup target's key and is not read as a field here. A list left empty is deleted. + - **The inline master-detail grid** (`inlineColumns`, `inlineAmountField`). These are declared on the child object's own `master_detail` field and name the child's own fields. A column that is a denied field, or is computed from one (`expr`), is dropped. A denied `inlineAmountField` is deleted. + - **Expressions** (`titleFormat`, field-group `visibleWhen`, row-CRUD `visibleWhen` / `disabledWhen`, `publicSharing.eligibility`, lifecycle `onlyWhen`, and a readable field's formula `expression`, `visibleWhen` / `readonlyWhen` / `requiredWhen`, `relatedListFilter`, `defaultValue`, `autonumberFormat` and per-option `visibleWhen`). An expression that reads a denied field is deleted. The readable field itself stays. + - **List views and actions.** An entry whose filter, sort, predicate, params or `patch` read a denied field is dropped. So is a list view whose key is a denied field's name. + + In these positions an object key counts as a field reference only where keys are field names: a `FilterCondition`, a lifecycle `onlyWhen` map, an action's `patch`. A denied field named like a schema word (`type`, `source`, `name`) no longer removes every rule, view, action or CEL envelope. A dotted path whose root segment is a denied field counts as a reference to it, whether it is a field-keyed key, a name-list entry or a pointer. The mask also terminates on a cyclic document. + + Names of another object's fields (`lookupColumns`, `displayField`, `summaryOperations`, …) are left alone, because that object's own projection governs them. A key that is not classified is deleted when it mentions a denied field in any string or key, so a new key over-masks until it is classified. A test holds the classification equal to the live `ObjectSchema`, `FieldSchema`, `InlineGridColumnSchema`, `ListColumnSchema`, `ColumnPrefixSchema` and `ColumnSummaryConfigSchema` key sets. A caller who is denied nothing still gets the same document reference, so nothing changes for unrestricted callers. + + The shared contract fixture in `@objectstack/metadata-core/testing` (`FLS_CONTRACT_OBJECT`) now names its fields in each of these positions, including list views (one with object-form columns), actions and the inline grid, and its formula field uses the real `expression` key. The contract's residue check matches a denied name as an identifier token anywhere in the served document, so a name inside an expression fails it. Projection cases also check what must survive (`retained`), so a mask that deletes too much fails as well. +- e83c9f6: A package install refused by the ADR-0087 D1 protocol handshake now answers `422 OS_PROTOCOL_INCOMPATIBLE`, with its structured diagnostic in `error.details`. It used to answer the `500` server-fault fallback, with the diagnostic only inside the message. + + Clause-②: yes + + - **`POST /api/v1/packages`:** a manifest whose declared range (`engines.protocol`, then `engines.platform`, then `engine.objectstack`) excludes this runtime's protocol major answers `422`, with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'` and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. `error.message` is unchanged, and the command after its `Run:` equals `migrateCommand`. Nothing is installed, so a later `GET /api/v1/packages/{id}` still answers `404`. + - **The no-protocol-service fallback:** in a composition without a `protocol` service, `POST /api/v1/packages` used to install such a manifest. It now runs the same handshake and answers the same `422`. + - **`@objectstack/metadata-core`:** `ProtocolIncompatibleError` declares `status` and `statusCode` `422`, with `code` as the literal `'OS_PROTOCOL_INCOMPATIBLE'`. It also carries a `Symbol.for` brand, and the new `isProtocolIncompatibleError(e)` recognises it across module instances, where `instanceof` would not. Any other caller that resolves the error (the boot-time `AppPlugin` load included) reads `422` rather than the `500` fallback. + - **`@objectstack/spec`:** the error-code ledger's `OS_PROTOCOL_INCOMPATIBLE` row states its status (422) and the door that carries the diagnostic. The vocabulary does not change. + + A client that branched on `500` for this code should branch on `422`, or on `error.code`. None was found in this repository, the SDK or the console. + + +- 75ddcd1: `POST /api/v1/marketplace/install-local` now runs the ADR-0087 D1 protocol handshake. A manifest whose declared range excludes this runtime's protocol major is refused with `422 OS_PROTOCOL_INCOMPATIBLE`, the answer `POST /api/v1/packages` already gives. It used to install with a `200` (#21762). + + Clause-②: yes (widening) + + - **Install.** The handshake runs after the manifest id is parsed and before anything is registered, written or synced. The range is read from `engines.protocol`, then `engines.platform`, then `engine.objectstack`. The refusal answers `422` with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'`, the handshake's own `error.message`, and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. It is the same on the inline-manifest branch and the cloud-snapshot branch. No ledger file is written, and an installed earlier version stays as it was. A manifest with no range, or a range the handshake cannot read, still installs, and the handshake's warning goes to the plugin's logger. + - **Restart.** On `kernel:ready`, a ledger entry whose range excludes this runtime's major is not loaded. Nothing is registered, synced, bound or seeded for it. One `error` line names the package, `OS_PROTOCOL_INCOMPATIBLE` and the replay command (`objectstack migrate meta --from N`). The boot continues with the other entries. The entry stays in the ledger, so `DELETE /api/v1/marketplace/install-local/{id}` still removes it, and installing a compatible version replaces it. Before, it was registered and its schemas synced, with no warning. + - **`@objectstack/metadata-core`:** a new export, `protocolIncompatibleAnswer(err)`, with its return type `ProtocolIncompatibleAnswer`. It turns a `ProtocolIncompatibleError` into the status, code, message and five-member `details` an HTTP door answers. Both install doors call it, so their answers are the same bytes. + - **`@objectstack/runtime`:** `POST /api/v1/packages` answers through that helper. Its response is unchanged. + + A client that relied on install-local accepting a package built for another protocol major gets `422` now. Install a version built for this runtime's protocol, or migrate the package with the `migrateCommand` in the refusal. +- e1790fd: The content hash keeps the order of an object's `fields`, so a pure field reorder is a new version instead of "no change" (#21790). + + Clause-②: yes + + - **`canonicalize(value, type?)` and `hashSpec(value, type?)`** take the metadata type. For a type whose body has a map the spec declares ordered, that map keeps its insertion order in the canonical form. Today that is one map: `object.fields`, whose traversal order is the field order the platform presents. Every other map stays key-order independent, including the keys around `fields` and the keys inside each field definition. Called without a type, both functions return exactly what they returned before. + - **`orderedMapKeys(type?)`** is a new export. It returns the top-level keys of a `type` body whose map keeps its order (`['fields']` for `object`, `[]` otherwise). + - `InMemoryRepository` and the repository contract suite hash as `ref.type`. Invariant 4 now reads `item.hash === hashSpec(item.body, item.ref.type)`. + - **Stored hashes.** An object whose `fields` are already in sorted key order hashes exactly as before. Any other object hashes differently from the hash stored before this release. A stored hash is still that row's version token: `@objectstack/metadata-protocol` keeps it as written and compares content to decide whether a save changed anything. + + `minor` because two exports widen: a new parameter and a new function. No metadata key, accepted value, wire payload or error code changes. +- e6dc7a2: The object-schema field mask (ADR-0106 D1) judges an action param that names another object's field through `objectOverride` against that object, not the one being served. + + Clause-②: yes (widening) + + **What a user saw.** A `delegated_admin` may invite members, and the invite door admits them, but `GET /meta/object/sys_user` served that principal no `invite_user` action. The action's `role` param is `{ field: 'role', objectOverride: 'sys_member' }`: it names `sys_member.role`. The mask read every param's `field` as a field of the served object, so a caller denied `sys_user.role` lost the whole action. A plain `member` lost it the same way. The member is now served the action too, and still not offered it: the action's `requiresMembershipReach` predicate excludes the member grade. + + **The rule.** A param whose `objectOverride` names another object reads that object's field. It is judged against the caller's readable fields on that object, and it is not a reference to the served object's fields. The action is still dropped when the caller cannot read the field there, and when that object's readable fields cannot be determined (no answer from the security service, a security service that throws, or an object that does not exist). Nothing about the other object is served on a guess. The rest of the param is still read against the served object: `visible`, an option's `visibleWhen`, `defaultValue`, and an explicit `name` that differs from `field`. A `name` that only repeats `field` is read as that field. With `defaultFromRow`, the param also reads `field` from the served object's row, so `field` is judged against the served object too. An exempt caller (platform admin, `isSystem`) is served the whole schema, as before. + + **The API (`@objectstack/metadata-core`), additive.** + + - `relateObjectSchemaMaskPosture(posture, ...documents)` completes a `project` posture for the documents it is about to mask. It reads the caller's readable fields on each other object their action params name through `objectOverride`. It runs after the fetch, because only the document names those objects. It returns every other posture, and any document with no such param, unchanged, and it never throws. + - The `project` member of `ObjectSchemaMaskPosture` gains two optional fields. `relate` asks the posture's question (same caller, same security service) about another object. `resolveObjectSchemaMaskPosture` sets it. `related` holds the answers. A `project` posture built without `related` gets no answers, so `applyObjectSchemaMask` drops every action with such a param. + - `applyObjectSchemaMask` folds each related read it withholds into the fingerprint, written as `object.field`. Two callers who are denied the same fields on the served object but differ on the other object get different validators. An unrestricted caller's ETag is unchanged. + - The shared contract fixture `FLS_CONTRACT_OBJECT` (`@objectstack/metadata-core/testing`) gains two actions whose params read `contact` fields through `objectOverride`. The contract's projection cases now require the readable one to be served and the denied one to be dropped. An exit that never relates its posture fails the contract by name. + + **Every exit relates its posture (`@objectstack/rest`, `@objectstack/runtime`).** These exits relate the posture after the fetch, before the projection: the shared item, layered and list chains, `RestServer`'s cached read and published read, and the runtime dispatcher's mask. The `/meta` diff route masks `fields` only and needs no relate step. + + **Measured on a showcase boot.** We read every object schema (78 objects, by-name read and list read) as five principals: a platform admin, an org owner, an admin, a `delegated_admin` and a `member`. Before and after this change, the only served action that moved is `sys_user.invite_user`, which is now served to the `delegated_admin` and the `member`. This repository has two authored params with `objectOverride`: `sys_user.invite_user`'s `role` and `sys_member.invite_user`'s `email` (on `sys_invitation`). The second was served to all five principals before and after. +- 3c7785d: A public form's explicit intake withdrawal at any metadata layer now holds: layering can only narrow anonymous intake, never re-open it + + Clause-②: yes (widening) + + - **What counts as a withdrawal.** A withdrawal keeps the form's `publicLink` and sets `sharing.enabled: false` or `sharing.allowAnonymous: false`. Only an explicit `false` counts: a switch that is absent is not a withdrawal. Removing the `sharing` block, clearing the `publicLink`, or deleting the view at one layer is not a withdrawal either. A sharing that names no public link withdraws nothing. + - **Organization-scoped saves and publishes.** A `view` save or draft promotion in the organization the anonymous form doors read is refused with `403 NOT_OVERRIDABLE` if it would leave open a form that the environment-wide definition withdraws. This check judges by the stored row: the organization's body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package's artifact), and also with the env-wide view list the way the doors read it (a container-shaped body is expanded the way the list read expands it). Inside the row, a withdrawn form matches by its place (`form`, the same `formViews` entry, or `config`) or by its public slug, and either match is enough. So a renamed `formViews` key, a `form.name`, a move to another place, a listViews collision rename in the expansion, and a new or re-cased slug are all judged as the same form. A form that differs from every withdrawn form in both place and slug, such as a sibling in the same container, stays independent. The check also covers an organization copy that was already open before the withdrawal, the next time it is saved. The message names the remedies: save the overlay withdrawn, or publish the form from its environment-wide definition. An organization-scoped save that keeps the form withdrawn is still accepted. + - **Anonymous form doors.** `GET /forms/:slug` and `POST /forms/:slug/submit` judge by the name of the view item they serve. Beneath the organization's read they read the env-wide view list, and they serve a form only when the env-wide item of the same name does not explicitly withdraw a form in the same place or with the same slug. A withdrawn form answers `404 FORM_NOT_FOUND` on both doors and creates no record. A form that is open at every layer is served as before. A form that only an organization carries is still served there. A different view that uses the same slug is a different form, and the two never close each other. + - **Package-shipped forms.** A package's form is part of the env-wide definition, not a separate layer beneath it. A package artifact that was parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal (fail closed). An artifact that reached the runtime without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: there a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch: an env-wide save may open a form the package ships closed. + - **Known limit: packages and names.** A withdrawal of a view name closes that name in every package. When two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name: it may over-close, never under-close. Per-package precision is tracked in #21934. A publish judges the draft it promotes under the same package key: with two packages holding a draft of the same view in one organization, each draft is judged on its own publish. + - **Known limit.** The doors match by served item name, and the save check runs only on an organization-scoped save or publish. An organization overlay that was stored before the env-wide withdrawal, or that a rollback or commit-revert restores, can still be served if it keeps the form open under a different key or place than the env-wide definition. Withdraw the form in that overlay to close it. Rollback and commit-revert restores are not gated by the save check. + - **Behaviour change.** Between 17.6.0 and this fix, an organization overlay that published a form the environment-wide (package) definition withdrew was honoured: the doors served the organization's copy. That behaviour never shipped in a release, and it is reversed on purpose. The environment-wide withdrawal now wins. + - **`@objectstack/metadata-core`** exports the shared judgement `anonymousFormIntakeWithdrawnIn` (a new, additive public export). Both the doors and the save path read it. +- 6dd99b8: Public forms: every declared means of withdrawing a form from anonymous intake is now honoured by every anonymous form door. Which forms a `view` opens to anonymous intake is now decided by one rule, `anonymousFormIntakeCandidates` (new in `@objectstack/metadata-core`, alongside `anonymousFormIntakeSlugs`, `anonymousFormIntakeSlug` and `publicFormSlug`), read by both the anonymous form endpoints in `@objectstack/rest` and the organization-scoped `view` write check in `@objectstack/metadata-protocol`, so the two can no longer disagree. A form is served anonymously only when its `sharing` config declares public sharing as `SharingConfigSchema` defines it: `sharing.enabled: true`, `sharing.allowAnonymous: true` and a `sharing.publicLink` slug. `enabled` defaults to `false`, so a form that set only `allowAnonymous` and `publicLink` is no longer served on the anonymous endpoints (`404 FORM_NOT_FOUND`). Migration: add `enabled: true` to the form's `sharing` block (and to any stored overlay of it) to keep it public; see the public forms guide. + +### Patch Changes + +- c98a72d: Provenance comments in `@objectstack/metadata-core` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each now cites the commit in this repository's history that made the decision it describes, with two + exceptions: two comments on `retiredFromLoadPath`'s jurisdiction (in `artifact-forward-conversion.ts` + and its test) cite ADR-0087, which records that determination, and five comments that meant an + objectui issue now spell it `objectui#6111`, as they already spelled `objectui#6110` beside it. One + commit citation sits inside a maintainer ruling quoted in `record-organization.ts`: the number there + became the bracketed editorial substitution `[commit 7901b2dd2]`, the commit that landed the ruling it + names, and the rest of the quotation is unchanged. Some of these docblocks sit on exported members, so + the reworded text appears in the published declaration files (`index.d.ts` / `index.d.cts`, + `testing.d.ts` and a shared declaration chunk); the JavaScript output and its sourcemaps do not change. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index efeedad8f0e..3fd62a36809 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "17.6.0", + "version": "17.7.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 73542b08d80..f7f3a34ce25 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,36 @@ # @objectstack/metadata-fs +## 17.7.0 + +### Patch Changes + +- 1e4ae08: Provenance comments in `@objectstack/metadata-fs` cite the commit that decided them, not a tracker number that no longer resolves + + Clause-②: no + + Comments and docblocks in the package cited an issue-tracker number that now answers 404 on GitHub. + Each one now cites the commit in this repository's history that made the decision it describes. One of + these docblocks sits on a public method (`FileSystemRepository.close()`), so the reworded text appears in + the published `index.d.ts` / `index.d.cts` and, because esbuild keeps that docblock, in the JavaScript + output (`index.js` / `index.cjs`); the sourcemaps do not change. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- e1790fd: `FileSystemRepository` hashes each item as its metadata type, so a reorder of an object's `fields` is a change (#21790). A reordered object is written, and an external edit that only reorders `fields` is reported as an update. + + Clause-②: yes + + Event-log entries written before this release carry the order-blind hash. For an object whose `fields` are not in sorted key order, `get()` and `list()` no longer find that entry until the item next changes. They fall back to the defaults: no parent hash, sequence `0`, the filesystem actor, and the epoch timestamp. The body and the version hash are unaffected. +- Updated dependencies [c98a72d] +- Updated dependencies [83b3d32] +- Updated dependencies [a6a7547] +- Updated dependencies [e83c9f6] +- Updated dependencies [75ddcd1] +- Updated dependencies [e1790fd] +- Updated dependencies [e6dc7a2] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] + - @objectstack/metadata-core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index af2d10b157b..499230b2d98 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "17.6.0", + "version": "17.7.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 73014dc520f..55f80584238 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,716 @@ # @objectstack/metadata-protocol +## 17.7.0 + +### Minor Changes + +- 96a9719: feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790) + + Clause-②: yes (widening) + + A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it. + + **⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '' (): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification. + + What else changes: + + - **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`. + - **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential. + - **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials. + - **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing. + - **Refused now, loudly**: + - With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again. + - The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret. + + +- 713b0fa: fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **Three things change for callers and operators.** + + 1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". + 2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. + 3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. + + **What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. +- 5555047: The runtime save door refuses a view container whose own `name` disagrees with the name it is saved under + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the ObjectQL boot loop's refusal of the same divergence shipped with. + + **What was accepted before.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, accepted an aggregated view container (`list` / `form` / `listViews` / `formViews`) whose body carried a `name` different from the name it was saved under. It stored the row under the save name and registered the container under the body's `name`, so one document answered under two names. The source registrars (the ObjectQL boot loop and the artifact/HMR loader) and `os validate` already refused a container whose `name` disagrees with the key they file it under. + + **What is refused now.** That body, with `VALIDATION_ERROR` / 400, before anything is stored or registered, through the same judge the source registrars call (`@objectstack/metadata/view-container-name`). The key judged here is the save name: a container saved under a name other than the object it binds to still saves, and so does the body the door stores for it when it is read and sent back. + + **The fix.** Drop the body's `name` (the door stamps the save name), or set it to the name the container is saved under. + + A standalone view record (`viewKind`) and every other metadata type are judged too, by the same release's every-type refusal at the save, restore and publish doors (its own entry). +- 2f837a5: fix(runtime)!: the in-process reader contexts refuse the stored-metadata-body family's EVALUATE shapes and serve what a write returns, the way the generic data door does (#21454) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what an action or hook body's object API, an action handler's scoped API and an action handler's engine handle accept when they read the two stored-metadata tables. A read there that filters, sorts or groups on the stored body column or on a content-hash column, a read that names one of those columns in an explicit search-field list, and a `count` carrying such a filter, ran before this release and now answer the generic data door's `400 INVALID_FIELD` before the query runs. The route: filter, sort, group and search those tables by their scalar columns (the type, the name, the state and the like), and read the bodies with a plain list, which is served projected — the body as its type's read projection, the content hash in keyed form. A default search with no field list is not refused: it is narrowed to the columns the door serves. Every other column of the two tables, and every other object, is unchanged. It ships as `minor` under the launch-window convention for accept-set narrowings. + + - **`@objectstack/metadata-protocol`** now exports the generic data door's four evaluate-refusal predicates — `storedMetadataBodyGroupingRefusal`, `storedMetadataBodyPredicateRefusal`, `storedMetadataHashEvaluateRefusal` and `storedMetadataSearchRefusal` — so the `@objectstack/runtime` reader-context seam refuses the same shapes through the door's own predicates rather than a second copy. Additive: nothing that imported the package before is changed. + - **`@objectstack/runtime`** extends the stored-metadata reader-context seam (`ctx.api.object(...)` for action and hook bodies, a handler's `ctx.api`, and `ctx.engine.find`): a filter, sort, grouping or search that would evaluate the stored body or content hash of `sys_metadata` / `sys_metadata_history` is refused with the door's `INVALID_FIELD` / 400 before the query runs (a `count` with such a predicate included); a default `$search` is narrowed to the door's served field set rather than refused; and the row a write verb returns is served projected and keyed. The engine's own action verb (`ScopedRepo.execute`) is unreachable from a served body and is left untouched. +- abe8f28: fix(runtime): a sandboxed body or an action handler that reads the stored-metadata tables is served what the generic data door serves (#21454) + + Clause-②: yes + + The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and a content hash computed over it. The generic data door serves such a row with the body as its type's read projection, with the stored credential material withheld, and the hash in keyed form. Three in-process reader contexts served the same rows as stored: + + - a sandboxed action or hook body that reads through `ctx.api.object(...)`, inside `ctx.api.transaction(...)` too; + - an action handler that reads through `ctx.engine.find(...)`; + - an action handler that reads through `ctx.api.object(...)`. + + An action body and an action handler run elevated, so the stored form reached whoever could invoke the action, a member included. + + **What changes.** A read of either table through any of these contexts now answers the data door's form: the projected body, and the content hash under the same key the data door uses. That key is the crypto provider's, or the process-scoped ephemeral key when no provider is registered. `find`, `findOne` and `aggregate` are served this way, and so is every context the scoped API derives: `sudo()`, `withRunAs(...)`, a `transaction(...)` callback's context, and the context `beginTransaction()` returns. A hook body that copies what it read into another record can now copy only the projected form. A projection that names the body column without the type column reads the type beside it and drops it again, as on the data door. + + **What does not change.** Every other object, every write and `count` behave as before. The platform's own readers of these tables still read the stored form, because the projection is applied at the reader contexts and not in the engine. + + `@objectstack/metadata-protocol` now exports the data door's stored-row serve, so these contexts consume it and keep no copy: `storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows`, `ephemeralStoredHashDigest` and the `StoredHashDigest` type. The exports are additive. + + The four functions `storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows` and `ephemeralStoredHashDigest`, and the type `StoredHashDigest`, are new public API of `@objectstack/metadata-protocol`, and `@objectstack/runtime` consumes them. +- 44defd4: Every runtime door that writes a metadata row refuses a body whose own `name` disagrees with the row's name, for every metadata type + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime write doors, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the view-container half of this refusal takes in the same release. + + **What was accepted before.** The runtime stores a metadata row under the name the request names and registers its body under the body's own `name`. These doors accepted a body whose `name` was not the row's, so the row answered under a name nobody saved it under, and under none by its own: + + - `saveMetaItem`, which `PUT /api/v1/meta/:type/:name` and the dispatcher's metadata save both call, for every type but a view container (a dashboard saved as `dash_a` with `name: 'dash_b'` registered as `dash_b`; a record view saved as `crm_lead.mine` with `name: 'crm_lead.other'` registered as `crm_lead.other`); + - `rollbackMetaItem` and `revertCommit`, which wrote such a stored history version back as the active row without passing `saveMetaItem`; + - `publishMetaItem` and `publishPackageDrafts`, which promoted such a stored draft the same way. + + **What is refused now.** Each of those bodies, with `VALIDATION_ERROR` / 400, before anything is stored or registered, through the judge the view-container refusal already used (`savedItemNameRefusal`, `@objectstack/metadata/view-container-name`). `rollbackMetaItem` and `publishMetaItem` throw it. `revertCommit` reports the item in `failed[]` with `code: 'VALIDATION_ERROR'`. `publishPackageDrafts` aborts the batch on it, as it does on any refused draft: nothing in the batch is published. A body with no `name` is accepted as before. A `name` the body carries is judged whatever its value; a `translation` saved with `name: ''`, which its schema accepts, is now refused instead of being registered under the empty string. A view at the save door is the exception: a missing or empty view `name` is still stamped with the save name. A `field` written through the `OS_METADATA_WRITABLE` operator hatch is accepted only without a body `name`: its row is named `object.field`, which the column `name` cannot spell, and registered it answered under the column name alone. Where the type's schema already refused such a body (an empty or non-string `name` on most types, any `name` on a `seed`, whose schema declares none), the answer is now this refusal (`VALIDATION_ERROR` / 400) instead of the schema's `INVALID_METADATA` / 422; nothing is stored either way. + + **The fix.** Set the body's `name` to the name you save it under, or save the item under the body's own `name`; for a view or a `field`, dropping `name` works too. To bring back a version or a draft that carries another `name`, save the item again with that fix, and publish that save if it is a draft. +- 74281a8: fix(cloud-connection): an install-local uninstall runs the protocol's registered uninstall cleanups, so the package's permission sets and their grants go with it + + Clause-②: yes + + `DELETE /api/v1/marketplace/install-local/:manifestId` removed the package's ledger entry and nothing else. After a restart the package's objects were gone, but its `managed_by: package` rows in `sys_permission_set`, and every grant of them, survived the uninstall. That broke ADR-0090's "No ghost grants" promise on this door. + + The door now runs the uninstall cleanups that domain plugins register with the protocol (`registerUninstallCleanup`) once the ledger entry is gone. It uses the same registry and the same runner as the protocol's own uninstall, so `plugin-security`'s `security.package-permissions` cleanup removes the package's sets with their position and user bindings, and any cleanup registered later fires here too. The cleanups run with the package's manifest id and no organization, because an install-local package is installed for the whole runtime. + + The response carries each outcome as `data.cleanups`, the way the protocol's uninstall reports them. A failed cleanup is reported there and named in the operator log with its remedy (install the package again, then uninstall it again). When the protocol cannot run the cleanups, the response says so as one failed `protocol.runUninstallCleanups` outcome. An uninstall that does not happen (a refused caller, an id this door never installed, a ledger write that fails) revokes nothing. + + `@objectstack/metadata-protocol`: `ObjectStackProtocolImplementation` gains `runUninstallCleanups({ packageId, organizationId?, actor? })`, the one runner of the uninstall-cleanup registry. It runs every registered cleanup for the package and answers one `UninstallCleanupOutcome` per cleanup. It never throws: a failed cleanup is an outcome, and a thrown fault's driver text goes to the operator log, not into the outcome. `deletePackage` now calls it as its last step in place of its own loop, and its `cleanups` are unchanged. The only visible difference there is the log tag of a failed cleanup's warning, now `[protocol.runUninstallCleanups]` instead of `[protocol.deletePackage]`. + + `@objectstack/cloud-connection` now declares its dependency on `@objectstack/metadata-protocol`, which it already received through `@objectstack/runtime`, for the cleanup outcome types. +- 5d0e4e2: fix(metadata-protocol)!: the generic data door refuses a stored-metadata filter that reads the body or a content hash through a cross-field comparand or below its depth backstop, and exports its one filter-field collector and one search narrowing for the reader-context seam (#21544) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the generic data door (`GET /api/v1/data/:object`, `POST /api/v1/data/:object/query` and the in-process `findData`) accepts when it reads `sys_metadata` or `sys_metadata_history`. Two filter shapes read the stored body column or a content-hash column (`checksum`, `previous_checksum`, or the history table's `change_note`) without the family's refusal ever seeing them, and both ran before this release: + + - a cross-field comparand naming one of those columns — `{ "name": { "$ne": { "$field": "metadata" } } }`, in `where` or in an aggregation's `filter`, under `$not` included. The SQL drivers evaluate it row by row, so row presence disclosed the column's value; + - a filter on one of those columns nested more than 32 combinators deep, which the door's field collector stopped reading at. A body `$contains` of a stored credential answered the row and a wrong guess answered none. + + Both now answer the door's `400 INVALID_FIELD`, naming the column, before the query runs — the answer the same filter already gets when it names the column directly. The route: filter those tables by their scalar columns (the type, the name, the state and the like), compare scalar columns with each other, and read the bodies with a plain list, which is served projected. Every other column of the two tables, and every other object, is unchanged; a dotted key into one of those columns was, and stays, refused by the door's dotted-path rule. It ships as `minor` under the launch-window convention for accept-set narrowings. + + - **`@objectstack/metadata-protocol`** exports two module functions the generic data door now calls itself: + - `collectStoredMetadataFilterFields(object, query)` — the family's one filter-field collector: every column a read query's filters read (`where`, the engine's `filter` alias and each aggregation filter): each key's head and each cross-field `{ $field }` comparand, at any depth. `[]` outside the family. + - `narrowStoredMetadataSearch(object, query, schema, wireSpelling?)` — the family's one default-search narrowing: an explicit search-field list naming the body or a hash column is refused, a default search is narrowed to the searchable set without them (returned for the caller to run as `searchFields`), and a set that narrows to nothing is refused. The `StoredMetadataSearchSchema` type it reads is exported beside it. + - **`@objectstack/runtime`**: the stored-metadata reader-context seam (`ctx.api.object(...)` for action and hook bodies, a handler's `ctx.api`, and `ctx.engine.find`) calls those two functions instead of its own copy of the narrowing and `@objectstack/plugin-security`'s condition walk, so the seam and the door answer every family filter and search identically. A `count` through the seam now runs the query the guard returns. The seam's accept set is unchanged: every shape it refused before it still refuses, now through the door's collector. +- e367002: The runtime save door refuses a view container saved under a name its own expansion produces + + Clause-②: yes (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's `name` refusals shipped with. + + **What was accepted before.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, accepted an aggregated view container (`list` / `form` / `listViews` / `formViews`) saved under one of the names its own expansion produces: for example `{ name: 'crm_lead.default', object: 'crm_lead', list: { … } }` saved as `crm_lead.default`, the name its bare `list` expands to. That row is the name's own stored row, and an expansion fills only names that have no row of their own (the object door adopts that rule in this same release), so the container's expansion never filled it. The object door (`GET /api/v1/meta/view?object=…`), which never lists a container, listed nothing under the name, and the by-name read answered the raw container. No door answered a view item for the name, and nothing said why. + + **What is refused now.** That save, with `VALIDATION_ERROR` / 400, before anything is stored or registered, in draft and in publish mode. Whether a name is one the container's own expansion produces is decided by the same expansion the read doors run, so every member kind (a bare or named `list`, `listViews`, `form`, `formViews`) and the expander's de-duplicated names (`…_2`) are judged where the readers place them. A container with no `name` is judged under the save name the door stamps on it. A container on another package's object expands under its own name, which is never the name it is saved under, so it is not refused. + + **What still saves.** A container under its object's name, which expands as before. A view item (a body carrying `viewKind`) under an expanded name, the sanctioned override for that name. The read doors are unchanged. A row stored in this shape before this change keeps its bytes and is served as before; `migrate meta --stored` and package duplication, which re-save stored rows through this door, now report such a row as failed with this refusal instead of re-saving it. + + **The fix.** Save the container under its object's name (`crm_lead`), or save a view item (`name`, `object`, `viewKind`, `config`) under the expanded name (`crm_lead.default`). +- 7b07749: The runtime save door refuses a view container saved under a name another stored container of the same object expands to + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier name refusals shipped with. + + **What was accepted before.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, accepted an aggregated view container (`list` / `form` / `listViews` / `formViews`) saved under a name that another stored container of the same object expands to. For example, with `{ name: 'crm_lead', object: 'crm_lead', list: { … }, listViews: { pipeline: { … } } }` stored, a second container `{ object: 'crm_lead', list: { … } }` saved as `crm_lead.pipeline`. The second container became that name's own stored row, and an expansion fills only a name with no row of its own, so the first container's `crm_lead.pipeline` view was no longer served: the object door (`GET /api/v1/meta/view?object=…`), which never lists a container, listed nothing under the name, and the by-name read answered the raw second container. Nothing said why. + + **What is refused now.** That save, with `VALIDATION_ERROR` / 400, before anything is stored or registered, in draft and in publish mode. The other containers are the stored rows the read doors select for the same caller (environment-wide rows plus the caller's organization's), each expanded exactly as the read doors expand it, so every member kind (a bare or named `list`, `listViews`, `form`, `formViews`), the expander's de-duplicated names, and the names a container on another package's object expands under its own name are all judged where the readers place them. A container with no `name` is judged under the save name the door stamps on it. + + **What still saves.** A container under its object's name, which expands as before, and its own re-save. A container under any other name of its own that no other stored container of its object expands to: this door keeps a container saved under a name other than its object, and this change leaves that alone. A view item (a body carrying `viewKind`) under an expanded name, the sanctioned override for that name. The read doors are unchanged. A row stored in this shape before this change keeps its bytes and is served as before; `migrate meta --stored` and package duplication, which re-save stored rows through this door, report such a row as failed with this refusal instead of re-saving it. + + **The fix.** Add the view as a member of the stored container that already expands the name (in the example, the container `crm_lead`, whose `listViews.pipeline` is that view), or save a view item (`name`, `object`, `viewKind`, `config`) under the expanded name (`crm_lead.pipeline`). +- eea82af: The runtime save door refuses a view container whose save name, or any name its expansion produces, is a name already served from elsewhere; a package-less container row named after a view item a package ships belongs to no package + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier container-name refusals shipped with. + + **One rule.** `saveMetaItem`, which `PUT /api/v1/meta/view/:name` and the dispatcher's metadata save both call, now refuses an aggregated view container (`list` / `form` / `listViews` / `formViews`) when its save name, or any name its expansion produces, is already served from elsewhere: by another stored container's expansion in the caller's selection (environment-wide rows plus the caller's organization's), whatever that container's object, or by a view item (a body carrying `viewKind`) a package ships. A name the container's own expansion produces is refused as its save name too. Every name is judged where the read doors place it, so every member kind, the expander's de-duplicated names and a container on another package's object (which expands under its own name) are all covered. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or registered, and it names the other owner: the stored container, the shipping package, or the container's own expansion. + + **Before and after, per shape** (with `{ name: 'crm_lead', object: 'crm_lead', list, listViews: { pipeline } }` stored where a sibling is named): + + - A container bound to **another object**, saved under a sibling's expanded name (`{ object: 'crm_account', list }` as `crm_lead.pipeline`). Before: accepted; the sibling's `crm_lead.pipeline` view was no longer served on either door, and the by-name read answered the raw container. After: refused, naming the container `crm_lead`. + - An **unbound** container (`{ list }`) under the same name. Before and after: as above. + - A **second container of one object** whose bare `list` takes `crm_lead.default`, under a free name (`{ object: 'crm_lead', list }` as `lead_other_views`), or the object-named container saved after such a one. Before: accepted; whichever container was read last replaced the other's default on both doors, and nothing said why. After: refused, naming the stored container that already serves the name. + - A container saved under the name of a **view item a package ships** (`{ name: 'showcase_task.in_progress', object: 'showcase_task', list }` as `showcase_task.in_progress`), package-less, organization-scoped or in a writable package. Before: accepted; the packaged view was no longer served on the object door and the by-name read answered the raw container. Package-less, the row was also judged a container of the shipping package, so its bare `list` replaced the packaged `showcase_task.default` on both doors, wearing that package's `_packageId`. After: refused, naming the shipping package. + - An overlay of a package's own container whose new member takes the name of a view item **the package ships on its own**. Before: accepted; the member replaced that packaged view on both doors. After: refused, naming the package. + - A container under a name its own expansion produces, or under a name another stored container of the same object expands. Refused before and after, with the same envelope. The own-expansion refusal no longer tells the author to save the container under its object's name when a stored container already holds that name; it names that container to add the view to. + + **What still saves.** A view item under any of these names: it is that name's sanctioned override. A container under its object's name, or under any other name of its own, whose expansion takes no name served elsewhere, and its own re-save. An overlay of a package's own container under that container's name. A container on another package's object, which expands under its own name. A container whose would-be sibling is in another organization: the caller's own selection decides, as it does for the read doors. + + **Rows stored before this change.** They keep their bytes and are served as before, with one change: a package-less container row stored under the name of a view item a package ships now belongs to no package. On that package's object it expands under its own name (`showcase_task.showcase_task.in_progress` for a bare `list`), with no `_packageId` and no default, and the packaged views it used to replace are served again on both doors. The row itself still takes its own name's slot, as any stored row does. A new save of a row in a refused shape, a re-save included, is refused until its body stops colliding; `migrate meta --stored` and package duplication report such a row as failed with this refusal instead of re-saving it. Delete stays open. + + **The fix.** Add the view as a member of the stored container that already serves the name (its `list`, `listViews`, `form` or `formViews`), or save a view item (`name`, `object`, `viewKind`, `config`) under that name to override it. For a name a package ships: save a view item under it to override the packaged view, or save the container under a name of its own that no package ships and no stored container expands. +- ced217c: The runtime save door refuses a hook whose `handler` names a function and that carries no `body`: a hook stored there ships with no code package, so that name can never bind + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier refusals shipped with. + + **One rule.** `saveMetaItem`, which `PUT /api/v1/meta/hook/:name` and the dispatcher's metadata save both call, now refuses a `hook` whose `handler` is a function name and that carries no `body`. A hook stored through this door ships with no code package, so it holds no functions, and a `handler` name resolves only inside the hook's own package. Before this change the door answered 200, the runtime then refused the hook at bind (`INVALID_REFERENCE` / 400, in the server log only), and the hook never ran. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or bound. It names the hook and the function, and prescribes a `body`. + + **Before and after** (with `{ name: 'stamp_status', object: 'crm_note', events: ['beforeInsert'], handler: 'x_stamp' }`): + + - Before: 200 `Saved hook 'stamp_status'`, the row stored, the hook refused at bind and never run, and nothing on the response said so. + - After: 400 `VALIDATION_ERROR`, naming `stamp_status` and `x_stamp`, and nothing stored. + + **What still saves.** A hook with a `body`. A hook carrying both a `body` and a `handler`: the binder runs the body and never consults the name, and the install-local door accepts the same shape. A malformed `body` still gets the type schema's located `422 INVALID_METADATA`. + + **What is unchanged.** `HookSchema` still accepts the string `handler`, because a build artifact carries it: `objectstack build` lowers an inline function to the hook's name and ships the function in the artifact's runtime module. A hook in an artifact or a `defineStack` config binds to its own package's functions on its own door, which never reaches this one. `os validate` and `os build` are unchanged. + + **Rows stored before this change.** They keep their bytes, nothing re-saves them, and the runtime refuses them at bind as before. A new save of one, a re-save included, is refused until it carries a `body`. Package duplication reports such a row as failed with this refusal; `migrate meta --stored` leaves it as it is. Delete stays open. + + **The fix.** Give the hook a `body`: sandboxed JS (`{ language: 'js', source }`) or an expression (`{ language: 'expression', source }`). A hook that must run a package's own function belongs in that package's code, where its `handler` resolves. +- 7fd2c34: The runtime save door refuses every hook that carries no `body`, including one with neither a `body` nor a `handler`: a hook stored there ships with no code package, so its `body` is the only code it can run + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing at the runtime save door, shipped as `minor` under the repo's launch-window convention for breaking changes, the grade the same door's earlier refusals shipped with. + + **One rule.** `saveMetaItem`, which `PUT /api/v1/meta/hook/:name` and the dispatcher's metadata save both call, now refuses every `hook` that carries no `body`. It already refused a hook whose `handler` names a function and that carries no `body`; that refusal is now one case of this rule, with the same envelope and the same message. The new case is a hook with neither field (or with an empty `handler`). Before this change the door answered 200 for it, the hook was served by name, the runtime skipped it at every re-sync (`skipping hook with unresolved handler`, in the server log only), and it never ran. The refusal is `VALIDATION_ERROR` / 400, in draft and in publish mode, before anything is stored or bound. It names the hook and prescribes a `body`. + + **Before and after** (with `{ name: 'stamp_status', object: 'crm_note', events: ['beforeInsert'] }`): + + - Before: 200 `Saved hook 'stamp_status'`, the row stored and served by name, and the hook never run. + - After: 400 `VALIDATION_ERROR`, naming `stamp_status`, and nothing stored. + + **What still saves.** A hook with a `body`, with or without a `handler` beside it: the binder runs the body and never consults the name. A malformed `body` still gets the type schema's located `422 INVALID_METADATA`. + + **What is unchanged.** `HookSchema` still accepts a hook with no `body`, because a build artifact carries a `handler` hook: `objectstack build` lowers an inline function to the hook's name and ships the function in the artifact's runtime module. A hook in an artifact or a `defineStack` config binds on its own door, which never reaches this one. `os validate` and `os build` are unchanged. + + **Rows stored before this change.** They keep their bytes, nothing re-saves them, and the runtime skips them at every re-sync as before. A new save of one, a re-save included, is refused until it carries a `body`. Package duplication reports such a row as failed with this refusal; `migrate meta --stored` leaves it as it is. Delete stays open. + + **The fix.** Give the hook a `body`: sandboxed JS (`{ language: 'js', source }`) or an expression (`{ language: 'expression', source }`). A hook that must run a package's own function belongs in that package's code, where its `handler` resolves. +- c43a8ae: fix(metadata-protocol)!: the ADR-0010 `_lock` gate refuses on a host-config kernel too, and the diagnostics `locked` count reads the item envelope's derivation (#21694) + + Clause-②: no (narrowing) + + + + **BREAKING**: a `/meta` write that a host-config kernel used to accept can now be refused. A host-config kernel is one with no `environmentId`: the CLI's lightweight assembler boots one for a stack whose `plugins` are instantiated, which is how the showcase app runs. On such a kernel the item-level `_lock` gate never ran, so `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` performed writes on items whose read envelope said `editable: false` or `deletable: false`. The gate now runs on every kernel, and answers the same `403 ITEM_LOCKED` an environment-bound kernel always gave. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. + + **What is refused now, on a host-config kernel.** A save, publish or rollback of an item whose effective `_lock` is `no-overlay` or `full`, and a delete of an item whose effective `_lock` is `no-delete` or `full`. The effective `_lock` is the packaged artifact's when one declares it, otherwise the stored row's. Each refusal writes its `denied` row to `sys_metadata_audit`, as on an environment kernel. The gate's own `sys_metadata` read fails closed there too: when it fails for any reason other than the table not being provisioned yet, the write is answered `503 SERVICE_UNAVAILABLE` before anything is written, where a delete used to reach the store and answer with the driver's code or a `500`. One shipped case reaches it: the platform's `setup`, `studio` and `account` apps declare `protection.lock: 'full'`, and a `DELETE /api/v1/meta/app/setup` used to pass the package door (removing a legacy app overlay is allowed) and then remove the app's overlay row, or answer success with nothing to remove. It now answers `403 ITEM_LOCKED`. The refusal names the lock and where it came from (`source=artifact` or `source=overlay`). If a host-config deployment relied on writing such an item: a lock a code package declares is changed in that package's source (`protection.lock`) and redeployed; a lock a stored row declares is held exactly as an environment-bound kernel holds it, so a row declaring `no-overlay` can still be deleted and saved again, and a row declaring `full` is no longer writable or removable through `/meta` on any kernel. + + **Unchanged.** Which code a packaged base answers: the `_lock` gate still ranks below the package door on both kernels, so a packaged item on a type with no overlay channel keeps `NOT_OVERRIDABLE` (or `ITEM_LOCKED` when the write names the read-only package) on a host-config kernel too. Every refusal, receipt and envelope on an environment-bound kernel. Both reads (`getMetaItem`, `getMetaItemLayered`), which already reported the declared `_lock` on every kernel. + + **The diagnostics count.** `getMetaDiagnostics().stats[type].locked` (the Studio directory's per-type tile) counted items with a declared `_lock`. It now counts items whose read envelope reports a lock other than `'none'`, from the same derivation the item read publishes. So an item that the package door refuses in place, such as a packaged flow or action with no `_lock` of its own, is counted, as its envelope has read locked since the previous release. The derivation reads the registry only, so the sweep makes no extra store read per item. +- cf60dbc: fix(metadata-protocol)!: the ADR-0010 `_lock` gate reads the row the read serves for the request's organization, so an env-wide row's lock binds an organization with no row of its own (#21716) + + Clause-②: no (narrowing) + + + + **BREAKING**: an organization-scoped `/meta` write that used to be accepted can now be refused. The item reads (`getMetaItem`, `getMetaItemLayered`) serve an organization its own stored row, and the env-wide row when it has none (ADR-0005). The item-level `_lock` gate read only the organization's own row. So when the env-wide row declared a lock, an organization with no row of its own read `lock: "full"` and `editable: false`, while its `saveMetaItem`, `publishMetaItem`, `rollbackMetaItem` and `deleteMetaItem` were admitted. The gate now reads the row the reads serve, through the same resolution, and answers `403 ITEM_LOCKED` where the envelope says the write is not allowed. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. + + **What is refused now.** On every kernel topology, a save, publish or rollback with an organization of an item whose env-wide stored row declares `_lock: "no-overlay"` or `"full"`, and a delete with an organization of one whose env-wide row declares `"no-delete"` or `"full"`, when that organization has no stored row of its own for the item. Each refusal writes its `denied` row to `sys_metadata_audit` under the requesting organization. Over the wire this reaches the five per-org overridable types (`view`, `dashboard`, `report`, `translation`, `email_template`): the REST and dispatcher write doors already send no organization for any other type. For those other types the reads never serve an organization-scoped row, and now neither does the gate: an in-process removal of a pre-#6190 organization-scoped row of such a type (`deletePackage`, `discardPackageDrafts`) is judged by the env-wide row's lock, the row both reads serve. If a deployment relied on writing such an item per organization: change or remove the env-wide row's lock (a `no-delete` row can still be saved, a `no-overlay` row deleted), or keep the lock and author the organization's variant under a new name. + + **Unchanged.** When the organization has a stored row of its own, that row is the one both reads serve, and its `_lock` decides, whatever the env-wide row declares (ADR-0005 precedence, never a merge). A request with no organization. The packaged artifact's lock, which still wins when it declares one. Both reads, apart from one case in `getMetaItemLayered`: it now serves an organization's own stored row whose body is JSON `null`, as `getMetaItem` already did, instead of falling back to the env-wide row, because the two reads now share one row resolution. Only residue can reach it: no live writer stores a `null` body (measured: `SysMetadataRepository.put`, the writer behind every `/meta` save, stores `{}` for an absent body; `saveMetaItem` refuses a `null` item with `400 INVALID_REQUEST`; and the only other `sys_metadata` writer, the datasource admin plugin, stores an object env-wide). The gate addresses the canonical type spelling only; the reads' last-resort read of a row stored under the type's other spelling is not extended to the write path. +- 18c2ddc: fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address, at the write doors and on both reads (#21761) + + Clause-②: no (narrowing) + + ADR-0048 lets one item (type, name, organization scope) hold several stored `sys_metadata` rows: one per package (`?package=` saves) and a package-less one. The ADR-0010 `_lock` gate asked for the item with no package and bound whichever row the store returned first, while `getMetaItem` and `getMetaItemLayered` naming a package reported that package's row. So a read and the door could state two different locks for one item, and the door's answer depended on row order. + + Now every caller selects the lock from the item's address through one function: the strictest lock among the item's stored rows in scope. The scope is ADR-0005's (the organization's rows when it holds any row of the item, else the env-wide rows), and within it every row of the item counts, whichever package it is bound to. The strictest lock refuses a write when any of those rows refuses it, and a delete likewise; two rows that refuse different verbs (`no-overlay` and `no-delete`) give `full`. Both reads report that lock in `lock`, `editable` and `deletable`, the served body carries its `_lock` family, and the list item and the `getMetaDiagnostics` locked count follow it. Content stays prefer-local: a read naming a package is still served that package's own row. + + **What moves for consumers.** The door now refuses where it used to depend on which row the store returned first: + + - a package-less row declaring no lock and a package's row declaring `full`: a save or delete, with or without `?package=`, is refused `403 ITEM_LOCKED` in every row order, where it was admitted when the package-less row came back first; + - a package's row declaring `none` and a package-less row declaring `full`: refused in every row order, where it was admitted when the package's row came back first. A package row's explicit `none` is not a grant over another row's lock; + - two rows refusing different verbs (`no-overlay`, `no-delete`): both a save and a delete are refused, where each was admitted under the row order that bound the other row; + - another package's row of the same name declaring a lock binds a save naming this package too, as it did under the row order that returned it first. + + No write the door refused before is admitted now. Both reads become stricter in exactly those arrangements: a read naming a package whose own row declares `none` now reports `editable: false` when another row in scope declares `full`. No key, export, status or error code changes. + + +- 18fe681: fix(metadata-protocol)!: an item's lock is the strictest among the installed packages that ship its name, at the write doors and on both reads (#21803) + + Clause-②: no (narrowing) + + ADR-0048 lets two installed code packages ship one `(type, name)`. The ADR-0010 `_lock` gate looked the packaged artifact up with no package, so it bound the artifact of whichever package was registered first, while `getMetaItem`, `getMetaItemLayered`, the metadata list and the `getMetaDiagnostics` locked count looked it up with the request's package. One item had two lock answers, and the door's answer depended on registration order. + + Now every caller takes the artifact layer from one selection: the artifact of every installed package that ships the name, a disabled package included (it is still installed). The lock is resolved once per shipping package, that package's artifact over the stored rows in scope exactly as before, and the item's lock is the strictest of those answers. With one package shipping the name, or none, nothing changes. + + **What moves for consumers.** The door now refuses where it used to depend on registration order: + + - one package ships a lock and another ships none: a save or delete, with or without `?package=`, is refused `403 ITEM_LOCKED` under both registration orders, where it was admitted when the unlocked package was registered first; + - one package ships no lock, the stored row in scope declares a lock, and another package ships a lock refusing the other verb (for example `no-overlay` on the row and `no-delete` on the artifact): both a save and a delete are refused, where each was admitted under the registration order that bound the other answer; + - a disabled package's packaged lock binds under both registration orders, where it bound only when that package was registered first. + + No write the door refused before is admitted now: the artifact the door bound before is always one of the shipping packages. Both reads report the same lock as the door in `lock`, `editable` and `deletable`, under both orders: a read naming a package that ships no lock now reports `editable: false` when another installed package ships one. The refusal and the reads carry the prose of the binding package (the request's own package first, then the others by package id), and no prose when no single package's answer is the strictest. Content stays prefer-local: a read naming a package is still served that package's own artifact, under that package's provenance. + + A served body (`getMetaItem`'s `item`, `getMetaItemLayered`'s `effective`, the list items) now carries exactly the lock family of the resolution's answer, and no `_lock` key when nothing binds. Before, a body served from a stored row outside the lock's scope kept that row's `_lock` while the envelope reported `none` (an organization holding only another package's row, with the request's package served its env-wide row), and an explicit `_lock: 'none'` stayed on a body. No key, export, status or error code changes. + + +- e864db5: fix(service-datasource): a destructive re-import's refusal names the remedies that work from the import route, instead of a `?force=true` that route never reads (#21841) + + Clause-②: yes (widening) + + - **What was wrong.** "Import as Object" (`POST /api/v1/datasources/:name/external/tables/:remote/import`) saves through the metadata door's own `saveMetaItem`. A re-import that would drop or retype a field the stored object still carries is refused by that save's destructive-change gate, and the import route relays the refusal as `400 EXTERNAL_IMPORT_ERROR`. The refusal ended `re-submit with ?force=true to proceed.` The import route reads no `force`, so a caller who did exactly that got the identical refusal back. + - **What the refusal says now.** The import states its own write face, and the refusal ends: this import cannot be forced, because the external-table import route accepts no `force`. Import the table under a new `name`, or save the changed definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Both remedies are measured on the showcase: each one answers `201` or `200` where the re-import answered `400`. The same words appear in this package's earlier changeset for the import. + - **What widens.** `SaveMetaItemRequestSchema.writeFace` (`@objectstack/spec`) and `saveMetaItem`'s `writeFace` parameter (`@objectstack/metadata-protocol`) gain one member, `'external-import'`. The member is stated by the server. No door reads it from a request body, and the import's own options cannot carry it, or a `force`, into the save. Nothing accepted today is refused. + - **What does not change.** The refusal itself stays: a destructive re-import is still `400 EXTERNAL_IMPORT_ERROR`, and the stored definition does not move. The import route gains no `force`. Acknowledging a destructive change stays on the metadata door. The other faces' wording is unchanged. A `422 INVALID_METADATA` relayed by the import keeps its full findings in the message, because the import route's envelope carries no `issues`. +- 9cc2c79: fix(metadata-protocol)!: the metadata door refuses an edit of a code-defined datasource, and removes only a stored row left under one (#21899) + + Clause-②: no (narrowing) + + A datasource an installed package declares in `*.datasource.ts` is code-defined: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin door already refused to edit or remove one. The metadata door did not. The runtime registers a code-defined datasource in memory only, never as a registry item, so the door's artifact check missed it and the write took the runtime-create tier: `PUT /api/v1/meta/datasource/:name` answered 200, persisted a row, and the metadata read then served that row in place of the code definition. + + The door's artifact check now reads the datasources the installed packages declare, so the package door that refuses every other code-shipped item of a type with no overlay channel refuses this one too. + + **BREAKING — what moves for consumers.** + + - `PUT /api/v1/meta/datasource/:name` on a code-defined datasource answered 200 and now answers `403 NOT_OVERRIDABLE`: "Datasource ':name' is code-defined and cannot be edited at runtime: it is read-only. Edit the *.datasource.ts source that declares it and redeploy." + - `DELETE /api/v1/meta/datasource/:name` on a code-defined datasource with no stored row answered 200 ("nothing to delete") and now answers the same `403 NOT_OVERRIDABLE`, saying "cannot be removed at runtime". + - The admin door keeps its own `400 DATASOURCE_ADMIN_ERROR`. The two doors' codes differ; the verdict and the remedy are the same. + - The metadata read envelope reports the same answers: `editable: false`, and `deletable` true only while a stored row exists under the name. + + **Remedy.** + + - To change a code-defined datasource, edit the `*.datasource.ts` source that declares it and redeploy. + - A row an earlier `PUT` stored under a code-defined datasource's name is still removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it, once per name. After the next restart both doors serve the code definition again. Until that restart the datasource-admin service keeps the stored copy it restored at boot (tracked in #21922). + + **Unchanged.** A runtime datasource, one no package declares, saves and deletes through the metadata door as before. `OS_METADATA_WRITABLE=datasource` opens the lock exactly as it did. The host's `default` datasource is declared by no package, so the metadata door still accepts edits to it as before; the admin door refuses them. + + +- 753e7a1: fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host's `default` (#21922, #21944) + + Clause-②: no (narrowing) + + A code-defined datasource (one the installed artifact declares in `*.datasource.ts`, or the host's own `default`) is read-only: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin service states "code wins on collision". The boot restore broke both. It registered every stored `datasource` row in `sys_metadata` over whatever the runtime had registered from code, so after a restart a row left under a code-defined name was served by the admin door, editable there when it carried `origin: 'runtime'`, and handed to pool rehydration. A stored `default` row opened a second live pool named `default` on the row's own connection. The metadata door also still saved edits to `default`, the one code-defined datasource no package declares. + + The runtime now keeps one in-memory set of the datasource names it registers from code, on the kernel service `code-datasource-names`: `AppPlugin` adds the datasources the artifact declares and `DefaultDatasourcePlugin` adds `default`, both in `init()`, so the set is complete before any `start()` runs. The boot restore skips a stored row under a name in that set, and the metadata door's code-datasource check reads the same set. + + **BREAKING — what moves for consumers.** + + - After a restart over a stored row under a code-defined datasource's name, `GET /api/v1/datasources` serves the code definition (`origin: code`) instead of the row, and `PATCH /api/v1/datasources/:name` answers `400 DATASOURCE_ADMIN_ERROR` ("… is code-defined and cannot be edited at runtime.") where it answered 200 for a row that carried `origin: 'runtime'`. + - No live pool is opened from such a row at boot. + - `PUT /api/v1/meta/datasource/default` answered 200 and now answers `403 NOT_OVERRIDABLE`. `DELETE /api/v1/meta/datasource/default` with no stored row answered 200 and now answers the same `403`. The refusal's remedy names the host's database configuration (the database URL the server starts with), which is what defines `default`; every other code-defined datasource's refusal still names its `*.datasource.ts` source. + - The skipped row is kept, and the boot logs one warning naming it. + + **Remedy.** + + - To change a code-defined datasource, change its code definition and redeploy: its `*.datasource.ts` source, or the host's database configuration for `default`. + - A row the boot warning names is removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it. + + **Unchanged.** A runtime datasource with no code twin restores, saves and deletes through both doors as before. A host that composes neither `AppPlugin` nor `DefaultDatasourcePlugin` registers no set, and its stored rows restore as before. While a stored row exists under a code-defined name, `GET /api/v1/meta/datasource/:name` still serves that row (the metadata door reads its stored overlay first); after the `DELETE` above it serves the code definition, in the same boot. + + +- c9761cd: A metadata publish promotes only the draft its gate judged + + Clause-②: yes (widening) + + - A publish (`publishMetaItem`, and each promotion of `publishPackageDrafts`) reads the draft to judge it and then promotes the draft row. The promotion is now handed the judged draft's hash. A draft saved after the judgement, or a draft that appears where the judgement found none, is refused with `409 METADATA_CONFLICT`, and nothing is published. Publishing again judges and promotes the current draft. + - `SysMetadataRepository.promoteDraft` takes a new optional `expectedDraftHash` (`string | null`). When it is stated, the draft row the promotion reads must carry that hash (with `null`, no draft row may exist); otherwise the promotion throws a `ConflictError` before anything is written. When it is omitted, the promotion behaves as before. + +### Patch Changes + +- bdd3654: `computeViewReferenceDiagnostics` no longer walks a list view's own `tabs[].filter` + + Clause-②: no + + The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape. The write door refuses it, and a stored or artifact-shipped body has it stripped by the conversion replay before it is served, so the read could never see it. A served body that still carries it is already badged by the spec diagnostics (`computeMetadataDiagnostics`), with the tombstone's prescription. The `userFilters.tabs[].filter`, `filterableFields` and `kanban` checks are unchanged. +- 0e10be6: fix: on MySQL, `sys_packages` is now created and written, so installed and edited packages survive a restart. When a `sys_packages` write fails, a package install or edit now answers the failure instead of success (#21243) + + Clause-②: no + + **`@objectstack/service-package`.** The `sys_packages` DDL and the publish upsert are spelled for the dialect the default driver names (`SqlDriver.dialectName`). SQLite and PostgreSQL keep the exact statements they always ran, and so does any driver that names no SQL dialect. MySQL gets the same `(id, version)` key and columns in its own spelling. Its index is created only after `information_schema` reports it absent, and its upsert is `INSERT … AS incoming ON DUPLICATE KEY UPDATE`, which needs MySQL 8.0.19 or later. Before this, the table was never created on MySQL. That DDL failed with `ER_INVALID_DEFAULT`, `ER_BLOB_KEY_WITHOUT_LENGTH` and `ER_PARSE_ERROR`. The DDL refusal was logged only at `debug`, as "may already exist". The `ON CONFLICT` upsert also failed with `ER_PARSE_ERROR`, so `POST /api/v1/packages/publish` answered `500 DATABASE_ERROR`. A refused DDL statement now fails the plugin's `start()` and is logged at `error`. + + **`@objectstack/metadata-protocol`.** `installPackage` and `updatePackage` no longer answer success when the `package` service's `sys_packages` write fails. The registry write is undone first. A fresh install leaves no package and releases the namespace it registered. A re-install puts the prior row back, and an edit puts the prior manifest back. Then the failure is thrown. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, `POST /api/v1/packages` answered `201` and `PATCH /api/v1/packages/:id` answered `200` over a write that never landed, and the package was gone after the next restart. A host with no `package` service still installs in memory only and says so with a warning. That degraded path is unchanged. +- 1fd5664: fix: when the store refuses an uninstall's `sys_packages` delete, the uninstall now answers the failure and removes nothing else, instead of answering success and coming back after the next restart (#21276) + + Clause-②: no + + **`@objectstack/metadata-protocol`.** `deletePackage` now deletes the package's `sys_packages` row first, before its `sys_metadata` rows, its tables, its registry entry and the rows the uninstall cleanups own. When the `package` service refuses that delete, whether it returns `{ success: false }` or throws, `deletePackage` throws and nothing else is removed. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, the refusal was logged as a warning, and `DELETE /api/v1/packages/:id` answered `200` after the package's metadata, tables and grants had been removed. The package then came back after the next restart. + + Before that store delete, `deletePackage` now also asks the registry whether the uninstall would be refused because another package extends an object this package owns (ADR-0029). If so, it throws the registry's own refusal with nothing removed. A registry without the new question is not asked, and the refusal then surfaces at the registry withdrawal, as before. + + **`@objectstack/objectql`.** New: `SchemaRegistry.assertPackageUninstallable(packageId)`. It throws the refusal `unregisterObjectsByPackage` and `uninstallPackage` raise for an object another package extends, with the same message, and it changes nothing. `unregisterObjectsByPackage` now calls it, so there is still one copy of that check. + + **`@objectstack/runtime`.** `DELETE /api/v1/packages/:id` now asks `deletePackage` before it touches anything. It checks that the package exists with a read, and it withdraws the package from the running registry and clears its saved disable record only after `deletePackage` has answered. So when the store refuses, the door answers `500`, the same process keeps serving the package, and a package that was disabled stays disabled after a restart. Before this, the door withdrew the package and cleared its disable record first. A refused delete then left the package missing until a restart, and brought a disabled package back enabled. + + An uninstall refused because another package extends an object this package owns still answers `500` with nothing changed: the stored rows, the registry entry and the disable record all stay as they were, in the same process and after a restart. That refusal is now decided before the store delete, instead of by the door withdrawing the package first. An ordinary uninstall, and a host with no `package` service, are unchanged. +- 535d1d2: fix(metadata-protocol): a view container saved for an object another package ships no longer replaces that package's views or its default + + Clause-②: no + + - **What was wrong.** A runtime view container expands each member to `.`. A `list` that names no key becomes `.default`, a `form` becomes `.form`, and every member that names a key uses that key. Saved under another name, in another package or in none, for an object a code package ships, those expansions replaced that package's views of the same names on `GET /api/v1/meta/view?object=`. The replacements were still stamped with the shipping package's `_packageId` and `_provenance: 'package'`. + - On an environment-scoped kernel, the by-name read `GET /api/v1/meta/view/` kept the packaged view, so the two reads disagreed. + - On an unscoped kernel, the by-name read served the replacement too, for a container saved into a package or environment-wide. + - The container's own default kept `isDefault: true`. It either replaced the object's default view or stood beside it as a second list default. + - **What it does now.** For an object a code package ships, a container that belongs to another package, or to none, expands every member under its own name: + - a `list` that names no key becomes `.`; + - every other member becomes `..`. That covers a `list` that names its key, each `listViews` and `formViews` entry, and `form`. + + None of these views carries `isDefault`. Every name the shipping package serves answers its packaged view on both reads, unchanged, and the only `isDefault` views the object lists are the shipping package's. + - **One exception.** When the shipping package itself serves `.` (a container named after one of that package's keys), the container's default list becomes `..` instead. + - **A container with no name of its own** expands nothing on such an object. + - **What these views carry.** The container's own package as `_packageId` (none for a package-less container), and no other package's `_provenance` or protection envelope. + - **What stays.** Three kinds of container expand exactly as before, `isDefault` included: + - a container bound to the package that ships the object; + - a package-less overlay of that package's own container, saved under that container's name; + - a container on an object no code package ships. + + A write to `.default` by its own name still overrides it on both reads. + - **What changes for a caller.** Such a container's views are now served under new names: + - its default list as `.`, instead of `.default`; + - each keyed member as `..`, instead of `.`. + + A navigation `viewName` or a form-action `target` that used an old name to reach one of these views now reaches the shipping package's view. Use the new name instead. +- e9dec3d: fix(metadata-protocol): a view a stored view container expands answers by name what the object door lists, on every kernel and for every container scope + + Clause-②: no + + - **What changed.** `GET /api/v1/meta/view?object=…` lists the views a stored view container expands, and the by-name read now answers each of those names with the same item. Before, `getMetaItem` expanded no container: it answered such a name only on an unscoped kernel and only for an environment-wide container, where the registry held a hydrated copy. On an environment-scoped kernel, and for an organization-scoped container on any kernel, it answered nothing. Where the name is one a package also ships, such as `.default` under a tenant's overlay of that package's container, it answered the packaged view while the list served the overlay's. + - **How.** The by-name read selects the stored containers in the caller's scope with the list read's own row selection, and expands them with the list read's own expansion. Nothing is persisted or registered, and a stored row of the name itself still answers first. + - **Layers, history and diff for such a name.** `getMetaItemLayered` reports the container's own stored row as `overlay`, with the scope it was read from as `overlayScope`, and the expanded view as `effective`. `historyMetaItem` and `diffMetaItem` answer exactly what they answer under the container's own name, and say so: every event's `ref.name` and the diff's `name` are the container's. No history is made up for a name that was never stored. + - **What does not change.** The container's own name still answers its stored row. The save door is unchanged, including a write by an expanded name. No response shape gains or loses a key. +- ce53218: Withdrawing or publishing a public form on a walled tenancy posture (degraded or not) is now refused loudly at authoring, with `403 NOT_OVERRIDABLE`, when the save is organization-scoped and the anonymous form doors cannot honour it. The message names the remedy: save the change env-wide, which every anonymous door honours. Drafts and draft promotion are refused alike. Other organization-scoped edits, env-wide saves and single-posture deployments are unchanged. +- 83b3d32: Public forms on a walled tenancy posture: saving or publishing a view whose public form cannot take anonymous intake now tells the author why, on the response. + + Clause-②: yes (widening) + + On a walled posture (`group` or `isolated` in force), an open public form whose object is walled by an organization column cannot take an anonymous submission: the submission carries no organization, and an insert without one into a walled object is refused. The two anonymous form endpoints already answer such a form as a withdrawn one (`404 FORM_NOT_FOUND`), and the administrator's read of the view (`GET /meta/view/:name`) already states why in `_diagnostics.warnings`. + + - **`@objectstack/metadata-protocol`**: saving the view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and a package's batch publish) now answers success with one `warning` advisory per such form, under `advisories`, with rule `public-form-intake-unavailable`. It is located at the form's `sharing` (for example `views[0].formViews.contact.sharing`), its `message` is the same text the administrator's read states, and its `hint` is the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. The write is never refused. The advisory reads the posture in force from the `tenancy` service, which is what the anonymous endpoints read: a single-posture deployment, a deployment whose walled posture is degraded to `single`, a deployment with no tenancy service, and a form bound to a tenancy-disabled object get no advisory, and a draft save is not judged. The publish refusal for an unstamped platform schedule flow still reads the requested posture, as before. + - **`@objectstack/metadata-core`**: the intake-availability rule moved here from `@objectstack/rest` and is exported, so the anonymous endpoints, the administrator's read and the publish advisory read one answer: `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` (`null` when the form can take intake, otherwise the object, the posture and the wall column; it judges the object's effective schema, with the injected `organization_id`), `anonymousFormIntakePosture(tenancy)` (the posture in force, as a tenancy service reports it), `anonymousFormIntakeUnavailableMessage` and `anonymousFormIntakeUnavailableRemedy` (the reason and its remedy), `anonymousFormSharingPath` and `anonymousFormObjectName`, and the type `AnonymousFormIntakeUnavailable`. + - **`@objectstack/rest`**: the anonymous form endpoints and the administrator's read import that rule instead of holding their own copy. Their answers are unchanged. +- 550f4cc: The metadata protocol registers its journal-backed migration plan, `metadata.recorded-by-sentinel-to-null`, with the kernel's `migration-plans` registry (#21498) + + Clause-②: no + + A migration journal records a run's plan hash, not the plan's code. To resume a run, the package that owns the plan has to register it. This package owns the `recorded_by` sentinel-to-NULL plan, and until now it never registered it. So any process that composed the registry still reported the run as unresumable. + + The protocol assembly (`assembleMetadataProtocol`, which `ObjectQLPlugin` and `MetadataProtocolPlugin` both run) now registers the plan at `kernel:ready`. It does so only when a `migration-plans` service is composed. That runs before `MigrationRecoveryPlugin`'s boot scan, so the scan reports the run as resumable. A kernel with no registry is unchanged. Registering a plan runs nothing: only `os migrate resume` acts on it. +- 5dbcee8: fix(metadata-protocol): the object door lists a stored view row under its own name even where a stored view container expands that name, as the by-name read already answers + + Clause-②: no + + - **What changed.** `GET /api/v1/meta/view?object=…` (the object door) no longer lets a stored view container's expansion replace a stored row of the same name. A view item (a row carrying `viewKind`) saved under a name the container also expands, such as `.default` beside a stored overlay of that object's container, is now what the object door lists under that name. Before, the object door listed the container's expansion there while the by-name read (`GET /api/v1/meta/view/NAME`) answered the stored row. Both doors now answer the row. + - **The rule.** A row stored under exactly a name is the override for that name (ADR-0005 keys an overlay by its own name). An expansion fills only the names that have no row of their own. The list read and the by-name read decide this with one test, over the rows each selects for the same caller, so a row stored for one organization does not hide the expansion from any other caller. + - **A container stored under one of its own expanded names.** That row is the name's own row as well, so its expansion no longer fills the name. The object door never lists a container, so it now lists nothing under that name. Before, it listed the container's expansion there. The by-name read answers the stored container, as before. + - **What does not change.** Every name a container expands that has no stored row of its own is still listed, and on both doors it still replaces a packaged view of the same name. The by-name read answers as before. The save door is unchanged. No response shape gains or loses a key. +- ec390ec: An expanded view of a stored view container is reported as tenant-authored on an unscoped kernel, as it already was on an environment-scoped one: not resettable, and with no `code` layer + + Clause-②: no + + On an unscoped (control-plane) kernel, registry hydration registers each view a stored environment-wide container expands, under that view's own name. The container was registered with the tenant-authorship marker (`_provenance: 'org'`), and its expansions were not. An expansion of a container bound to a package therefore carried that package's id and no marker, and the registry's artifact lookup took it for a view the package ships. For such a name `getMetaItem` (`GET /api/v1/meta/view/NAME`) answered `resettable: true`, and `getMetaItemLayered` (`/layers`) answered the stored container's expansion as the `code` layer. The `code` layer was also wrong for an expansion of a package-less container. An environment-scoped kernel registers nothing, and answered `resettable: false` and `code: null`. + + Each registered expansion now carries its container's marker, applied before the expansion's own artifact envelope, in the same order the container gets it. Where the container's own package ships a view of that name, that artifact's envelope still wins (ADR-0010 §3.3). Both kernels now give the same answer for every expanded name. Studio's reset affordance and its code-versus-overlay diff are drawn from these two values. + + The save door is unchanged: it accepts a write by an expanded name on both kernels, as before, and the stored row then answers that name. +- eb9ef79: The data door's object-existence gate builds its `OBJECT_NOT_FOUND` from the shared factory, and two best-effort readers treat the engine's refusal of their own object as the not-provisioned case + + Clause-②: no + + - `assertObjectRegistered` (the data door's object-existence gate) now throws `objectNotFoundError(object)` from `@objectstack/core`. The code, the status, the `object` field and the message are unchanged, byte for byte. + - `SeedLoaderService.resolveSoleOrganizationId` and the history counters `SysMetadataRepository` reads (`version`, `event_seq`) already answered a missing table of their own object as "nothing here yet". `@objectstack/objectql` now refuses an object name its registry does not hold with `OBJECT_NOT_FOUND` instead of reaching the driver, so each reader also answers that refusal as the same absence when the error's own `object` is the object it read. A refusal naming another object, and every other read failure, still propagate. With a registered object nothing changes. +- ff16740: A per-organization seed replay now gives each organization its own row identity. On a walled deployment, every organization created after the first used to start without the app's fixed-id seed rows. The showcase's `sys_business_unit` tree is one example. The replay inserted each authored `id` again, every insert was refused as a duplicate on the global primary key, and the parent references into those rows stayed unresolved. + + Clause-②: no + + - A row authored with an `id` keeps that id while no row holds it. The first organization a seed is replayed into is unchanged: it gets exactly the ids the seed authors. + - When another organization (or an organization-less row) already holds the authored id, the row gets an id derived from the authored id and the organization. A second replay into the same organization finds that row again, so it is not inserted twice. + - References in the same replay that name the authored id follow the row to its new id, in pass 1 and in pass 2. This includes a UUID-shaped authored id, which used to be kept verbatim and would have linked to another organization's row. + - The replay logs one `info` line per dataset that it re-identified. + - The rule lives in `SeedLoaderService`, so every load that names an organization follows it: the per-organization replayer, and package apply, draft publish and marketplace install into an organization. + - Boot seeding without an organization, dry runs and rows without an authored `id` are unchanged. + - ⛔ No schema, export, accepted input or error code changes. +- fe10172: The metadata reads' `lock` / `editable` / `deletable` now say what the write doors do with a packaged item + + Clause-②: no + + Both metadata reads publish the ADR-0010 protection envelope beside the item: `GET /api/v1/meta/:type/:name/layers` (and its deprecated `?layers=true` spelling), and the by-name read `GET /api/v1/meta/:type/:name` where it resolves the envelope. The envelope was resolved from the item's own `_lock` alone, so it ignored the other refusal the write doors apply: an item a code package ships, on a type with no per-org overlay channel, is locked against in-place edits. + + **Before.** A packaged flow, action, object, hook, seed, mapping, datasource, external catalog, doc, picklist, field, job, api, capability or agent with no `_lock` read `lock: 'none'`, `editable: true` and `deletable: true`. A packaged page, app, dataset, book, permission set, position, tool or skill read the same. Yet `PUT` refused each of them with `403 NOT_OVERRIDABLE` (or `403 ITEM_LOCKED` when the write names the read-only package), and the removal of the first group was refused too. + + **After.** Each read reports what its doors answer: + + - The first group reads `lock: 'full'`, `editable: false` and `deletable: false`. + - The second group reads `lock: 'no-overlay'`, `editable: false` and `deletable: true`. Removing a leftover overlay row of these types is allowed: that is the repair path for overlays written before their per-org channel was withdrawn. + - Items of the overlay types (`view`, `dashboard`, `report`, `translation`, `email_template`) are unchanged. So are items no package ships, such as an organization's own flows and actions, and every item while the `OS_METADATA_WRITABLE` operator hatch opens its type. + + An item's own `_lock` still applies on top: the two refusals join, and neither replaces the other. `lockReason`, `lockSource` and `lockDocsUrl` are still present only when the item declares them. `provenance` and `packageId` already name the package. + + The verdict is the one the write doors already share, read rather than re-derived, so the read moves whenever a door moves. The `lock` field's description in `@objectstack/spec` now names both refusals it reports. No key, type or accepted value changes. + + **What to do.** Nothing, unless a client gated an edit or delete affordance on `editable` / `deletable`: it now hides that affordance for packaged items the server refuses, instead of offering a write that answers 403. The refusal itself names the sanctioned route for each type: for a packaged flow, clone it under a new name or switch it off; for a packaged action, switch it off. +- b7a13c7: fix(metadata-protocol): the item reads take an item's ADR-0010 `_lock` from the same resolution the write doors enforce, so the read envelope and the doors agree on the artifact layer too (#21738) + + Clause-②: no + + The `_lock` gate of the write doors (save, publish, rollback, delete) resolves an item's lock from two layers in order: the packaged artifact's `_lock`, unless it is `'none'`, then the stored `sys_metadata` row's. The two item reads did not use that resolution. `getMetaItem` took the lock from its served document, onto which `mergeArtifactProtection` had copied any declared artifact `_lock`, `'none'` included. `getMetaItemLayered` (`GET /api/v1/meta/:type/:name/layers`) took it from the code layer whenever one existed. Now the gate, both reads' envelopes, the served body's lock fields and the `getMetaDiagnostics` per-type `locked` count all call one resolution, `resolveItemLock`. + + **Read answers that change**, on both kernel topologies: + + - An artifact that declares `_lock: 'none'` over a stored row that declares a lock (env-wide, or the organization's own row) now reads with the stored row's lock in `getMetaItem` and `getMetaItemLayered`. Under `'full'`, that is `editable: false` and `deletable: false`. The served body's `_lock`, the `getMetaItems` list item's `_lock` and the diagnostics `locked` count say the same. The doors already refused these writes with `403 ITEM_LOCKED`. An artifact's `'none'` declares no lock; it does not override an administrator's stored lock. + - `getMetaItemLayered` for a packaged item whose artifact declares no `_lock`, under a stored row that declares one, now reads the stored row's lock, as `getMetaItem` and the doors already did. Before, it read `lock: 'none'`, `editable: true`. + - `lockReason`, `lockSource` and `lockDocsUrl` are the binding layer's. When no layer binds, they are absent: a reason explains a refusal, and there is none. Before, they were whatever the served document carried, so an explicit `'none'` artifact's reason was reported. For the same reason, an explicit `'none'` artifact's `_lock`, `_lockReason`, `_lockSource` and `_lockDocsUrl` are no longer copied onto a stored row's served body. + - A `_lock` that only a copy the doors never read declares is no longer reported as binding. Examples are a MetadataService copy the dev watcher reloaded after boot, and an item registered at runtime with no package. Such an item now reads as the doors answer it. + + **Unchanged.** Every write-door verdict, refusal code and refusal text: the gate still reads the stored row only when the artifact's lock does not bind, so a packaged lock is still answered without a store read. `provenance`, `packageId` and `packageVersion` on both reads, and the artifact's `_packageId`, `_packageVersion` and `_provenance` on served bodies. Which stored row each read serves. The one declared difference between the reads and the gate also stays: the reads still serve a row stored under the type's other (plural) spelling when no canonical row is in scope, and the gate does not read it. No export, accepted input, key or error code changes. +- e1790fd: Publishing a pure field reorder of an object now saves it. The object designer's drag-to-reorder used to answer success on publish, keep the old order and delete the draft (#21790). + + Clause-②: yes + + - `SysMetadataRepository` hashes each body as its type, so a reorder of an object's `fields` is a content change. It is written, recorded in history and served by `GET /api/v1/meta/object/:name`. + - **Rows stored before this release** keep the `checksum` they were written with. That value is still the version token: reads return it, `If-Match` tokens are derived from it, and the optimistic lock compares against it, so upgrading raises no conflict. + - For an object, "is this save a no-op?" is decided by hashing the stored body under the current rule, not by comparing against the stored checksum. An identical re-save of an old row writes no history row and keeps its checksum. A reorder into sorted key order is written too. Its new hash equals the old order-blind checksum, so a checksum comparison would have dropped it. + - A save that changes nothing returns the stored checksum as its version, so the receipt's token is the one the next `If-Match` save must send. +- 3237b4a: fix(metadata-protocol): a package's slot in the metadata list serves the package's own stored row in every row order, as `getMetaItem` naming that package does (#21804) + + Clause-②: no + + ADR-0048 lets one item (type, name, organization scope) hold a package's stored `sys_metadata` row beside a package-less one. `getMetaItem` naming the package has always served the package's own row (prefer-local), and the organization's rows before the env-wide rows (ADR-0005). The list (`getMetaItems`, and the `getMetaItemsForExecution` view of it) built each package's slot from the LATEST of the package's row and the package-less row in the order the store returned them. So with both rows stored, the list served the package-less body for the package in one row order, and an env-wide row of the package could beat the organization's package-less row. + + Now the list and the by-name read take one resolution: the organization's rows, then the env-wide rows; within each, the type's canonical spelling, then the other; within each, the package's own row, then the package-less row, never another package's. A package's list slot serves the row `getMetaItem` naming that package serves, whatever the row order, and the `previewDrafts` list previews the draft the by-name read previews. A package with no row of its own still falls back to the package-less row, as before. The lock the list item reports is unchanged: it is still the strictest lock among the item's rows in scope. + + No key, export, status or error code changes. A list request scoped to one package (`packageId`) reads only that package's rows and is unchanged. +- 2e78046: fix(metadata-protocol): a package-scoped metadata list serves a package-less customization of an item the package ships, as `getMetaItem` naming the package does (#21817) + + Clause-②: no + + ADR-0048 lets one item hold a package-less stored `sys_metadata` row, an ordinary customization, beside the package's own artifact or row. `getMetaItem` naming the package serves the package's own row, else the package-less row, the organization's rows before the env-wide rows (ADR-0005). A list scoped to the package (`getMetaItems({ type, packageId })`, `GET /api/v1/meta/:type?package=`, and the `getMetaItemsForExecution` view of it) read only the package's own rows. So it served the package's artifact over a package-less customization of it, and an env-wide row of the package over the organization's package-less row, while `getMetaItem` naming the package served the customization. + + Now each slot of a package-scoped list resolves the way `getMetaItem` naming the package does: the package's own row, else the package-less row, by the same order the unscoped list uses. This holds over a registry item, a MetadataService item and a view the package's stored container expands, and in the `previewDrafts` list, where a package-less draft stands in when the package has none. The list's membership is unchanged: it still lists only the items the package ships, and a package-less row of an item the package does not ship adds no item to it. The lock each item reports is unchanged. + + No key, export, status or error code changes. +- 87712ab: Global search (`GET /api/v1/search`) skips the objects a caller cannot read instead of failing the whole request with `403 PERMISSION_DENIED` (#21836). + + Clause-②: no + + - **Object level.** Before an object is queried, `searchAll` asks the `security` service's `canReadObject` with the caller's context, the same read gate the engine middleware enforces. An object it refuses is skipped. A member whose scope included any object they hold no read grant on used to get 403 for every query. The console palette then showed "No results found." with no error. + - **Field level.** Each object is searched only on the fields the caller may query (`getQueryableFields`). The engine refuses a search that would match on a hidden field, and `sys_user`'s searchable fields include admin-only columns, so a member's unscoped search hit that refusal as well. An object left with no queryable search field is skipped. + - **Nothing about a skipped object reaches the response.** It is not queried, named or counted in `totalObjects`, and the decision is made before any row is read. An explicit `objects=` naming an unreadable object gets the same answer as a name that matches no object. + - **Other failures still fail the search.** A read error on a readable object, or an admission check that throws, propagates as before. Callers that can read every object, and calls without a context, get the same answer as before. +- d16b9fb: The platform's own `sys_metadata` reads and writes now carry the explicit system opt-in (`isSystem: true`) instead of reaching the data engine with no principal at all + + Clause-②: no + + - **What moved.** Each engine call in these functions now passes `context: { isSystem: true }`. Inside a repository transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along. + - `@objectstack/metadata-protocol`: the overlay reads (`findServedOverlayRow`, `overlayLockLayerAt`), the list read (`readActiveOverlayRows`, and `readFlattenedMetaItems`' draft preview), the authoring gate's stored-collection fold (`foldStoredCollection`), the audit and commit trail writes (`recordMetadataAudit`, `persistPackageCommitRow`), and the package verbs' store calls (`publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage`, `reassignOrphanedMetadata`). + - `SysMetadataRepository`: `get`, `put`, `delete`, `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage counters. + - `@objectstack/objectql`: `ObjectQLPlugin`'s authored action and hook reads, at boot and on resync. + - `@objectstack/core`: the authored-translation read (`readAuthoredTranslationLayer`). + - **Why.** plugin-security passes an engine operation whose context has no user, no position, no permission set and no `isSystem` straight on to the next handler (ADR-0096's principal-less hand-off). These calls worked only because of that pass-through. They are platform plumbing: any door in front of them has already authorized the caller, and the protocol scopes its own rows by organization. So they now say so with the opt-in that already exists. + - **No gate verdict moves.** A system context skips the six gates the middleware still runs before that pass-through: package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. Four of them only act on other objects. The engine-owned gate never fires on a context with no user. The delegated-administration gate only acts on the RBAC link tables. So none of the six applies to the `sys_metadata` family. An instrumented run of the dogfood suite and a booted dev composition recorded no gate firing on any of these calls before the change. After the change it recorded no principal-less call from these functions. + - **One engine check also stands down under `isSystem`.** That is the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The instrumented runs recorded no refusal from it on any of these calls. + - ⛔ No new API, no export change, and no change to what any door authorizes. +- c9761cd: A metadata publish consults the item lock at the package key it resolved + + - A publish that states no package promotes the draft row it resolves, under that row's own package. Its ADR-0010 lock lookup now uses that same resolved key (the stated package, else the draft row's own), the key the gate reads the draft under and the promotion writes under, instead of only the package the request stated. Where several packages' rows declare the strictest lock, the refusal now carries the lock of the package whose draft is being promoted. + - The authoring gate's package narrowing is unchanged: it still uses only the package the caller stated. +- c9761cd: The organization-scoped save check judges a view overlay against every package's environment-wide definition of its row + + - An organization-scoped `view` save or publish in the organization the anonymous form endpoints read is refused when it would leave open a form the environment-wide definition withdraws. Its row anchor is now resolved per package, the way the list read resolves each package's item: each package's own environment-wide row, else the package-less environment-wide row (which stands in for every package), else that package's artifact. Before, with no environment-wide row stored, it judged only the first package's artifact in registry order, and a stored row of any one package hid every package's artifact of the name. + - The known limit stated with the public-form withdrawal ("it may over-close, never under-close") is narrowed. A withdrawal of a view name still closes that name in every package, so it may over-close. The organization-scoped save check judges every package's environment-wide definition of the name. The anonymous endpoints do too, with one exception: where a package's environment-wide copy of a view container is saved, the endpoints read that copy's expansion alone for each form it expands, and can miss another package's withdrawal of that form, whether saved or shipped, until the form is withdrawn in every saved environment-wide copy of that container as well. Reading each package's expansion separately is tracked in #21967. +- c9761cd: The view list serves one item per package for a view name that several installed packages ship, whether or not a view row is stored + + - Where two installed packages ship a view of the same name, the list read (`getMetaItems` for `view`) now serves each package's item of that name, as it already did while no view row was stored. Only a name that a stored view container's expansion writes is upserted by name. + - The environment-wide view list is the layer the anonymous form endpoints judge a withdrawal against. A package-less organization copy of the view, stored before one package withdrew the form, is now judged against every package's body of the name there, so it stays closed whichever package withdraws. +- 3c7785d: A public form's explicit intake withdrawal at any metadata layer now holds: layering can only narrow anonymous intake, never re-open it + + Clause-②: yes (widening) + + - **What counts as a withdrawal.** A withdrawal keeps the form's `publicLink` and sets `sharing.enabled: false` or `sharing.allowAnonymous: false`. Only an explicit `false` counts: a switch that is absent is not a withdrawal. Removing the `sharing` block, clearing the `publicLink`, or deleting the view at one layer is not a withdrawal either. A sharing that names no public link withdraws nothing. + - **Organization-scoped saves and publishes.** A `view` save or draft promotion in the organization the anonymous form doors read is refused with `403 NOT_OVERRIDABLE` if it would leave open a form that the environment-wide definition withdraws. This check judges by the stored row: the organization's body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package's artifact), and also with the env-wide view list the way the doors read it (a container-shaped body is expanded the way the list read expands it). Inside the row, a withdrawn form matches by its place (`form`, the same `formViews` entry, or `config`) or by its public slug, and either match is enough. So a renamed `formViews` key, a `form.name`, a move to another place, a listViews collision rename in the expansion, and a new or re-cased slug are all judged as the same form. A form that differs from every withdrawn form in both place and slug, such as a sibling in the same container, stays independent. The check also covers an organization copy that was already open before the withdrawal, the next time it is saved. The message names the remedies: save the overlay withdrawn, or publish the form from its environment-wide definition. An organization-scoped save that keeps the form withdrawn is still accepted. + - **Anonymous form doors.** `GET /forms/:slug` and `POST /forms/:slug/submit` judge by the name of the view item they serve. Beneath the organization's read they read the env-wide view list, and they serve a form only when the env-wide item of the same name does not explicitly withdraw a form in the same place or with the same slug. A withdrawn form answers `404 FORM_NOT_FOUND` on both doors and creates no record. A form that is open at every layer is served as before. A form that only an organization carries is still served there. A different view that uses the same slug is a different form, and the two never close each other. + - **Package-shipped forms.** A package's form is part of the env-wide definition, not a separate layer beneath it. A package artifact that was parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal (fail closed). An artifact that reached the runtime without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: there a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch: an env-wide save may open a form the package ships closed. + - **Known limit: packages and names.** A withdrawal of a view name closes that name in every package. When two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name: it may over-close, never under-close. Per-package precision is tracked in #21934. A publish judges the draft it promotes under the same package key: with two packages holding a draft of the same view in one organization, each draft is judged on its own publish. + - **Known limit.** The doors match by served item name, and the save check runs only on an organization-scoped save or publish. An organization overlay that was stored before the env-wide withdrawal, or that a rollback or commit-revert restores, can still be served if it keeps the form open under a different key or place than the env-wide definition. Withdraw the form in that overlay to close it. Rollback and commit-revert restores are not gated by the save check. + - **Behaviour change.** Between 17.6.0 and this fix, an organization overlay that published a form the environment-wide (package) definition withdrew was honoured: the doors served the organization's copy. That behaviour never shipped in a release, and it is reversed on purpose. The environment-wide withdrawal now wins. + - **`@objectstack/metadata-core`** exports the shared judgement `anonymousFormIntakeWithdrawnIn` (a new, additive public export). Both the doors and the save path read it. +- 6dd99b8: Public forms: every declared means of withdrawing a form from anonymous intake is now honoured by every anonymous form door. Which forms a `view` opens to anonymous intake is now decided by one rule, `anonymousFormIntakeCandidates` (new in `@objectstack/metadata-core`, alongside `anonymousFormIntakeSlugs`, `anonymousFormIntakeSlug` and `publicFormSlug`), read by both the anonymous form endpoints in `@objectstack/rest` and the organization-scoped `view` write check in `@objectstack/metadata-protocol`, so the two can no longer disagree. A form is served anonymously only when its `sharing` config declares public sharing as `SharingConfigSchema` defines it: `sharing.enabled: true`, `sharing.allowAnonymous: true` and a `sharing.publicLink` slug. `enabled` defaults to `false`, so a form that set only `allowAnonymous` and `publicLink` is no longer served on the anonymous endpoints (`404 FORM_NOT_FOUND`). Migration: add `enabled: true` to the form's `sharing` block (and to any stored overlay of it) to keep it public; see the public forms guide. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [7e7e64b] +- Updated dependencies [15b29d3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [0fc8087] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [39a912e] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1371dc9] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [d70353f] +- Updated dependencies [6210f88] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [a4fd82a] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [0fe0a59] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/lint@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/sdui-parser@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index cdc1f2feaa3..693a62336ba 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "17.6.0", + "version": "17.7.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 319ee13481e..c561e1b82fb 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,174 @@ # @objectstack/metadata +## 17.7.0 + +### Minor Changes + +- 5555047: One judge for a view container's own `name` at every door that files a container: the new `@objectstack/metadata/view-container-name` entry + + Clause-②: yes + + - New subpath `@objectstack/metadata/view-container-name`. It exports `viewContainerNameRefusal(container, sourceLabel, ownerId)`, the source registrars' entry, whose key is the object the container binds to (its own `object`, else `list.data.object` / `form.data.object`). It returns a `VALIDATION_ERROR` / 400 refusal for an aggregated view container whose own `name` is set and differs from that key, and `undefined` otherwise; a container with no `name`, and a standalone view record (`viewKind`), are not judged by it. The subpath also exports `savedItemNameRefusal(type, item, saveName, door)`, the runtime write doors' entry, which judges every metadata type against the name the row is written under, and the `ViewContainerNameRefusal` type both entries return. + - The artifact/HMR loader's container branch now refuses such a container through the judge, before it files anything. What it refuses and the envelope are unchanged (`VALIDATION_ERROR` / 400). The message is now the judge's, the words the ObjectQL boot loop and `os validate` print, where it was the generic `IMetadataService.register` contract's. +- 44defd4: The runtime write doors' `name` judge covers every metadata type: `savedItemNameRefusal` replaces `savedViewContainerNameRefusal` on `@objectstack/metadata/view-container-name` + + Clause-②: yes + + - `savedItemNameRefusal(type, item, saveName, door)` is the one entry the runtime write doors of `@objectstack/metadata-protocol` call. It returns a `VALIDATION_ERROR` / 400 refusal when a body of any type carries its own `name` and that `name` differs from the name the row is written under, and `undefined` otherwise. `door` is `'save'`, `'restore'` or `'publish'`. A body with no `name` passes. A `name` the body does carry is judged whatever its value (`''`, `null` and non-strings included), with one exception: a `view` at the `'save'` door, which stamps a missing name there, is judged only on a non-empty string `name`. + - It replaces `savedViewContainerNameRefusal(container, saveName)`, which judged view containers only. That export was added to this subpath in this same release cycle and never shipped in a published version, so no published export is removed. + - The words name the type and give a remedy that works for it: "drop `name`, or set it to KEY" for a view, whose missing name the save door stamps; "drop `name`" for a `field`, whose row is named `object.field`, which its dot-free column `name` cannot spell; "set `name` to KEY, or save the item under NAME" for every other type; and at the restore and publish doors, whose caller cannot edit the stored body in place, the save that fixes it. For a view container at the save door the message is byte for byte the one `savedViewContainerNameRefusal` returned. + - `viewContainerNameRefusal` (the source registrars' entry), its words and the `ViewContainerNameRefusal` type are unchanged. + +### Patch Changes + +- 8598614: Provenance comments in `@objectstack/metadata` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each now cites the commit in this repository's history that made the decision it describes, except two + that meant an objectui issue and now spell `objectui#6111`. Some of these + docblocks sit on exported members, so the reworded text appears in the published `index.d.ts` / + `index.d.cts`, `node.d.ts` / `node.d.cts` and `view-container.d.ts` / `view-container.d.cts`, and + comments that esbuild keeps appear in the JavaScript output (`index.js` / `index.cjs`, `node.js` / + `node.cjs`). + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 0fe0a59: `DatabaseLoader` now persists a `register` whose only change is the order of an object's `fields` (#21828). Before, the loader's checksum sorted every map, so a field reorder hashed equal to the stored row and was never written. The running process still saw the new order, but the persisted `sys_metadata` row kept the old one. + + Clause-②: no + + - **One hash vocabulary in `sys_metadata.checksum`.** The loader now stamps the hash `SysMetadataRepository` stamps on the same column: `hashSpec(body, type)` from `@objectstack/metadata-core`, written as `sha256:` + 64 hex. It is hashed as the item's metadata type, so a reorder of an object's `fields` is a change and every other map is still key-order independent. Its history rows carry the same value as the row they record. Before, the loader wrote bare hex from `calculateChecksum`. That function is unchanged and still exported, but nothing writes the column with it. + - **Rows stamped before this release.** Whether a `register` is a no-op is now decided by re-hashing the stored body, not by comparing the stored checksum. A row with an unchanged body is not rewritten: no version bump, no history row, and it keeps its old checksum until its content next changes. The first real change rewrites it with the new stamp. A reorder into sorted key order is written too, even against a stored checksum that sorted every map. + - **ETags.** `load()` and `stat()` report the row's checksum as `etag`, so a row the loader writes from this release on reports a `sha256:` value. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [1e4ae08] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata-fs@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index c49fae3a9db..1cbe5edd757 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.6.0", + "version": "17.7.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 6cf608c0be6..c3b84440ee1 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,678 @@ # @objectstack/objectql +## 17.7.0 + +### Minor Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- 713b0fa: fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **Three things change for callers and operators.** + + 1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". + 2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. + 3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. + + **What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. +- c2cd651: fix(objectql)!: `having` and the per-aggregation `filter` refuse a plain `{ $field }` reference between two columns of different comparison classes with `INVALID_FILTER` / 400, as `where` refuses it + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a `{ $field }` reference may pair at two positions of `engine.aggregate`. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What was accepted before.** At `having` and at a per-aggregation `filter` (`aggregations[i].filter`), the comparison-class rule was applied only to a reference carrying `addDays`. A plain reference across two classes was answered: `{ closed_at: { $lte: { $field: 'due_on' } } }`, with `closed_at` a `datetime` and `due_on` a `date`, counted rows by `@objectstack/formula`'s whole-day reading of the bare day, and a `having` of `max(closed_at)` against a `day` date bucket kept groups the same way. The same comparison in a `where` is refused `INVALID_FILTER` / 400 by `driver-sql`. + + **What is refused now.** A scalar comparison (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) whose comparand is a plain `{ $field }` naming a column of a different comparison class. The classes are the spec's `CROSS_FIELD_COMPARISON_CLASSES` (`numeric`, `text`, `boolean`, `date`, `datetime`, `time`), judged by the spec's `crossFieldComparisonVerdict`, the classification `driver-sql`'s `where` compiler reads. The refusal is `INVALID_FILTER` / 400, raised before any driver is asked for a row, on an empty set as on a populated one, through `engine.aggregate` and `POST /api/v1/data/:object/query`: + + - in a per-aggregation `filter`, the fields, the operator and the reason are withheld from the message and written to the server log, as `where` withholds them; the message now names the same-class rule beside the `addDays` one; + - in `having`, the message names the two columns of the query's own projection and their classes, in the sentence `where` logs for the same pair. A `having` column's class is read off the query: a `day` date bucket is a `date`, a coarser bucket a `text` label, `count` / `count_distinct` / `sum` / `avg` are `numeric`, and `min` / `max` take the type of the field they read. + + **The remedy.** Compare same-class columns: a `datetime` with a `datetime`, a `date` with a `date` (a `day` bucket is one), a number with a number. A comparison between a `datetime` and a calendar day has no single answer across SQL and memory, so the platform does not define one. + + **Unchanged.** A reference between two columns of one class answers as before. A `{ $field, addDays }` pair keeps its judgement and its words. A column whose class the declaration cannot tell is not judged, as an `addDays` pair is not: a host with no registered object, a column the field map does not list (`id`), an aggregation over an undeclared field. A column the spec gives no comparison class at all (a structured-JSON, multi-valued or file field, a formula) is not judged by this rule either. +- ceb4a93: fix(objectql)!: `having` and the per-aggregation `filter` refuse a `{ $field }` comparison against a column with no comparison class (a file field, a list, a formula) with `INVALID_FILTER` / 400, as `where` refuses it; `applyInMemoryAggregation` takes the same reference rules + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what a `{ $field }` reference may pair at two positions of `engine.aggregate`, and what `applyInMemoryAggregation` accepts when it is handed a field map. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What was accepted before.** The spec's comparison-class verdict (`crossFieldComparisonVerdict`) answers `no-class` for a pair in which either column has no comparison class: a list or an object (a structured-JSON type, a multi-option type, a multi-capable type flagged `multiple: true`), a file field (`FILE_REFERENCE_TYPES`), or a formula. `having` and a per-aggregation `filter` (`aggregations[i].filter`) did not judge that answer. Measured on `SqlDriver` over better-sqlite3 through `engine.aggregate`, beside a `where` twin that `driver-sql` refused `INVALID_FILTER` / 400 each time: + + - a per-aggregation `{ customer_id: { $ne: { $field: 'photo' } } }` (text against an image) counted 6 of 6 rows; + - a per-aggregation `{ closed_at: { $lte: { $field: 'due_f' } } }` (datetime against a formula) counted 0 of 6; + - a per-aggregation `{ amount: { $ne: { $field: 'tags' } } }` (number against a multiselect) counted 6 of 6 when the column held no value, 0 on an empty table, and was refused by the per-row array check, in other words, when the column held a list; + - `having: { photo: { $ne: { $field: 'n' } } }` over a groupBy on an image field kept all 6 groups. + + A `{ $field, addDays }` pair against a formula was answered at both positions. `applyInMemoryAggregation`, called with a field map, applied none of `engine.aggregate`'s reference rules: a reference to a field the map does not declare, a pair across two classes and a pair against a column with no class were all counted. + + **What is refused now.** At `having` and at a per-aggregation `filter`, a scalar comparison (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`) whose `{ $field }` comparand, or whose own column, has no comparison class, with or without `addDays`. The refusal is `INVALID_FILTER` / 400, raised before any driver is asked for a row, on an empty set as on a populated one, in the reason `driver-sql`'s `where` logs for the same pair: the column it names (the referenced one first, as `where` asks it first) "has no scalar stored column a comparison can read". The per-aggregation `filter` withholds the fields, the operator and the reason from the message and writes them to the server log, as `where` does; `having` names the two columns of the query's own projection. A `{ $field, addDays }` pair against a file or list column was already refused, in the `addDays` pair rule's words (that rule reads those types as text, so it answered with a cross-class or an offset sentence); it is now refused in this one. + + `applyInMemoryAggregation(rows, ast, timezone, fields, reportWithheld)`, when `fields` is passed, judges each per-aggregation `filter` by the reference rules `engine.aggregate` applies at that position, through the same function, before any row is judged: the referenced column (and an `addDays` offset column) is declared, a pair across two classes or against a column with no class is refused, and an `addDays` pair follows its class rule. The refusal is `INVALID_FILTER` / 400; the withheld diagnostic goes to `reportWithheld`, and it names no object (this function is not told one). + + **The remedy.** Compare two columns that each have a comparison class, and the same one: a file field, a list and a formula have no stored scalar a comparison can read. Compare the scalar column the value is derived from, or filter the column with a literal. + + **Unchanged.** A reference between two columns of one class answers as before, and the cross-class refusal keeps its words. A side with no declaration is not judged at any of the three positions: a host with no registered object, a column the field map does not carry (`id`, and an audit-opt-out object's row-carried `created_at` / `updated_at`), an aggregation over an undeclared field. A declared type outside `FieldType` is not judged either. `applyInMemoryAggregation` called without `fields` judges nothing it did not judge before. +- 9f13c94: fix(objectql)!: a comparand against a declared boolean field is narrowed to its boolean at the engine's filter door, and any string other than "true" / "false" / "1" / "0" is refused with `INVALID_FILTER` / 400 + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what a filter may compare a declared `boolean` or `toggle` field with, at every filter position and through every door that reaches the engine's filter walk (`engine.find` / `findOne` / `count` / `aggregate` / `update` / `delete`, and every spelling the data API hands it). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **What was accepted before.** A string compared with a boolean field was neither refused nor read as a boolean: the engine handed it to the driver as written, and every answer was a 200. Measured on two rows (one `true`, one `false`) on InMemoryDriver and on SqlDriver over SQLite, through `engine.find`, `engine.aggregate` and the protocol's `findData` with each spelling the `POST /api/v1/data/:object/query` and `GET /api/v1/data/:object` routes hand it: + + - `"true"` (implicit, `$eq`, `$in`) and `"false"` (implicit), and both through `?filter=`, `?$filter=`, the filter AST and the bare query parameter (`?flag=true`), matched no row on either driver; + - `$ne "true"` and `$nin ["true"]` returned both rows, the true row included; + - `"yes"` matched no row, and `$ne "yes"` both rows; + - `1`, `"1"`, `0` and `"0"` at `where` (and `"1"` / `"0"` through every spelling above) matched the right row on SQLite and no row on InMemoryDriver (`$ne 1` returned both rows there); + - the per-aggregation `filter` and `having` (the engine's own evaluator) answered `"true"` with no row and no group, and `$ne "true"` with every one. + + **What is answered now.** At `where` (both spellings), the per-aggregation `filter` and `having`, on every verb that collects a filter, before any driver is asked for a row: + + - `true` / `false` are handed to the driver as written; + - `1` / `0`, `"1"` / `"0"` and `"true"` / `"false"` are narrowed to `true` / `false`, so every driver receives the one boolean each names. Measured on InMemoryDriver and on SqlDriver over SQLite, `?flag=true` and `?flag=1` now return the true row; any other driver receives the same narrowed boolean by mechanism (PostgreSQL and MySQL not measured); + - any other string, a different letter case (`"TRUE"`), surrounding whitespace, a blank and a `{placeholder}` included, is refused `INVALID_FILTER` / 400. The message names the field, its declared type, the comparand and its position, and says what is wrong with it. + + The accepted set is the one the record validator already admits when a boolean field is WRITTEN. The rule lives in `@objectstack/spec/data`'s `filter-boolean-comparand-declared-type.ts`, and the engine applies it in the same walk that judges number comparands. + + **The remedy.** Write `true` or `false`. In a querystring, where every value is a string, write `true` / `false` or `1` / `0`. + + **Unchanged.** A boolean comparand, `null` (the null test) and the flag operators (`$null`, `$exists`, `$empty`) answer as before, and so does every comparand against a field that is not boolean. A number other than `1` / `0` against a boolean field is still handed to the driver as written. A filter on a `formula` field is still refused one step earlier, as before. +- 45efcfa: fix(objectql)!: a number other than `1` / `0`, a `Date` or an array compared against a boolean field is refused with `INVALID_FILTER` / 400 at `where`, a per-aggregation `filter` and `having`, instead of a PostgreSQL 500 or an empty 200 + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what a filter may compare a declared `boolean` or `toggle` field with, at every filter position and through every door that reaches the engine's filter walk (`engine.find` / `findOne` / `count` / `aggregate` / `update` / `delete`, and every spelling the data API hands it). It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type of this package changes; the rule is `@objectstack/spec/data`'s `booleanComparandDoorVerdict`, whose own changeset lists what moved there. + + **What was answered before.** A non-string comparand outside the accepted set reached the driver as written. Measured on two rows (one `true`, one `false`) through `engine.find` / `engine.aggregate`, on InMemoryDriver, SqlDriver over SQLite and SqlDriver over PostgreSQL 16: + + | position | comparand | before: memory · SQLite · PostgreSQL | now, on all three | + |:--|:--|:--|:--| + | `where` | implicit / `$eq` `2`, `-1`, `0.5`, a `Date` | no row · no row · `DATABASE_ERROR` (500) | `INVALID_FILTER` / 400 | + | `where` | `$ne` the same | both rows · both rows · 500 | `INVALID_FILTER` / 400 | + | `where` | a `$in` member `2` or a `Date` | the other members' rows · the same · 500 | `INVALID_FILTER` / 400 | + | `where` | a `$in` member `[true]` | the other members' rows (200) · a driver 400 · a driver 400 | `INVALID_FILTER` / 400, in one set of words | + | per-aggregation `filter` / `having` | any of the above | count 0 and no group (every row and group under `$ne`), a `$in` member ignored, on all three | `INVALID_FILTER` / 400 | + | all three positions | `true`, `1`, `"true"` (the controls) | the true row, count 1, the true group | the same | + + **The remedy.** Write `true` or `false` (or `1` / `0`). To match either value, use `$in`, each member a boolean. Compare a `Date` with a date or datetime field. + + **Unchanged.** `true` / `false` pass as written, the accepted spellings (`1` / `0`, `"1"` / `"0"`, `"true"` / `"false"`) narrow as before, and any other string is refused in the same words as before. `null` (the null test) and the flag operators answer as before. A value outside the accepted comparand types (`undefined`, a plain object, a `Map`) keeps the comparand-type door's own refusal and words. A filter on a `formula` field is still refused one step earlier. Driver-direct callers that never pass through the engine keep each driver's native binding. +- eb9ef79: An in-process engine verb refuses an object name the registry does not resolve, with the data door's own `OBJECT_NOT_FOUND`, instead of handing it to the driver as a raw table name + + Clause-②: no (narrowing) + + + + **BREAKING** accept-set narrowing of the engine's in-process verbs, shipped as `minor` under the repo's launch-window convention for breaking changes. + + **What was accepted before.** `find`, `findOne`, `count`, `aggregate`, `insert` (and `insertMany`), `update`, `delete` and `validate` resolved their target through the schema registry and, for a name the registry did not resolve, handed the name to the driver as a raw table name. A caller in the process (a sandboxed action or hook body's `ctx.api`, an action handler, host code) could therefore read or write a table by a name the generic data door refuses with `404 OBJECT_NOT_FOUND`, and every in-process guard keyed by a registered object name could be stepped around by naming the target another way. + + **What is refused now.** Such a name is refused with the data door's own envelope (`OBJECT_NOT_FOUND`, `status: 404`, the name on `object`, built by `objectNotFoundError` from `@objectstack/core`) before any hook, middleware or driver runs. A registered name resolves exactly as before. `judgeFilter` still judges the filter for a name the registry does not hold, because it reads nothing and reaches no driver; execution refuses that object before admission. + + **Inside the engine.** The single-tenant organization probe asks the registry first: an install that registers no organization object is the lean case it always was, with no organization to derive, and the write proceeds unstamped without a driver read. + + **The fix.** Register the object (in the stack, with `registry.registerObject`, or through a plugin manifest) before addressing it through the engine. Host code that must reach storage without a registry entry addresses the driver itself (`datasource(name)`, `getDriverForObject(name)`), a path a sandboxed body cannot reach. +- 5c9138b: fix(objectql)!: a read with no projection serves the object's declared fields and the platform's system columns, never a column no metadata declares (#21571) + + **BREAKING (narrowing)** — what a released read door serves shrinks. A column that + no metadata declares, typically a field retired in an upgrade whose column additive + schema sync leaves in the table until `os migrate apply --allow-destructive`, is no + longer returned by any read through the engine. + + | read | before | now | + | --- | --- | --- | + | `POST /api/v1/data/:object/query` or `GET /api/v1/data/:object` with no `fields` | every column of the table, retired ones and their values included | the declared fields, the registry's system columns, `id`, `created_at`, `updated_at` | + | `GET /api/v1/data/:object/:id`, export, search hits, the RPC dispatcher, `expand`ed records | the same whole row | the same declared set | + | `engine.find` / `engine.findOne` in process (hooks, flows, plugins), no `fields` | the whole row | the declared set | + | an explicit `fields` naming a declared field whose column does not exist yet (driver-sql retries `select('*')`) | the whole row, retired columns included | the declared set | + | `POST /api/v1/data/:object/:id/clone` of a record whose table carries a retired column | refused `INVALID_FIELD` (the copy carried the retired column into the insert) | cloned | + + **Unchanged:** naming a retired column in `fields` still answers `400 INVALID_FIELD` + on the data door. Declared fields keep their treatment: `internal: true` omission, + credential masking, formula evaluation and the hidden `__search` strip run as + before, and the registry-injected tenant, owner and audit columns are still served. + No driver changed: the engine shapes the rows any driver returns, so the answer is + the same on every driver and every door. Writes, and the rows a write returns, are + not changed by this release. + + **If you still read a retired column's values** (for example a one-time conversion + that copies the old columns into their replacement field): run that conversion + BEFORE upgrading to this release, while the old field is still declared, or, once + it lands, read the unmapped columns through the operator-only `os migrate` read + (objectstack#21573). There is no flag that re-opens undeclared columns on a runtime + door. An in-process reader that needs a column must declare it as a field. + + Clause-②: no (narrowing) + + +- 98eb3b9: fix(objectql,spec)!: a hook's `handler` name resolves inside the hook's own package only (#21604) + + Clause-②: yes (narrowing) + + + + **BREAKING**: a hook whose `handler` is a function NAME (the deprecated form, `handler: 'my_fn'`, with no `body`) now binds only to a function its own package holds. It used to fall back to the engine-wide function registry, which is keyed by bare name, so the hook could bind to a function another package registered under the same name and run that package's code on its own events. + + - **Accepted before:** a string `handler` resolved against the functions handed to the hook's bind, then against every function any package had registered on the engine. A name found nowhere was skipped with a `warn`. + - **Accepted now:** a string `handler` resolves against the functions handed to the hook's bind (the package's `functions`, which an `--artifact` runtime module supplies), then against the functions the same package (`packageId`) registered on the engine. Nothing else. + - **Refused now, at registration:** a name the hook's own package does not hold, whether another package registered it or nobody did. The hook is not bound. The refusal carries `INVALID_REFERENCE` with status `400` (ADR-0112), names the hook, the function and the package, and is recorded on the bind result (`BindHooksResult.errors[]` gains `code` and `status`) and logged at `error`. Under `strict` (`OBJECTQL_STRICT_HOOKS=1`) it is thrown. + - **The doors:** a hook authored at runtime through the metadata API (`PUT /api/v1/meta/hook/:name`) ships with no code package and holds no functions, so a `handler`-only hook authored there is refused when the door binds it; the save itself still answers as before. In a composition of several apps, one app's hook can no longer bind to another app's function. A bind that names no owning package (direct `bindHooksToEngine` use without `packageId`) resolves only the functions handed to it. + - **Unchanged:** a hook with a `body` binds as before. An app's hook naming its own `defineStack({ functions })` entry, or a function its own `--artifact` runtime module exports, binds as before. The install-local door's refusal of a hook with no `body` is unchanged. + + What to do with a refused hook: give it a `body` (sandboxed JS), or declare the function in the hook's own package's `functions`. To reuse another package's function, import it from the package that owns it and declare it there. This ships as `minor`, under the launch-window convention for narrowings of an accept set. +- 5b5e83f: fix(objectql)!: the record a write returns, and the prior read it binds as `previous`, serve the object's declared fields and the platform's system columns, never a column no metadata declares (#21613) + + **BREAKING (narrowing)** — what a released write door returns shrinks. A column that + no metadata declares, typically a field retired in an upgrade whose column additive + schema sync leaves in the table until `os migrate apply --allow-destructive`, is no + longer returned by any write through the engine. This completes the read-side rule of + the previous release (#21571) for writes. + + | write | before | now | + | --- | --- | --- | + | `PATCH /api/v1/data/:object/:id`, batch `update`, `updateMany` | the post-write row read back with `select *`: retired columns with their stored values | the declared fields, the registry's system columns, `id`, `created_at`, `updated_at` | + | `POST /api/v1/data/:object`, `POST /api/v1/data/:object/:id/clone`, `createMany`, batch `create` / `upsert`, `insertMany` outcomes | the inserted row from `returning('*')`: retired columns as `null` | the same declared set | + | `data.record.created` / `data.record.updated` events, and the webhook deliveries that carry them as `after` | the same whole row | the declared set | + | `engine.insert` / `engine.update` in process (actions, flows, plugins), and a hook's `ctx.result` | the whole row | the declared set | + | a hook's `ctx.previous` on update and delete (by id and per row), and the audit ledger's delete `old_value` and create `new_value` | the whole stored row, retired columns and their values included | the declared set | + + **Unchanged:** declared fields keep their treatment. An `internal: true` field is still + returned whole on the engine-level write result to the privileged writer that just + wrote it, and still stripped from every data-door response; formulas are still + hydrated onto the result; the registry-injected tenant, owner and audit columns are + still returned. No driver changed: the engine shapes the rows any driver returns, so + the answer is the same on every driver and every door. A retired column's values stay + in the table. + + **If you still read a retired column's values off a write** (for example a hook, a + flow or a webhook receiver that copied an old column into its replacement): run that + conversion BEFORE upgrading to this release, while the old field is still declared, or, + once it lands, read the unmapped columns through the operator-only `os migrate` read + (objectstack#21573). There is no flag that re-opens undeclared columns on a write. A + reader that needs a column must declare it as a field; a validation rule, a + `readonlyWhen` or a hook condition that reads a column must name a declared field too. + + Clause-②: no (narrowing) + + +- 8843505: fix(objectql)!: a system write's readonly value is judged for its shape — a seed's `'yesterday'` on a readonly datetime is refused with the sentence any other field gets, never stored (#21663) + + **BREAKING** — a write that keeps a readonly value now has that value's SHAPE + checked. The static readonly strip still exempts a system write (seed replay, + migration, `isSystem` plugin code, a `before*` hook's stamp) and still drops a + non-system caller's readonly value; what changed is that the value the + exemption keeps is no longer stored unjudged. Before, the record validator + skipped every readonly field, so under `isSystem` a malformed readonly value + reached the driver verbatim — a seed's `run_at: 'yesterday'` on a readonly + `datetime`, an unresolved `cel` envelope from a seeder that skips its + resolution, an authored `created_at` the seed now keeps — while the same value + on a non-readonly field was refused. + + Now it is refused the same way: `VALIDATION_FAILED` (400 at an HTTP boundary), + the same field code and the same sentence a non-readonly field gets + (`Run At must be a valid datetime (ISO-8601)`), and a seed counts the row as a + seed error. This holds on insert, on the dry run (`ObjectQL.validate`), and on + both update paths, where the readonly values left after the strip are judged. + + Which checks a readonly value reaches — its type's shape, never a constraint: + + - refused: a `date` / `datetime` / `time` the platform does not read, a + non-number on a number-typed field, a non-boolean on a boolean, a non-array on + a multi-value field, a filter-operator object, and an ADR-0104 reference / + media / structured-JSON shape under the object's own posture (warn-first, as + on any other field, until the deployment's evidence enforces it); + - NOT checked, exactly as before: option membership, `maxLength` / + `minLength`, `valueDomain`, `min` / `max` / `scale` / `precision`, the email / + url / phone formats, and `required`. Option membership stays out on purpose: + `sys_activity.type` is a readonly `select` whose options are the built-in set + of an open vocabulary, and an author-contributed value there is stored. + + A numeric string on a readonly number field is now written as its number, and a + lone scalar on a readonly multi-value field as a one-member list, as on any + other field — the door reads the value the same way it judges it. + + **What moves for consumers.** A seed, migration or `isSystem` write that puts a + malformed value in a readonly field — or a hook that stamps one — is refused + where it was stored. Fix the value at its producer: write an ISO-8601 instant + (or a `Date`) into a readonly `datetime`, resolve a `cel` value before the + write, and stamp numbers and booleans as such. Rows already stored are never + re-read or rewritten. `validateRecord`, as exported, is unchanged: the readonly + scope is the engine write path's own. + + Clause-②: no (narrowing) + + + +### Patch Changes + +- 41a3c8d: Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it + + Clause-②: no + + `driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships: + + - `@objectstack/spec`: + - The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it. + - `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`). + - `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did. + - A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it. + - `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too. + - `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`. + - `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 04f0cc4: fix(objectql): a driver error that leaves the engine no longer carries the failing statement or the caller's values + + Clause-②: no + + The engine has cut the bound statement out of its own log line for a failed driver call for a long time, but it rethrew the driver's raw error. Any in-process code that logged what it caught, such as an auth library's error logger, printed the statement and the row's values. The same cut now runs where the error leaves the engine, so no consumer needs a patch of its own. + + - **Where.** Every engine operation that reaches a driver: `find`, `findOne`, `count`, `aggregate`, `insert` (batch included), `update` and `delete` (by id and by predicate), `execute`, `transaction`, `resolveSecretField` and `resolveInternalField`. + - **What is cut.** The statement and the caller's values, from the error's `message` and `stack`, from the properties drivers attach (mysql2's `sql` and `sqlMessage`; node-postgres' `detail`, `where` and `internalQuery`), and down the `cause` chain. A `DuplicateRecordError` keeps its own fields and carries a cut `cause`. + - **What stays.** The error's class (`instanceof` still holds), `name`, `code`, `errno`, `sqlState`, Postgres' identifier fields (`constraint`, `table`, `column`, …) and the database's own diagnostic. The message now reads as the statement's kind, a `[statement and bound values redacted]` marker and the diagnostic. A Postgres key-shaped `detail` keeps its column list. Every REST answer keeps its status, code and `field`. + - **What changes for a caller.** Code that read the statement or a value out of a driver error's message or properties now gets the marker instead. Branch on the class, `code` or `errno` instead. The driver error on a `DuplicateRecordError`'s `cause` is an equivalent copy, no longer the object the driver threw. An import's row report for a value-bearing database error no longer repeats the rejected value. +- 1fd5664: fix: when the store refuses an uninstall's `sys_packages` delete, the uninstall now answers the failure and removes nothing else, instead of answering success and coming back after the next restart (#21276) + + Clause-②: no + + **`@objectstack/metadata-protocol`.** `deletePackage` now deletes the package's `sys_packages` row first, before its `sys_metadata` rows, its tables, its registry entry and the rows the uninstall cleanups own. When the `package` service refuses that delete, whether it returns `{ success: false }` or throws, `deletePackage` throws and nothing else is removed. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, the refusal was logged as a warning, and `DELETE /api/v1/packages/:id` answered `200` after the package's metadata, tables and grants had been removed. The package then came back after the next restart. + + Before that store delete, `deletePackage` now also asks the registry whether the uninstall would be refused because another package extends an object this package owns (ADR-0029). If so, it throws the registry's own refusal with nothing removed. A registry without the new question is not asked, and the refusal then surfaces at the registry withdrawal, as before. + + **`@objectstack/objectql`.** New: `SchemaRegistry.assertPackageUninstallable(packageId)`. It throws the refusal `unregisterObjectsByPackage` and `uninstallPackage` raise for an object another package extends, with the same message, and it changes nothing. `unregisterObjectsByPackage` now calls it, so there is still one copy of that check. + + **`@objectstack/runtime`.** `DELETE /api/v1/packages/:id` now asks `deletePackage` before it touches anything. It checks that the package exists with a read, and it withdraws the package from the running registry and clears its saved disable record only after `deletePackage` has answered. So when the store refuses, the door answers `500`, the same process keeps serving the package, and a package that was disabled stays disabled after a restart. Before this, the door withdrew the package and cleared its disable record first. A refused delete then left the package missing until a restart, and brought a disabled package back enabled. + + An uninstall refused because another package extends an object this package owns still answers `500` with nothing changed: the stored rows, the registry entry and the disable record all stay as they were, in the same process and after a restart. That refusal is now decided before the store delete, instead of by the door withdrawing the package first. An ordinary uninstall, and a host with no `package` service, are unchanged. +- 57cc695: feat(spec): `CryptoContext` gains a required `scope` discriminant, and `LocalCryptoProvider` binds it into a delimiter-safe, versioned AAD (ADR-0128 D1–D3, #21326 stage 1) + + Clause-②: yes + + **BREAKING** for `ICryptoProvider` implementers and for every direct caller of + `encrypt`, `decrypt` or `rotateKey`: `CryptoContext.scope` is required, so a + context literal without it stops compiling (`TS2741`), and the compiler names the + missing member. `LocalCryptoProvider` also refuses such a context at runtime with + `CryptoContextScopeError`, for a caller the compiler never saw. Code that only + injects a provider is unaffected. + + `scope` is a member of the new closed set `CRYPTO_CONTEXT_SCOPES` (type + `CryptoContextScope`), one member per producer of `CryptoContext`: + `settings` (`SettingsService`), `object_secret_field` (the ObjectQL engine's + secret-field path) and `datasource_credential` (the datasource secret binder). + Each producer in this release passes its own member on every call. A new producer + adds its own member; it never borrows an existing one. + + What the contract now requires of every provider that binds AAD: + + - **Producer-discriminated (D1).** The AAD binds `(scope, namespace, key)`, so a + ciphertext sealed by one producer does not authenticate under another + producer's context, whatever the two `(namespace, key)` pairs are. + - **Delimiter-safe (D2).** Distinct triples produce distinct AAD bytes. An + unescaped join is not permitted. + - **Versioned.** A ciphertext records which AAD derivation sealed it, and is + opened only with that derivation. An unknown derivation fails closed. No second + derivation or scope is ever tried after a failure (D3). + + `LocalCryptoProvider` seals every new value under derivation version 2: a lead + byte that never occurs in UTF-8, a versioned label, then the scope, namespace and + key, each prefixed with its 4-byte length. The ciphertext carries a `v2:` marker. + A ciphertext with no marker is version 1, the bare base64 every earlier release + sealed, and it still opens with the older `(namespace, key)` binding. Existing + secrets therefore keep working with no action, and carry the older binding until + they are re-wrapped. Re-wrapping existing ciphertext at rest is stage 2 of + #21326. `rotateKey` already re-seals a version-1 handle under version 2. Any other + marker is refused with `UnknownCiphertextVersionError`. + + Operational note: a secret set or rotated by this release carries the `v2:` + marker, and an earlier release cannot open it. A rollback past this release needs + those values to be set again. + + `@objectstack/objectql` and `@objectstack/service-datasource` pass their own + scope on every seal and open. Their public surface is unchanged. + + +- d956910: fix(objectql): a raw statement's driver fault, and a lifecycle sweep's direct-driver fault, no longer carry the statement or the caller's values + + Clause-②: no + + Two paths the engine-boundary cut did not reach now take it. + + - **`ObjectQL.execute`.** The cut ran on a driver error's message only when the shared leak predicate recognised a statement in it, and the predicate recognises four leading verbs. A raw statement opening with any other word, such as a common-table-expression form or a dialect's own upsert or merge verb, kept the statement and the bound values on the declared fault's `cause` (its `message` and `stack`), where any logger that prints an error's cause chain wrote them out. The door now tells the cut that it sent a statement, so the cut runs whatever word the statement opens with. The predicate's list is unchanged. + - **The lifecycle sweep.** The Archiver copies rows to the cold store and deletes them from the hot store through the drivers directly, not through an engine door. A driver fault there, such as a cold write the archive store refused, put the archived row's values into the sweep's warning line and its `report.errors` entry. The sweep now cuts the fault the same way before it reports or logs it. + - **What stays.** The error's class, `code`, `status` and the database's own diagnostic, on the fault and on its `cause`. A raw statement opening with one of the four recognised verbs is cut exactly as before. A sweep failure that is not a driver error is reported word for word as before. + - **What changes for a caller.** Code that read the statement or a value out of a raw statement's fault, or out of a lifecycle sweep's error entry, now gets a `[statement and bound values redacted]` marker followed by the diagnostic. Branch on the error's class and `code` instead. +- 6d728b8: refactor(objectql): the engine takes its driver-fault redaction from `@objectstack/types` + + Clause-②: no + + The redaction the engine applies to its write-path log lines, at its boundary, at the raw-statement door and in the lifecycle sweep now lives in `@objectstack/types`, so `@objectstack/driver-sql` calls the same cut. The engine calls it as before, with the same arguments, and its answers are unchanged. None of the moved names was exported from `@objectstack/objectql`'s entries, so its public surface does not move. +- 5555047: `viewContainerNameRefusal` is now re-exported from `@objectstack/metadata/view-container-name` + + Clause-②: no + + The divergent view-container `name` judge moved to `@objectstack/metadata`, the one layer the boot loop, the artifact/HMR loader and the runtime save door all depend on, so all three call one judge. `@objectstack/objectql` keeps the `viewContainerNameRefusal` export, its signature and the `ViewContainerNameRefusal` type. The boot loop's refusal and the words it and `os validate` print are unchanged, byte for byte. +- a1ca156: `LifecycleService` asks the registry whether `sys_organization` is registered before its governance tenant scan reads it, so a composition that registers no `sys_organization` sweeps single-tenant again instead of aborting every sweep + + Clause-②: no + + - The engine's in-process verbs refuse an object name the registry does not resolve (`OBJECT_NOT_FOUND`, 404) before any driver is asked. Take a composition with a settings service and lifecycle-declared objects but no `sys_organization` object. Its tenant scan got that refusal instead of a missing table, so every sweep aborted before applying any policy. The scan now asks `engine.registry.getObject('sys_organization')` first, the same shape `ObjectQL.probeInstallOrganizations` takes. An unregistered object answers "no tenant overrides", and the sweep runs one global pass on each declared window. + - A registered `sys_organization` is read as before. A missing table is still the one benign driver cause. Every other failure still aborts the sweep and is reported through `report.errors`, an `OBJECT_NOT_FOUND` from that read included. + - `LifecycleEngineLike['registry']` now declares the optional `getObject?(name)` member the scan reads. A registry without it cannot be asked, and the scan then reads exactly as before. No new export and no change to the sweep report's shape. +- be55fd2: A seed row's authored `created_at` is kept when the row is first inserted, the same as when a later boot replays it (#21646). + + Clause-②: no + + - **Before.** The built-in audit stamp (`sys_stamp_audit_insert`) replaced a seed row's authored `created_at` with the boot instant on insert. A later boot's upsert update then wrote the authored value, so a fresh or reset database showed every seeded record as created at boot until the next restart. This held for a literal instant and for a `cel` value such as ``cel`daysAgo(5)` ``. + - **After.** Under the seed write context (`ExecutionContext.seedReplay`, set by `SEED_WRITE_EXECUTION_CONTEXT`), the insert stamp keeps an authored `created_at` and stamps the boot instant only when the row has none. Both paths now store the authored value. The update stamp is unchanged, so `updated_at` still moves on a replay. All three seed writers pass that context: `SeedLoaderService`, `AppPlugin`'s replay of a stack's `data[]`, and `@objectstack/verify`'s `seed()`. + - **Unchanged.** A REST create, a create from a bare `isSystem` context and every other caller still have `created_at` stamped now. `preserveAudit` is unchanged, and the seed context does not gain it. A non-system create that requests `preserveAudit` gets the same warning as before. `created_by` is not stamped on a seed write, because the seed context has no user. An authored value is kept on insert and on replay, as it was before this change. + - ⛔ No schema, key, export or error code is added or removed. +- 5259a35: `droppedFields` on a create names only keys the caller sent, never a value a write middleware filled in + + Clause-②: no + + With `@objectstack/organizations` mounted (the walled tenancy postures), a non-system create that names no `organization_id` has that column filled with the caller's active organization by the organizations write middleware. `ObjectQL.insert` took its record of what the caller sent after that middleware had run. So the static `readonly` strip treated the fill as a caller write, took it, and reported it: + + - Before: `POST /api/v1/data/` with a body that names no `organization_id` answered 201 with `droppedFields: [{ object, fields: ['organization_id'], reason: 'readonly' }]`. `onFieldsDropped` fired with the same event. The console showed it as a warning toast on every such create. + - After: the same create answers 201 with no `droppedFields`, and `onFieldsDropped` does not fire. The row is stored in the active organization, as before. + + `insert` now records which keys each row carries before any write middleware runs. Only those keys count as sent by the caller. No field is exempted by name, so this covers every write middleware fill, including the `owner_id` fill of `@objectstack/plugin-security`. The referential-integrity check reads the same record, so it no longer checks a middleware-filled reference as if the caller had sent it. + + Unchanged: + + - A key the caller does send is judged and reported as before. That includes `organization_id` itself, and a key whose value a middleware rewrote. + - The array insert (`createMany`) and the `single` posture already reported nothing for an absent `organization_id`, and they still do. + - The stored row is the same. Before, the filled column was stripped and then filled again from the same active organization further down. Now the fill is kept. + + No export, type, error code or status changes. +- 26d710e: A runtime schema sync (`ObjectQL.syncSchemas()`) no longer sends DDL to an object with `external` set. That sync runs on an install-local install, a rehydrate and template seeding. It also no longer logs the durability ERROR "Schema sync FAILED … not durable" for such an object. The ERROR still fires for any other object whose sync fails. + + Clause-②: no + + - **What was wrong.** A federated object (ADR-0015) lives on a datasource whose schema the remote database owns. The boot sync never sends it DDL. It binds the object to its remote table with the driver's DDL-free `registerExternalObject`. The runtime sync had no such branch, so it called `syncSchema` on every federated object in the registry. On an external-schema datasource the driver refuses that DDL, as designed. The refusal was then logged as a durability failure, although nothing durable was lost. A showcase-based host printed two false ERROR lines on every install-local install, one each for `showcase_ext_customer` and `showcase_ext_order`. A false alarm on every run teaches operators to skip the one line that, for any other object, means its data is not on disk. + - **What it does now.** `syncSchemas()` treats a federated object exactly as the boot sync does. It binds the object without DDL and logs a `debug` line. If the driver has no `registerExternalObject`, it skips the object at `debug`. A binding that throws is logged at `warn`. It never calls `syncSchema` for the object. The binding matters for an object registered at runtime: without it, every read resolves to a table named after the object instead of the remote table, and fails with "no such table". + - **One predicate.** The boot sync, `syncSchemas()` and `syncObjectSchema()` now ask one shared predicate, `external != null`, so the runtime and boot syncs cannot drift apart again. The predicate reads the object's own `external` block, not its datasource's `schemaMode`. An object without `external` that lands on an external-schema datasource still gets the ERROR when its DDL is refused, because it expected a table it did not get. + - No accepted input, key, export, status or error code changes. +- 0728cbf: A field-narrowed `$search` no longer matches through the pinyin search companion of a field outside the search-field set (#21880). + + Clause-②: no + + - **What changed.** When the optional pinyin search companion is on (`OS_SEARCH_PINYIN_ENABLED`), the engine's search expansion (`expandSearchToFilter`) adds the companion clause only when every field the companion mirrors is inside the effective search-field set: the set `resolveSearchFields` computes, after any `$searchFields` narrowing. The mirrored fields are read from `resolveSearchCompanionSources`, the same function the companion is provisioned and filled from. + - **What stays the same.** A search with no narrowing keeps the clause whenever the display/name field is in the object's searchable set, so pinyin recall there is unchanged. A CJK term still skips the clause. Deployments with the companion off see no change. + - **Who notices.** A search narrowed to fields that leave out the display/name field, by a `$searchFields` override, by the narrowing global search applies to the fields a caller may query, or by a declared `searchableFields` that omits it, no longer matches through that field's pinyin form. +- f243a29: Deleting an organization no longer fails with a 500 on a deployment that has a federated (ADR-0015 `external`) object bound. The engine's referential cascade no longer treats the `organization_id` the platform injects into a federated object as a reference to `sys_organization`. + + Clause-②: no + + - **What was wrong.** The registry injects `organization_id` into every object, federated ones included, and the platform provisions no storage for a federated object. The cascade's dependents probe filtered the remote table on that column, the SQL driver refused the unknown column (`INVALID_FILTER`), and the probe's failure propagated, so the delete failed. The showcase, with its federated fixture provisioned, answered every organization delete with 500. + - **What changed.** The cascade scan skips a federated object's injected tenant anchor. It asks the same `isFederatedObject` predicate as the driver-option builder and the related-record read, plus the injected-column provenance marker, so an `organization_id` the author declared on a federated object is still probed. The cascade's atomicity plan asks the same question, so it keeps counting exactly the relations the scan probes. + - **What did not change.** Any lookup an author declares on a federated object is still probed, and a probe that cannot run still fails the delete. Only a missing child table is passed over as having no dependents. +- d16b9fb: The platform's own `sys_metadata` reads and writes now carry the explicit system opt-in (`isSystem: true`) instead of reaching the data engine with no principal at all + + Clause-②: no + + - **What moved.** Each engine call in these functions now passes `context: { isSystem: true }`. Inside a repository transaction it passes `{ ...ctx, isSystem: true }`, so the transaction handle still rides along. + - `@objectstack/metadata-protocol`: the overlay reads (`findServedOverlayRow`, `overlayLockLayerAt`), the list read (`readActiveOverlayRows`, and `readFlattenedMetaItems`' draft preview), the authoring gate's stored-collection fold (`foldStoredCollection`), the audit and commit trail writes (`recordMetadataAudit`, `persistPackageCommitRow`), and the package verbs' store calls (`publishPackageDrafts`, `resolveOverlayPackageBinding`, `storedFlowBindingAgrees`, `deletePackage`, `duplicatePackage`, `reassignOrphanedMetadata`). + - `SysMetadataRepository`: `get`, `put`, `delete`, `promoteDraft`, `restoreVersion`, `listDrafts` and the two lineage counters. + - `@objectstack/objectql`: `ObjectQLPlugin`'s authored action and hook reads, at boot and on resync. + - `@objectstack/core`: the authored-translation read (`readAuthoredTranslationLayer`). + - **Why.** plugin-security passes an engine operation whose context has no user, no position, no permission set and no `isSystem` straight on to the next handler (ADR-0096's principal-less hand-off). These calls worked only because of that pass-through. They are platform plumbing: any door in front of them has already authorized the caller, and the protocol scopes its own rows by organization. So they now say so with the opt-in that already exists. + - **No gate verdict moves.** A system context skips the six gates the middleware still runs before that pass-through: package-managed, system-row, curated-capability, audience-anchor, engine-owned and delegated-administration. Four of them only act on other objects. The engine-owned gate never fires on a context with no user. The delegated-administration gate only acts on the RBAC link tables. So none of the six applies to the `sys_metadata` family. An instrumented run of the dogfood suite and a booted dev composition recorded no gate firing on any of these calls before the change. After the change it recorded no principal-less call from these functions. + - **One engine check also stands down under `isSystem`.** That is the referential-integrity check on a caller-supplied lookup. On these writes the only lookup it judged was `sys_metadata.organization_id`, which the repository fills from the door-derived organization. The instrumented runs recorded no refusal from it on any of these calls. + - ⛔ No new API, no export change, and no change to what any door authorizes. +- 13a22d0: Deleting a business unit or a user no longer fails on a deployment that has a federated (ADR-0015 `external`) object bound. The engine's referential cascade no longer treats any column the platform injects into a federated object as a reference. + + Clause-②: no + + - **What was wrong.** The registry injects its own columns into every object, federated ones included: the tenant anchor `organization_id`, the business-unit anchor `owning_business_unit_id`, the owner `owner_id`, and the audit lookups `created_by` and `updated_by`. The platform provisions no storage for a federated object, so none of them exists on the remote table. An earlier fix taught the cascade to skip `organization_id` alone. The cascade's dependents probe still filtered the remote table on the other anchors, the SQL driver refused the unknown column (`INVALID_FILTER`), and the failure propagated. On the showcase with its federated fixture provisioned, deleting a business unit answered 400 and removing a user answered 500. + - **What changed.** The cascade scan and its atomicity plan skip every column the registry injected into a federated object and the object does not provision. They read which columns those are from the registry's own injected-column provenance, not from a list of names, so a column the registry injects later is covered too. The lifecycle reap and archive passes no longer split a federated object's rows per tenant on its injected `organization_id`: such rows carry no organization, so a tenant-scoped retention override for that object has no rows to select, and the object is swept in one global pass. + - **What did not change.** A lookup the author declares on a federated object, including the author's own `organization_id` or `owner_id`, is still probed, and a probe that cannot run still fails the delete. Only a missing child table is passed over as having no dependents. +- 568dc0b: Record-change payloads apply the same credential mask and internal-field omission as write responses. + + Clause-②: yes (widening) + + - **`data.record.created` / `data.record.updated` events.** The engine projects the event's `after` and `changes` bodies through `omitInternalFieldsFromWriteResponse` (`@objectstack/core`), the helper every external write response already uses: credential-class fields (`secret`, and `password` outside the exempt `managedBy` buckets) carry `SECRET_MASK` (or `null` when unset), and `internal: true` fields are omitted. The engine's own write result is unchanged, so a privileged in-process caller that reads the stored value back off `insert` / `update` still sees it. + - **Approval request snapshot.** The record snapshot an approval request stores (`payload_json`) applies the same rule when the request is opened. + - **Outbound webhook body.** The delivered body, and the delivery row that stores it, apply the same rule to `before`, `after` and `changes`. + - **Knowledge index documents.** `recordToDocument` takes the object definition as an optional fourth argument and skips credential-class and `internal` fields, under `'*'` and when a source names one explicitly. `KnowledgeService` passes the definition from the bound engine. + - **New public surface of `@objectstack/service-knowledge` (additive):** `recordToDocument` accepts the object definition as an optional fourth argument; existing three-argument calls behave as before. + - **Receivers see masked values.** Webhook receivers and realtime clients now get `SECRET_MASK` (or `null` when unset) for credential-class fields and no key for `internal` fields. + - **Existing rows are not rewritten.** Approval snapshots, webhook delivery rows and knowledge documents written before this change keep their stored bodies; reindexing a knowledge source refreshes its documents. + - The audit trail already masked these fields and is unchanged. No other accept set or public schema changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [7aab759] +- Updated dependencies [0e10be6] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [535d1d2] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [e9dec3d] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [ce53218] +- Updated dependencies [44defd4] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [74281a8] +- Updated dependencies [9a4182a] +- Updated dependencies [550f4cc] +- Updated dependencies [41b1333] +- Updated dependencies [5dbcee8] +- Updated dependencies [ec390ec] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [5d0e4e2] +- Updated dependencies [e367002] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [7b07749] +- Updated dependencies [eea82af] +- Updated dependencies [a2aadab] +- Updated dependencies [ced217c] +- Updated dependencies [ff16740] +- Updated dependencies [fe10172] +- Updated dependencies [7fd2c34] +- Updated dependencies [c43a8ae] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [cf60dbc] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [b7a13c7] +- Updated dependencies [045f764] +- Updated dependencies [18c2ddc] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [18fe681] +- Updated dependencies [3237b4a] +- Updated dependencies [2e78046] +- Updated dependencies [0fe0a59] +- Updated dependencies [cab6396] +- Updated dependencies [87712ab] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [9cc2c79] +- Updated dependencies [d16b9fb] +- Updated dependencies [753e7a1] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 1001384eea1..d27e7e2baa6 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "main": "dist/index.js", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index fef7816d664..879f1f31ccd 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/observability +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index 8d891d10c86..c1a2431240c 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.6.0", + "version": "17.7.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 bcb47b2deea..cc1017c1af9 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,470 @@ # @objectstack/platform-objects +## 17.7.0 + +### Minor Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- 7665c54: fix(platform-objects)!: retire the `sys_account` `link_social` action, which was dead on every boot; `unlink_account` stays + + Clause-②: no (narrowing) + + + + **BREAKING**: `sys_account` no longer declares the `link_social` action, so the "Link Social Account" toolbar button is gone from the Account app's Linked Accounts list and from Setup's Identity Links. It never completed a link on any boot: it navigated to a `GET` of the social sign-in route, which is served as `POST` only, and it offered a fixed list of seven providers whatever the boot had configured. It is retired under ADR-0049 (enforce or remove) and ships as `minor` under the launch-window convention for narrowings. + + **What stays.** `unlink_account` is unchanged: the same type, target, placement and row-id parameter. Its confirm question no longer says the user can re-link "from their account settings", because no console surface offers that now. The `sys_account._actions.link_social` leaves are gone from the `en`, `zh-CN`, `ja-JP` and `es-ES` bundles. + + **What to do after upgrading.** Linking a social or OIDC identity stays available through the signed-in `POST /api/v1/auth/link-social`, which is `auth.accounts.linkSocial({ provider, callbackURL })` in `@objectstack/client`: call it and navigate to the `url` it answers. + +### Patch Changes + +- 22c2d6f: feat(spec)!: an agent's `memory` contract states exactly what the runtime honours — `maxEntries` and `reflectionInterval` are required once long-term memory is enabled, `longTerm.store` is retired, and the block is `live`, enforced by the cloud AI runtime (#20274) + + **BREAKING** — `agent.memory` narrows to what the cloud AI runtime, the one runtime + that executes agents, actually does with it. That runtime recalls the newest + `maxEntries` distilled notes for the user before the first round, writes one note + every `reflectionInterval` delivered interactions, evicts notes beyond `maxEntries`, + and keeps them in its own database store. Before an agent's first turn it refused + exactly the declarations this spec still accepted, so authoring now refuses them, + by name, with a prescription (ADR-0049 enforce-or-remove): + + - **`longTerm.maxEntries` and `reflectionInterval` are required when + `longTerm.enabled` is true.** No default is declared for either: none has a + measured basis, and the runtime adds none. + - **`reflectionInterval` is refused without an enabled `longTerm`** — a reflection + writes a long-term note, so with none enabled it would do nothing. + - **`longTerm.store` is retired as a whole key.** The memory store is platform + infrastructure, not agent metadata: the runtime keeps the notes in its own + database store, and refused `vector` (the key's default, so what an omitted + `store` parsed to) and `redis`. Its old spellings `backend`, `storage` and + `provider` under `longTerm` are answered with the same prescription instead of + being steered onto `store`. + + `longTerm.enabled` is unchanged. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `memory.longTerm.store` — any value, `database` included | delete the key; where the notes are kept is the platform's choice. | + | `longTerm: { enabled: true, … }` without `maxEntries` | add `maxEntries`: how many distilled notes are kept for each user (an integer of at least 1). | + | `longTerm: { enabled: true, … }` without `memory.reflectionInterval` | add `reflectionInterval`: how many delivered interactions pass between the reflections that write a note (an integer of at least 1). | + | `memory.reflectionInterval` without `longTerm.enabled: true` | enable long-term memory with both numbers, or delete `reflectionInterval`. | + + **The one-line fix: declare `maxEntries` and `reflectionInterval` when `longTerm.enabled`; delete `store`.** + `os migrate meta --from 17` lists the mechanical edits for existing sources (the + `store` deletion); the two numbers are the author's to choose. + + Each refusal is a parse error at the key's own path, naming the key and the fix, and + `store` also fails `tsc` (its input type is `never`). + + ### The retirement kit + + - **Tombstone.** `longTerm.store` is a `retiredKey()` carrying the prescription; the + three old alias spellings moved from `aliases` to `guidance`, because an alias may + not steer an author onto a tombstone. + - **The contract check** is a refinement on `memory` (`reflectionInterval` is + `longTerm`'s sibling), one `custom` issue per missing or misplaced key. A JSON + Schema cannot state a value-conditioned requirement in the closed projection list, + so the published `ai/Agent` schema (and the four installed-package schemas that + embed agents) names the site in `x-dropped-refinements`, recorded in + `dropped-refinements.baseline.json`. + - **D2 conversion `agent-memory-long-term-store-removed`** (step 18, retired from the + load path): it deletes `store` from `memory.longTerm`, whatever it holds — the + delete is lossless, because no value of it ever chose a backend. Stored + `sys_metadata` agent rows and built artifacts replay it; one notice per agent. It + supplies neither number. + - **D3 entry `agent-memory-store-retired-and-limits-required`** carries the judgement + the conversion cannot make: the two numbers an enabled `longTerm` now requires. + - **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:memory.longTerm.store`. + - **No deprecation window**, per the project's startup-stage posture. + + ### Describes and the liveness ledger + + - `agent.memory` drops `[EXPERIMENTAL — not enforced]`: it states that the cloud AI + runtime enforces it and that the open framework edition does not run agents. + `longTerm`, `enabled`, `maxEntries` and `reflectionInterval` each state what the + runtime does with them. + - The ledger row moves `experimental` → `live`, citing the cloud reader + `agent-runtime.ts#compileAgentMemory` (via `AgentRuntime.resolveTurnGuardrails`), + the enforcement in `ai-service.ts` and the store `agent-memory.ts#AgentMemoryStore`, + as attested by the cloud seat's reading at cloud `ef5a4344`, `verifiedAt` + 2026-10-02. `os lint` / `os validate` no longer warn + `liveness-experimental-property` on an agent that sets `memory`. + - ⚠️ **The window, stated.** At `ef5a4344` the cloud reader still reads `store`: it + honours `database` only and refuses `vector` and `redis`. Cloud drops `store` in + that one reader once this release reaches its pin, and no earlier. + + ### The agent form's help texts + + - The `memory` row's help text on the agent metadata form named short-term memory, + a key the schema refuses. It now states what memory does and that `maxEntries` + and `reflectionInterval` are required once long-term memory is enabled. + - The neighbouring `planning` row named a strategy and a replan switch the schema + does not declare; it now states the one key it has, the iteration cap. + - The `platform-objects` metadata-form catalogs follow: the English leaves are + regenerated, and the `zh-CN`, `ja-JP` and `es-ES` leaves are authored, not copied. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published, and tenant-authored agents were not measured. This repo authors no + `longTerm` outside `packages/spec`, and no cloud built-in agent declares one. + + Clause-②: yes (narrowing) + + +- 48fa7a3: Provenance comments in `@objectstack/platform-objects` cite the commits that decided them, not tracker numbers that no longer resolve + + Clause-②: no + + Docblocks and comments across the package cited issue-tracker numbers that now answer 404 on GitHub. + Each now cites the commit in this repository's history that made the decision it describes, except one + that cites ADR-0104's 2026-09-05 addendum, the record of that ruling. Some of these docblocks sit on + exported members, so the reworded text appears in the published declaration files (`apps`, `identity`, + `metadata-translations` and `system` `index.d.ts` / `index.d.mts`), and the field comments esbuild keeps + appear in the JavaScript output (`index`, `apps`, `audit`, `identity` and `plugin`, `.js` / `.mjs`). + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- 36ad321: feat(spec)!: `element:text` `variant` refuses `heading` / `subheading` by name — the vocabulary is the nine `ui:text` publishes, and `os migrate meta` rewrites them to `h2` / `h3` (#21015) + + **BREAKING** — `heading` and `subheading` leave `ElementTextPropsSchema.variant` (an + `element:text` page component's `properties.variant`). This is the second release of + the ruled two-release convergence on the nine values `ui:text` publishes — `h1`-`h6`, + `body`, `caption`, `overline`. 17.5.0 added the nine and refused nothing; 17.6.0 was + the full release in which both vocabularies parsed; this release refuses the two old + spellings. A heading is a document level, not a text style: `heading` and + `subheading` named a style and left the renderer to pick the level. + + ### FROM → TO + + | removed | what to write instead | + | --- | --- | + | `variant: 'heading'` | `variant: 'h2'` — the heading element `heading` always rendered — or the level the page outline means. | + | `variant: 'subheading'` | `variant: 'h3'` — the heading element `subheading` always rendered — or the level the page outline means. | + + **The one-line fix: `heading` → `h2`, `subheading` → `h3`.** + `os migrate meta --from 17` lists the mechanical edits for existing sources. + + The rewrite keeps the heading ELEMENT (so the document outline is unchanged) but not + the size: `heading` drew in the `h3` style and `subheading` in a medium-weight small + heading style, and `h2` / `h3` draw their own, larger styles. Where the old look + mattered more than the level, pick the level whose style you want. + + Each retired spelling is refused at parse with a prescription naming the level to + write, and in `tsc` (the two members are gone from the input type). Any other unknown + value keeps zod's own message. An `element:text` with no `variant` still parses to + `body`. + + ### The retirement kit + + - **Value-level retirement.** The enum is declared through `enumWithRetiredValues` + (`shared/retired-key.ts`), with the two prescriptions module-private. No authorable + KEY and no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four + surface ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`, + `api-surface-signatures`) are byte-identical; the generated component reference + page drops the two values. + - **D2 conversion `element-text-variant-heading-levels`** (step 18, retired from the + load path): `heading` → `h2` and `subheading` → `h3` on every `element:text` page + component — regions, named slots and container nesting. Stored `sys_metadata` page + rows replay it at rehydration; one notice per rewritten block. + - **D3 entry `element-text-variant-heading-subheading-retired`** carries the judgement + the conversion cannot make: whether the rewritten level is the one the page means. + - **No further deprecation window**: 17.6.0 was the window the ruling asked for. + + ### Producers moved in this repository + + - `@objectstack/platform-objects`: the four section headings on the `sys_user` record + page's Security tab (`Password & Sign-in`, `Two-Factor Authentication`, `Email + Verification`, `Danger Zone`) move from `subheading` to `h3`. They render the same + h3 element, in the `h3` style. + - `examples/app-showcase`: the `page-variables` detail heading moves to `h3`. + + ⚠️ **The out-of-repo author population is NOT MEASURED.** `@objectstack/spec` is + published, and tenant-authored pages were not measured. In this repository the five + writers above were the only ones outside `packages/spec`. objectui at `main` authors + neither value; its `element:text` renderer, registry `inputs` enum, html tier and the + published `sdui.manifest.json` still list the two, and drop them once this release is + installable there (the objectui follow-up). + + Clause-②: no (narrowing) + + +- 1878ef9: fix(plugin-security,platform-objects): an org member reading a colleague's `sys_user` row is no longer served the identity object's `Admin` field group, directly or through the activity stream (#21237) + + Clause-②: no + + - **What a member was served.** The platform baseline `member_default` opens every org peer's `sys_user` row (the `sys_user_org_members` policy) and declared no field-level security on it. An org member reading a colleague's row was therefore served the whole `Admin` field group: the sign-in trail, the lockout state, the ban reason and expiry, the password and MFA stamps, the legacy platform role scalar and the AI-seat flag. With object-level read on `sys_activity`, the colleague's activity metadata carried the same fields, because the activity field redaction serves exactly what the data plane serves. + - **What changes.** `member_default` and `viewer_readonly` now declare the `Admin` group `readable: false` through the permission set's existing `fields` entries. The withheld set is built from the identity object's declaration, so a field the declaration adds to the group is withheld from the day it is declared. `admin_full_access` and `organization_admin` (and so `organization_admin_no_bypass`) declare the group readable and editable, the same state as a field no set names, so an administrator's reads and writes are unchanged. `member_default` is the additive baseline every authenticated user resolves, and field grants merge most-permissively, which is why the admin sets carry that keeping entry. + - **What a member sees now.** On the direct read, the list read and the activity metadata, a member is served no `Admin`-group field of a colleague's row. The directory fields (name, email, image) are still served. Field-level security does not distinguish rows, so the member's own row read through the generic data API is withheld the group too; every platform reader of those fields on a member's own row (the auth gates, the sign-in stamps, the session, the AI-seat resolution) reads under system or auth context and is unaffected. A member's query that filters or sorts on a withheld field is refused (`403 PERMISSION_DENIED`, the filter-oracle rule). A member's user-context write that names a withheld field is refused by the field-level write gate (`403 PERMISSION_DENIED`), and a payload mixing such a field with profile fields no longer lands partially. + - **The deactivation flag is directory data.** `sys_user.banned` moves from the `Admin` field group to the `Account` group in `@objectstack/platform-objects`, so members are still served it. Every user picker filters its candidates on it, and a filter on a withheld field would be refused. Its reason and expiry stay in the `Admin` group. In a record form the field now renders in the `Account` section. + + **Migration.** None for shipped apps. A custom permission set that grants an org member read on `sys_user` and is meant to show them the `Admin` group must name those fields `readable: true` in its `fields` entries. A client that filtered members' `sys_user` queries on an `Admin`-group field must drop that predicate or run it with an administrator's grant. +- 6e33b67: feat(spec)!: retire `agent.lifecycle`, the agent conversation state machine, and with it the XState `StateMachineSchema` family — a conversation phase is a skill with `triggerConditions`, orchestration is Flow, record transitions are the `state_machine` validation rule (#21320) + + **BREAKING** — `agent.lifecycle` was parsed and never read. No runtime, in this + repository or in the cloud AI runtime that executes agents, moved an agent through a + declared state or refused an undeclared transition, so an authored machine changed + nothing an agent did (ADR-0049 enforce-or-remove). Enforcing it would have meant a + statechart interpreter beside Flow, the two-engine shape ADR-0020 rejected. Authoring + now refuses the key by name, with a prescription, and TypeScript rejects it. + + Its value schema had no other authorable door: ADR-0020 had already retired the XState + shape as a record-lifecycle declaration and kept the file only for this key. So the + family leaves the package with it. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `agent.lifecycle` — any value | delete the key. | + | a conversation phase in the machine (its own instructions and tools) | a skill with its own `instructions` and `tools`, selected by its `triggerConditions`, listed in the agent's `skills`. | + | a multi-step process in the machine | a Flow. | + | a record's status transitions in the machine | a `state_machine` validation rule in the object's `validations`: `{ type: 'state_machine', field, transitions: { from: [to, …] } }`. | + | `StateMachineSchema`, `StateNodeSchema`, `TransitionSchema`, `ActionRefSchema`, `GuardRefSchema` and the types `StateMachineConfig`, `StateNode`, `StateNodeConfig`, `Transition`, `ActionRef`, `GuardRef` from `@objectstack/spec/automation` | no replacement: declare the shape your code needs itself, or drop it. For record transitions, `StateMachineValidationSchema` in `@objectstack/spec/data` is the enforced shape. | + | `StateNodeConfig` from `@objectstack/spec` or `@objectstack/spec/ai` | removed with the family; nothing in those entries mentions it any more. | + + **The one-line fix: delete `lifecycle`; put phase-scoped instructions and tools in + skills with `triggerConditions`, and orchestration in Flow.** `os migrate meta --from 17` + lists the mechanical edits for existing sources (the `lifecycle` deletion). Where each + deleted machine's intent goes is the author's judgement. + + The refusal is a parse error at `lifecycle` naming the key and the fix, and the key + fails `tsc` (its input type is `never`). + + ### The retirement kit + + - **Tombstone.** `lifecycle` is a `retiredKey()` on `AgentSchema` carrying the + prescription; the agent metadata form no longer offers it. + - **D2 conversion `agent-lifecycle-removed`** (step 18, retired from the load path): + it deletes `lifecycle` from every agent, whatever it holds. The delete is lossless, + because no value of it ever changed what an agent did. Stored `sys_metadata` agent + rows and built artifacts replay it; one notice per agent. An object's ADR-0057 + `lifecycle` block shares the name and is not touched. + - **D3 entry `agent-lifecycle-retired`** carries the judgement the conversion cannot + make: which of the three destinations each deleted machine meant. + - **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:lifecycle`, and + **`RETIRED_DEFS_BY_MAJOR[18]`** registers the five published defs + `automation/StateMachine`, `automation/StateNode`, `automation/Transition`, + `automation/ActionRef` and `automation/GuardRef`. Their reference page + (`references/automation/state-machine`) is gone. + - **No deprecation window**, per the project's startup-stage posture. + + ### The liveness ledger + + The `agent.lifecycle` row moves `experimental` → `dead` with a REMOVED note + (`verifiedAt` 2026-10-02); the tombstone keeps it in the walked shape. No `agent` row is + `experimental` any more. `os validate` and every other parsing door refuse the key at + parse, before any advisory runs. `os lint` reads the unparsed stack, so it now grades the + key `liveness-dead-property` where it used to say `liveness-experimental-property`. + + ### `@objectstack/platform-objects` + + The agent metadata-form catalogs drop the `lifecycle` row's label and help text in all + four locales. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published: tenant-authored agents, and code outside this repository importing the + family's exports, were not measured. This repository authors no `agent.lifecycle` + outside `packages/spec` and imports none of the family outside it; the pinned objectui + checkout imports none of the family and reads no `agent.lifecycle`. + + Clause-②: yes (narrowing) + + +- ca0dfb6: The agent metadata form now offers `structuredOutput`, the output contract the cloud AI runtime enforces on every final answer. It is a `composite` row in the AI Configuration section, spelled like the `memory` and `guardrails` rows: Studio derives its seven sub-rows from the served JSON Schema. + + Clause-②: no + + - Before this, the block had no row on the agent form, so the only way to author it in Studio was the Source tab. The form's reconciliation test excused that with a ledger row saying the key was declared but not enforced. The key has been enforced since the structured-output enforcement landed (liveness `live`), and that row is gone. + - What Studio renders, read in the console's metadata form renderer: `format` and `fallbackFormat` are selects over `json_object` / `json_schema`. `strict` and `retryOnValidationFailure` are switches, and `maxRetries` is a number. `transformPipeline` is a multi-select over `trim` / `parse_json` / `validate`. `schema`, the free-form JSON Schema record, is a JSON text editor: the stored value is shown as JSON and saved back as parsed. That is the same editor the action form already gives `ai.outputSchema`, which is the other slot this JSON Schema rule governs. + - Two editing limits of those controls. A multi-select toggle stores the steps in the order the enum declares them (`trim`, `parse_json`, `validate`). And the schema editor keeps the last valid JSON while the text does not parse. A value nobody edits is saved back unchanged. + - No schema, parse or export change. The accept set of `AgentSchema` is unchanged, and so is the refusal of an untyped JSON subschema at `structuredOutput.schema`. What moves is the form payload `getMetaTypes()` serves, and the two new leaves of the `platform-objects` metadata-form catalogs (the row's label and help text). Those are authored in `zh-CN`, `ja-JP` and `es-ES`, not left as copies of the English source. +- 2df3d13: Studio's object form offers `imageField`, the record's picture, as a text row beside `nameField` + + Clause-②: no + + The object form in the metadata form registry now has an `imageField` row, a plain text input placed beside `nameField`. Until now the only way to set the record picture from Studio was the Source tab's raw JSON. The help text says what the parse accepts: a field of this object whose type is `image` or `avatar`. Left empty, the object has no record picture and no placeholder is drawn. The row brings no picker and no validator of its own. A name that is not an `image` / `avatar` field of the object is refused when the object is saved, by the same parse rule as before. + + `@objectstack/platform-objects` ships the row's label and help text in its metadata-form translation catalogs, translated for `zh-CN`, `ja-JP` and `es-ES`. + + No schema key, accept set, refusal, error code or status changes, and you have nothing to re-author. +- 607463d: The organization's member, invitation and team actions are offered only to the membership grades the server admits. A plain member no longer sees "Invite User", "Change Role", "Remove Member", "Cancel Invitation", "Create Team" and the rest, each of which the server refused with 403. + + - `invite_user` (on the Users, Members and Invitations lists) and `resend_invitation`: owner, admin and delegated_admin. + - `update_member_role`, `remove_member`, `cancel_invitation`, `create_team`, `update_team`, `remove_team`, `add_team_member` and `remove_team_member`: owner and admin. + - `transfer_ownership`: the owner alone, on a non-owner row. + - `add_member` is unchanged. Its door is platform-admin standing, not a membership grade. + + Each action declares `requiresMembershipReach` from `@objectstack/spec`, which is lowered into its `visible` predicate. +- 8e35895: Studio's action form now offers `onSuccess` (the route an `api` or `script` action opens once it succeeds, and whether it opens in place or in a new tab) and `outcomeMessages` (a JSON map from each `outcome` the handler returns to the success message shown for it), with their labels and help text translated for `zh-CN`, `ja-JP` and `es-ES`. +- f76c622: Setup → Users now opens on the "All Users" list. Before this, the console opened `sys_user`'s first declared list view, "My Profile". That view is filtered to the caller with a page size of 1, so an administrator saw one row, themselves, and nothing said the rest of the organization was one tab away. + + Clause-②: no + + - The Setup app's `nav_users` entry now sets `viewName: 'all_users'`. The key is the one the spec already declares on an object navigation item, and the console honours it. No new key, no `listViews` reorder, and no view is removed. + - "My Profile" (`me`) is still a tab on the Users page. The Account app's profile entry is unchanged: it is the `account:profile_card` component, which reads the signed-in user from the session, not this list view. The `me` view's code comment no longer says the Account app surfaces it. + - ⛔ No schema, parse, export or accept-set change. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [e1790fd] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/metadata-core@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 9115b4c94ff..6c379f03d34 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "main": "dist/index.js", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index 7360b85716c..ba9ab3d8546 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/embedder-openai +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 6e89ed2fb16..f8a4f8ff8bf 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "OpenAI-compatible embedder for ObjectStack — works against OpenAI, 阿里通义 DashScope, 智谱 BigModel, 硅基流动 SiliconFlow, 火山引擎 Doubao, MiniMax, Ollama, and any drop-in OpenAI-shape endpoint.", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index 770f3d8ae8a..ab27a799670 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,123 @@ # @objectstack/knowledge-memory +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/service-knowledge@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index cde7d4f710f..369798c7ae6 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index 6839e181aca..9bac1aef77a 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,123 @@ # @objectstack/knowledge-ragflow +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/service-knowledge@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index fdf76b4df1f..99084b73e77 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "main": "dist/index.js", diff --git a/packages/plugins/organizations/CHANGELOG.md b/packages/plugins/organizations/CHANGELOG.md index c09eb1d4842..ead49479435 100644 --- a/packages/plugins/organizations/CHANGELOG.md +++ b/packages/plugins/organizations/CHANGELOG.md @@ -1,5 +1,181 @@ # @objectstack/organizations +## 17.7.0 + +### Minor Changes + +- 251a7dd: fix(organizations)!: a create that names an `organization_id` meets the Layer 0 write wall, as the update does — the insert stamp no longer rewrites it (#21666) + + Clause-②: no (narrowing) + + **BREAKING.** On a walled posture (`isolated` / `group`), the insert stamp (Middleware A) overwrote a supplied `organization_id` with the caller's active organization in every user context. A create naming another tenant's organization answered `201` and stored the row in the caller's own organization, while the PATCH naming the same organization and the array insert (`createMany`) were refused `403 PERMISSION_DENIED`. One operation answered two ways, and the caller of the `201` had no signal that its input had been replaced. + + The stamp now fills only an absent or empty `organization_id`, for every non-system context (ADR-0105 D5). A supplied value is left as sent and meets the Layer 0 write wall in `@objectstack/plugin-security` (ADR-0095 D1), which answers the create exactly as it answers the update: + + - **Another tenant's organization** → `403 PERMISSION_DENIED`, nothing stored (was `201`, stored in the active organization). This holds for a member and for a platform administrator on a tenant object. A member's forged `organization_id` stays refused; the wall refuses it now, where the stamp used to rewrite it. + - **No organization** → stamped with the active organization, as before. + - **The caller's own active organization** → admitted, as before. + - **Under `group`, a sister organization the caller holds** → admitted and stored in that organization, the same place the PATCH already moves a row to (was `201`, stored in the active organization). Where `organization_id` is the platform-injected column, the engine still strips it from a non-system payload as `readonly` and reports it in `droppedFields`, on the create as on the update. + - **A platform administrator on a posture-permitting object** (`private`, platform-global, better-auth-managed) is exempt from the wall on the create as on the update. + + Every door that writes one row at a time under the caller's context gives the same answer: `POST /data/:object`, the `create` operation of `POST /batch`, the clone route and the import runner's per-row fallback. Two of these change in ways worth knowing: + + - An import row naming another tenant's organization is now reported as a failed row (`PERMISSION_DENIED`). Before, it was created in the active organization. + - The clone route copies an `organization_id` that the object declares itself. So under `group`, a clone of a sister-organization row now lands beside its source instead of in the active organization. + + System contexts are unchanged. The per-organization seed replay, the orphan claim, migrations and every other `isSystem` writer meet neither the stamp nor the wall. The `single` posture is unchanged too, because `objectstack serve` mounts this package only under a walled posture. + + **What to do.** On create, either omit `organization_id` or name your active organization. If a platform operator needs a row in another organization, write it with a system-context write. + + + +### Patch Changes + +- 149153c: Membership under the `auto` policy is settled when the user is created, per ADR-0093 D7. + + Clause-②: yes (widening) + + - **At creation.** A user created under `auto` is bound to the default organization at creation, and the first session of that creating request carries it. Membership is not decided again when the user signs in later. + - **One-time backfill.** The ADR-0093 D6 backfill of pre-existing users runs once per deployment, and once per process even if its record cannot be written. Its verdict is recorded in the `sys_migration` ledger with id `adr-0093-membership-backfill`. A pass on a deployment with no organization at all records nothing, and the backfill runs again once the default organization is created. If the ledger is missing or cannot be read, the pass does not run and logs a warning. If the record cannot be written, that is logged as an error. `OS_SKIP_MEMBERSHIP_BACKFILL=1` still disables the pass. + - **Default organization owner.** The platform admin is bound as owner of the default organization once, by the bootstrap that first decides it, in both the single-org and the walled organizations wiring. The decision is recorded in the same ledger with id `adr-0093-default-org-owner-bind` and held for the rest of the process even if the record cannot be written. After that, a missing default organization is recreated without binding anyone. To recover, an administrator re-adds members, including themselves, through member management. On a kernel without the ledger, the owner is bound only when the bootstrap creates the default organization. If the ledger exists but cannot be read, that call binds nobody and the next trigger decides. + - **Full scan.** The backfill reads the user and membership tables page by page with no row cap. A scan that cannot read either table in full binds nobody and records nothing. With organizations present but no default target, as in multi-organization deployments, the refusal is recorded. + - **Upgrade.** The first boot of an upgraded deployment runs the backfill once. + - **Unchanged.** `invite-only` binds nobody. Multi-organization deployments get no automatic binding. Users created through sign-up, admin create-user, import or SSO are bound under `auto` as before. + - **Narrowed.** A `sys_user` row inserted straight through the data engine never passes through user creation. Once the backfill is recorded, a later `app:seeded` pass leaves it unbound. That includes users written by a seed that finishes after its inline budget. Code that inserts users this way must write their membership itself; the showcase approval-demo personas now do. + - **`keysetWalk` (`@objectstack/types`).** The walk now decides that a page did not advance only when it gets back the same cursor key or the same page again. It no longer compares keys in JavaScript string order, which disagrees with database collations and could report a healthy walk as truncated. + - **New public surface of `@objectstack/plugin-auth` (additive).** + - `createEnsureDefaultOrganizationOnce` and `EnsureDefaultOrganizationOnceOptions` are the gated bootstrap both wirings call. + - `ObjectQLAdapterFactoryOptions` adds `onRecordCreated`, passed as the new optional second argument of `createObjectQLAdapterFactory`. + - `EnsureDefaultOrganizationOptions` gains `bindOnlyOnCreate` and `bindOwner`. + - `EnsureDefaultOrganizationResult.reason` gains `'owner_bind_decided'`. + - `BackfillMembershipsResult.reason` gains `'scan-incomplete'`. + - Code that switches exhaustively over those reasons sees one more member. + - **`backfillMemberships` (exported) changed behaviour.** Its `limit` option used to cap the rows scanned (default 5000); it is now the page size of a full scan with no cap. The function now needs a reader that can page by `id`; a reader that cannot gets `scan-incomplete` and binds nobody, where it used to bind. A direct call is not gated by the one-time ledger and decides membership again on every call; call it through the one-time pass instead. + - **Policy switch.** Once a pass under `invite-only` is recorded, switching the policy to `auto` later does not backfill the users who existed then; they get membership through invitation or member management. + - **Deprecated, not removed.** The ungated `ensureDefaultOrganization`, both plugin-auth's helper and the `@objectstack/organizations` wrapper, is `@deprecated` in favour of `createEnsureDefaultOrganizationOnce`. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [1c3a4d9] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [41a1135] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/plugin-auth@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/organizations/package.json b/packages/plugins/organizations/package.json index 4bfa1fde2c2..40010e257bf 100644 --- a/packages/plugins/organizations/package.json +++ b/packages/plugins/organizations/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/organizations", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Multi-organization runtime for ObjectStack — registers the `org-scoping` service that turns single-database row-level Organization isolation on: `organization_id` auto-stamp on insert, per-org seed replay, default-organization bootstrap, and the walled-posture membership-policy gate.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index 6680c4a7ee9..f39d76519f9 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,317 @@ # @objectstack/plugin-approvals +## 17.7.0 + +### Minor Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- c9c555a: fix(plugin-approvals)!: `role:` is no longer a position address, and the deprecated `role` approver type stops writing `role:` slots (ADR-0090 D3) + + Clause-②: no (narrowing) + + `position:` is now the one spelling of a position address. ADR-0090 D3 retired the word `role` with no alias window; the approvals service still read `role:` as a second spelling of the same position everywhere it compares a slot with the caller ("My Pending", the participant gate, `viewer.can_act`, and the slot test of every decision). The stock console now sends `position:`, so that arm is gone. + + **FROM → TO.** FROM `role:` → TO `position:`, wherever a caller names a position: the `approverId` filter of `GET /api/v1/approvals/requests`, and the `actorId` of approve, reject, send back, reassign, request info and comment. A `role:` ask now matches only a slot stored under that exact spelling, and a `role:` actor is refused with 403 `FORBIDDEN` ("cannot act as …"). + + **The writer.** An approver authored with the deprecated type `{ type: 'role', value: … }` already resolved as `org_membership_level` (the org-membership tier: owner, admin, member). When that lookup found no one, the request's fallback slot kept the authored spelling, `role:`, and a holder of a position with the same name decided it through the `role:` arm. That fallback now writes the canonical `org_membership_level:`, so no path writes a `role:` slot. A stored slot is never rewritten. + + Two classes of pending request are now decided only by an admin override: + + - a request a 15.x-era release opened, whose slot is stored as `role:`; + - a new request opened from a flow that still authors `{ type: 'role', value: '' }` and whose membership-tier lookup finds no one (its slot is `org_membership_level:`). + + **Author's one-line fix:** write `{ type: 'position', value: '' }`. `os lint` already reports the old form as `approval-approver-not-membership-tier` or `approval-approver-type-deprecated`. + + **Admin's one-line handling, both classes:** a platform admin (`admin_full_access`) or a tenant admin of the request's organization approves or rejects it (`POST /api/v1/approvals/requests/:id/approve` or `/reject`; recorded with `via_override: true`, and the flow run resumes), or reassigns it to the position's holder (`POST /api/v1/approvals/requests/:id/reassign` with `{ "to": "" }`), who then decides it normally. + + + +### Patch Changes + +- f9bcd08: Datasource and approval refusals, warnings, field help and generated-draft comments no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these two packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/service-datasource`: the credential-migration refusal says an unbindable key is either an alias spelling from before inline credentials were refused at publish, which no connection builder reads, or turso's `encryptionKey`, which has no secret slot of its own because the one slot carries the `authToken`; the remote-primary-key comment in a generated object draft says a driver's introspection can report only the first column of a composite key, so the list is a lower bound. + - `@objectstack/plugin-approvals`: the `queue` approver warning says the platform has no ownership queue to expand the type from, that the type is no longer offered for authoring, and to route the step to a team, department or position instead; the live-record warnings say approvers are being resolved against the trigger snapshot instead of the live record they are normally resolved from; the recall refusal's log line names the admin override; the `sys_approval_action` `via_override` help (in every shipped locale) says a platform or organization admin may act on any pending request, so that one nobody in its slate can decide never stays stuck; the cross-organization team, team-member and manager warnings, the expanded-to-nobody warning, the revise-window refusal, the `attachments` help and the `sys_approval_delegation` description drop their citations. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 6d487d2: The approvals inbox's "My Pending" now lists a request routed to a position for the users who hold that position, whichever spelling of the position address the client asks for + + Clause-②: no + + A request whose approver position nobody held when it opened keeps the literal `position:` slot. A user staffed into that position afterwards could already decide it, by naming `position:` as the actor. `resolveActor` admits a holder under `position:` and under `role:` (the deprecated pre-rename spelling) as the caller's own identity, but the decision's slot test is literal: on that slot, `role:` or no actor at all answers 403. The list read did not agree with either half. + + - `GET /api/v1/approvals/requests?approverId=…` matched each value literally. The stock console sends `role:` for every position the session carries, so the request never appeared in "My Pending". A position address now matches under both spellings `resolveActor` admits a holder under, and no others. A `team:`, `org_membership_level:` or bare-name value still matches only itself. + - The participant gate behind every approvals read counted a "current approver" by user id alone. A holder of the position who neither submitted the request nor holds admin standing got an empty list under both spellings and a `404` on `GET /api/v1/approvals/requests/:id`, though their approve call naming `position:` succeeded. The gate now also counts the slot addresses of every position on the caller's server-resolved context. A request becomes visible only to someone who can decide it. + - The decision routes are unchanged. They admit exactly the identities they admitted before, and a pin compares them against the previous predicate. + + A caller who sent the stored `position:` spelling and was already the submitter or an admin sees no change. +- 5e58193: A holder of a position whose approval slot reads `position:` now decides it from the stock console, sees `can_act` on it, and keeps sight of it after deciding; a reviewer named by a `user` approver authored as an email does too + + Clause-②: no + + A request whose approver position nobody held when it opened keeps the literal `position:` slot. After the position is staffed, its holder found the request in "My Pending", but `viewer.can_act` was `false`, an approve with no `actorId` (what the console's approve action sends) or with `role:` (the deprecated pre-rename spelling the console uses) answered 403, only naming `position:` decided it, and `GET /api/v1/approvals/requests/:id` then answered 404 to the holder who had just decided it. A `user` approver authored as an email had the same shape: its reviewer saw neither the request nor `can_act`, and only naming the email decided it. + + Every place the approvals service compares a slot with the caller now reads the caller's acting addresses, the set its decision routes already admitted: the user id, the email the caller's own account carries, and both spellings of each position on the caller's server-resolved context. + + - **Decisions** (approve, reject, send back, reassign, request info, comment): with no `actorId`, the caller takes the first pending slot keyed by one of those addresses, their user id first. A named `role:` or `position:` takes that position's slot under either spelling. Nobody new may decide: a user who holds another position is still refused with 403. + - **What is recorded:** `sys_approval_action.actor_id` holds the slot the action took, in that slot's stored spelling. That is what naming the slot always recorded, and the multi-approver tally counts approvals by matching it against the slate. + - **`viewer.can_act`** is computed by the same slot test the decision routes run with no `actorId`, so it is `true` exactly when such an approve would be admitted as a slot holder. + - **Visibility:** the participant gate counts a current approver by the email half too. "Already acted" is counted by the same addresses, so a request decided under `position:` stays visible to whoever holds that position. + + An admin who holds the routed position now decides it as a slot holder (`via_override: false`, one vote in a multi-approver tally), exactly as when they named the slot; an admin who holds no slot is unchanged. +- 6f17d1d: An approval action now records the user who took it in `sys_approval_action.actor_id`, and the pending-approver slot it was taken as in a new `acted_as` column; rows stored before this move their slot out of `actor_id` at the next boot + + Clause-②: no + + `actor_id` is a lookup to `sys_user`, so under ADR-0118 D1 it holds a user id or nothing. A slot-gated action used to record the slot it took there instead: a `position:` literal for a position staffed after the request opened, or an email for a `user` approver authored as one. On those decisions no record named the person who decided. The audit ledger and activity rows the write produces carry no user, so the attribution was lost, and every join or report on the lookup silently dropped the row. + + **This supersedes the "What is recorded" sentence of the unreleased `21379-position-address-readers` changeset**, which says `actor_id` holds the slot. From this release it holds the person. + + - **What is recorded.** + - `actor_id` is the user the request's context vouches for: the signed-in caller, whatever address they named. + - `acted_as` is the slot the action took, in the slot's stored spelling (a user id, an email, or `position:`). It is empty on actions no slot admitted: the submitter's own actions, system actions, and an admin override, which `via_override` still marks. + - An emailed action link records the one account that carries the token's email. If no account carries it, the link records no person. + - The SLA sweep keeps its reserved `system:sla` actor for now. + - **What reads it.** + - The multi-approver tally and `decision_progress` count `acted_as`. + - A participant who already acted keeps sight of a request by either of two facts: `actor_id` is their user id, or `acted_as` is a slot they act under (so a decision taken as `position:` stays visible to that position's holders). + - Nothing compares a slot with `actor_id` any more. + - The action log (`GET /api/v1/approvals/requests/:id/actions`, `listActions`) returns `acted_as` beside `actor_id` and `actor_name`, filling the `ApprovalActionRow.acted_as` member `@objectstack/spec` declares. It is omitted when the action took no slot, or when no stored record kept the slot. + - **Stored rows.** A repair runs on every boot and is idempotent. + - Pass 1: a row whose `actor_id` still holds a slot address gets `acted_as` set to it and `actor_id` cleared. No stored record names who decided it, so it shows the slot and no person. + - Pass 2: the approve votes a still-pending request's tally counts get their `acted_as`, so in-flight `unanimous`, `quorum` and `per_group` requests keep the approvals they already collected. + - A failure is logged at error level and retried at the next boot. + - **For a report or integration that read `actor_id` as the slot:** read `acted_as` instead. `actor_id` now always joins to `sys_user`. +- 88fb5e8: Every `sys_user` lookup the approvals plugin writes now holds a user id or nothing: the SLA and dead-run sweeps record no actor instead of a `system:` placeholder, notifications name only the person who acted, and `reassign_from` / `reassign_to` become slot-address text columns; stored placeholders are cleared at the next boot + + Clause-②: no + + Under ADR-0118 D1 a lookup to `sys_user` holds a user id or null, never a placeholder value. Four writers broke that, and a lookup holding a non-id drops the row from every join and report on it, silently. + + **This supersedes the "The SLA sweep keeps its reserved `system:sla` actor for now" sentence of the unreleased `21411-approval-actor-person` changeset.** Both ship in one release; from it, the sweep records no actor. + + - **Machine actors record no actor.** + - The SLA sweep's `escalate` row, and the `approve` / `reject` an `auto_approve` / `auto_reject` escalation then records, have `actor_id` empty. Before, both held `system:sla`. The `escalate` row's comment still names the configured action. + - The dead-run sweep's `recall` row has `actor_id` empty. Before, it held `system:dead-run`. Its comment still names the dead run and its status, and a submitter's own recall still records the submitter. + - **Notifications name only a person.** The actor the plugin hands to `sys_notification.actor_id` (and so to each `sys_inbox_message.actor_id`) is the user the action's context vouches for, or nothing. + - Before, a reassign, reminder, request for information, comment or send-back taken under a named position or email forwarded that address as the actor. + - Before, every SLA notification forwarded `system:sla`. It now forwards no actor, as the out-of-office notifications already did. + - **`reassign_from` / `reassign_to` are slot addresses.** A reassignment moves a pending-approver slot, so both columns hold the slot's address in its stored spelling: a user id, an email, or `position:`. They are now text columns (max 255 characters, like `acted_as`) instead of `sys_user` lookups, which matches what they already stored. + - Existing values need no rewrite, and an existing database keeps its columns as they are. On SQLite and PostgreSQL 16, booting the new declaration over a table created by the old one issues no DDL, keeps every stored value, and reports no schema drift for either column. A new database creates them as `text`, as it does `acted_as`. + - The action log still resolves `reassign_from_name` / `reassign_to_name` where an address names an account: a user id, or an email an account carries. A position address has no name. + - **Stored rows.** The boot-time repair that moves slot literals out of `actor_id` now also clears `system:sla` and `system:dead-run` from it, in the same pass and in the same idempotent way. A cleared sentinel gets no `acted_as`, because a sweep takes no slot. The boot log line reports the count as `sentinelsCleared`. + - **For a report or integration that read these values:** + - To find the SLA sweep's actions, read the `escalate` rows, and the decision that directly follows an `escalate` row whose comment names `auto_approve` or `auto_reject`. Do not test `actor_id` for `system:sla`. + - To find a dead-run release, read the `recall` row whose comment names the run. Do not test `actor_id` for `system:dead-run`. + - Read `reassign_from` / `reassign_to` as slot addresses. Do not expand them as `sys_user` references. +- 255a777: Approval notifications reach their recipient with their text (#21847). The approvals service put each notification's text in `payload.message`. The messaging service builds the delivered notification from `payload.title` and `payload.body`, the fields its `EmitInput` documents, and no channel reads `message`. So `GET /api/v1/notifications` served every approval notification as a title over an empty `body`, and the inbox row's `body_md` was empty too. The lost texts were comments, request-info questions, send-back notes, reassignments, reminders, escalations, SLA breaches and out-of-office substitutions. Every one of them now travels in `payload.body`. + + Clause-②: no + + - The texts are unchanged. Only the field they travel in moved. Who is notified, and when, is unchanged. + - Approval notifications no longer carry `payload.message`. Nothing in the platform or the console read it. A tenant-authored `sys_notification_template` for an `approval.*` topic that wrote `{{ message }}` should write `{{ body }}`. + - The service's notify helper now declares its payload: `title`, `body`, `actionUrl`, and a reminder's `actions`. A call site that spells the text any other way no longer compiles. + - `@objectstack/service-messaging` is unchanged. There is one field, as documented, and no alias for the old one. +- 568dc0b: Record-change payloads apply the same credential mask and internal-field omission as write responses. + + Clause-②: yes (widening) + + - **`data.record.created` / `data.record.updated` events.** The engine projects the event's `after` and `changes` bodies through `omitInternalFieldsFromWriteResponse` (`@objectstack/core`), the helper every external write response already uses: credential-class fields (`secret`, and `password` outside the exempt `managedBy` buckets) carry `SECRET_MASK` (or `null` when unset), and `internal: true` fields are omitted. The engine's own write result is unchanged, so a privileged in-process caller that reads the stored value back off `insert` / `update` still sees it. + - **Approval request snapshot.** The record snapshot an approval request stores (`payload_json`) applies the same rule when the request is opened. + - **Outbound webhook body.** The delivered body, and the delivery row that stores it, apply the same rule to `before`, `after` and `changes`. + - **Knowledge index documents.** `recordToDocument` takes the object definition as an optional fourth argument and skips credential-class and `internal` fields, under `'*'` and when a source names one explicitly. `KnowledgeService` passes the definition from the bound engine. + - **New public surface of `@objectstack/service-knowledge` (additive):** `recordToDocument` accepts the object definition as an optional fourth argument; existing three-argument calls behave as before. + - **Receivers see masked values.** Webhook receivers and realtime clients now get `SECRET_MASK` (or `null` when unset) for credential-class fields and no key for `internal` fields. + - **Existing rows are not rewritten.** Approval snapshots, webhook delivery rows and knowledge documents written before this change keep their stored bodies; reindexing a knowledge source refreshes its documents. + - The audit trail already masked these fields and is unchanged. No other accept set or public schema changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 2062b301cbf..cee65a289b5 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index cff9b56cace..90a20b8ffa6 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,309 @@ # @objectstack/plugin-audit +## 17.7.0 + +### Minor Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- 713b0fa: fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **Three things change for callers and operators.** + + 1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". + 2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. + 3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. + + **What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. +- 7ebb543: feat(spec,plugin-audit): the compliance ledger's audit capability, `view_all_audit_log`, exempts its holder from the ledger's parent-record read gate; platform administrators hold it by default (#21260) + + Clause-②: yes (widening) + + - **The capability.** `PLATFORM_CAPABILITIES` (`@objectstack/spec/security`) gains `view_all_audit_log` ("View All Audit Log", `scope: 'org'`). It is seeded into `sys_capability` like every other curated capability, and a permission set grants it through `systemPermissions`. It is a platform capability, so an app that declares a capability of the same name cannot bind a set carrying it to the `everyone` or `guest` anchor. + - **Who holds it.** `ADMIN_FULL_ACCESS_CAPABILITIES` (`@objectstack/spec`) now lists it, so platform administrators hold it by default: through the `admin_full_access` grant, and through the envelope a configured platform owner resolves to. No other shipped permission set carries it. Any other position holds it only through a permission set that grants it. + - **What it does.** A read of `sys_audit_log` keeps only the rows whose parent record the caller can read. The holder skips that gate and is served every ledger row its grant on `sys_audit_log` reaches: rows about deleted records, sign-out rows, sign-in rows whose session has ended, and rows about records it cannot open. A broad read is served whole. The gate's 2,000-row pre-scan does not run for a holder, so the read is not cut off at that bound. + - **What still applies to the holder.** The holder still needs object-level read on `sys_audit_log`. The field-level redaction still narrows every before/after snapshot it is served. Under a walled tenancy posture, the tenant wall still keeps the holder to its own organization's rows, which is why the capability is declared `org`. + - **What it does not touch.** The activity stream (`sys_activity`) keeps its own parent-record gate for every caller, holders included. A non-holder's ledger reads are unchanged. + + **Migration.** None: no metadata, code or configuration change is needed. Platform administrators get the deletion and sign-out trail back with no action. To give an auditor the trail, grant `view_all_audit_log` through `systemPermissions` in a permission set that also grants read on `sys_audit_log`. + +### Patch Changes + +- cc07862: Automation refusals, prescriptions, log lines and run-object field help, and the activity type help, no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these two packages show to flow authors, operators and administrators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/service-automation`: the refusal for a `fieldValues` write map says a runtime alias for it was rejected by design, so the node keeps one strict `fields` key; the refusal for a screen field's `visibleIf` says a predicate under any other key is never read, so the field always shows, and a `required` field meant to stay hidden then blocks the screen from ever being submitted; the undeclared-config-key refusal says the built-in node types were reconciled so that every key their executors read is declared; the unknown-function error in a flow value expression says such a name is refused rather than evaluated to null, which would write the field as undefined; the inert-connector warning says entries without a `provider` are catalog descriptors, while an entry that names a `provider` is a connector instance that provider's installed executor materializes; the `sys_automation_run` field help says the paused node's type decides who may continue a run (an approval pause only through its owning service), that rows written before run history recorded its trigger were not backfilled, and that a finished run's bounded step log keeps its per-node detail across a restart; three bridge debug lines say what each bridge provides. The bulk-intent guidance, the degraded-connector dispatch error and retry lines, the user-less `runAs` warning and refusal, the unclaimed-branch warning, the script-function and node-config refusals and the `sys_flow_dispatch` description drop their citations. + - `@objectstack/plugin-audit`: the `sys_activity` `type` help, whose English text all four shipped locale bundles carry, says the vocabulary is open by decision, not a gap awaiting enforcement. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 4916168: Sharing refusals and log lines, and the audit write-failure line, no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these two packages show to administrators and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/plugin-sharing`: the orphan-sweep line for record shares says every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it; the same line for share links says a share link is a bearer token, so a reused record id must not inherit it; the write-gate failure line says a failed lookup is a refusal, never an abstention, because an abstention would hand the row to the other write authorities, which may admit it; the authored-row-write probe line says only an app-authored row-level policy that positively admits the row may lift the sharing refusal; the hierarchy-scope line says the resolver contract makes a resolver fail closed on a missing organization. The two sharing-rule refusals (no active organization; deleting a platform-global rule) drop their citations, since each sentence already says why. The `OrphanSweepSubject.issue` member's doc comment now says the member carries that reason in words. + - `@objectstack/plugin-audit`: the missing-table fix in the audit write-failure line says that on a fresh `os dev` boot the table exists in the sibling telemetry file and not in the primary one, so look there before concluding it was never created. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 69a12a0: The `Audit write FAILED` line names the table whose insert was refused and the row that is lost, gives a missing table the two causes the evidence cannot tell apart, and says it is printed once per audited object, refused table and error code + + Clause-②: no + + The record writer stores the `sys_audit_log` row that records who did it, then, when activities are enabled and the write has one, its `sys_activity` timeline row. When either insert was refused, the line always said the `sys_audit_log` row never landed. When the refused insert was `sys_activity`, every ledger row had in fact landed. + + - The line now opens `Audit write FAILED on TABLE` and names the table the writer had in flight when it threw. A refused `sys_activity` insert says the ledger row landed and only the activity row is lost. A refused `sys_audit_log` insert says the ledger row is lost, and so is the activity row due after it when the object writes one. + - A missing table no longer gets only the telemetry-datasource split as its remedy. The table may never have been created because schema sync's DDL for it was refused at boot. The line cannot tell the two causes apart, so it names both, in order: look for `Schema sync FAILED for object 'TABLE'` in the boot log first, then the split and `OS_TELEMETRY_DB=0`. Any other cause keeps the driver-fault remedy. + - Whether the table is missing is asked about the refused table first. An error code that means "missing" without a phrase naming a relation is now attributed to that table, not to `sys_audit_log` by list order. + - The line is printed once per audited object, refused table and error code, and it now says so in place of "reported ONCE". The refused table joins the key, so the other table refusing with the same code on the same object gets its own line. The same missing table still prints one line per audited object that writes through it. Repeats stay at `debug`, which now also carries the `table`. + + Log text and log metadata only: no status, error code, route, row or control flow changes. A log filter that matches the old text (`Audit write FAILED (`, `reported ONCE`) needs the new spelling. +- 3bddd4a: fix(plugin-audit): an activity row recording an update whose every changed field the reader is withheld is no longer served to that reader, on any listing face + + Clause-②: no + + A `sys_activity` row's recorded change (`metadata.old` / `metadata.new`) is narrowed key by key for each reader, through the security service's served-fields answer. An update whose every changed field the reader is withheld still reached that reader as a row with an empty change, and its summary, actor and timestamp said that the record changed, and when. An org member holding object-level `sys_activity` read was served that row for each sign-in stamp on a colleague's identity record (`last_login_at`), and for each failed-sign-in counter bump, lockout, password-change stamp and MFA-required stamp. + + Such a row is now withheld from that reader as a row: + + - **What counts as one.** An update row (its stored change has both an `old` and a `new` side) whose stored change had at least one key, where the reader is served none of those keys. The keys are read from the STORED change, not the redacted one. + - **What is unaffected.** A create or a delete keeps its row. A row whose stored change is empty on both sides (an update that touched only `internal` fields) is unaffected. A mixed update keeps its row, with the served keys only. A reader served every field (an administrator) still reads every row with its change, within the pre-scan's bound. A system-context read is not narrowed. + - **Every face agrees.** The rule is a WHERE built from a system-context pre-scan on `find`, `findOne`, `count` and `aggregate`. So a list's `total`, its pages, a by-id read (`404`) and a grouped count agree with the rows served. A pre-scan that reaches its 2,000-row bound answers a broad read from the rows it judged, for every reader, administrators included, and logs a warning. The remedy is to scope the query by `object_name` and `record_id`. + + No migration: no key, export or config changes. A reader the security service gives no answer for (no security plugin wired) is not narrowed, as before. +- 50b5e03: A write refusal on an attachment or a comment no longer names a parent record the caller cannot read (#21755). + + Clause-②: no + + - **What changed.** The attachment gate (`sys_attachment`, `@objectstack/service-storage`) and the comment gate (`sys_comment`, `@objectstack/plugin-audit`) refuse an update or a delete by a caller who neither wrote the row nor can edit its parent record. That refusal names the parent record. A caller who cannot read the parent now gets the platform's not-visible refusal instead. This is the answer the row-level write check gives the principals it covers: `PERMISSION_DENIED` (403), with the same localized `record_access_denied` sentence. It names neither the parent nor the row's link to it, in the message or in the envelope. + - **What did not change.** A caller who can read the parent but may not edit it keeps the named refusal: `ATTACHMENT_DELETE_DENIED` for an attachment delete, and `RECORD_NOT_ACCESSIBLE` for an attachment update and for a comment update or delete. Who may update or delete is unchanged. + - **A comment whose thread names no record** is read by nobody, so a non-author's write on it now gets the not-visible refusal too, and the thread value is not echoed back. + - **Localization.** `installAttachmentAccessHooks` and `installCommentAccessHooks` accept an optional fourth argument: a lazily resolved i18n lookup. With it, the sentence honours a deployment's `errors.record_access_denied` override, as the row-level write check's sentence does. Without it, the built-in catalog still renders the caller's locale. +- b238856: An activity row names its record by the record's title as every renderer resolves it, not by a guess from a fixed list of field names + + Clause-②: no + + The record-change mirror writes a label for the record into each `sys_activity` row (`record_label`, and inside the created, deleted and generic updated summary). It used to pick that label from a fixed list of field names (`name`, `subject`, `title`, `full_name`, `label`, `first_name`, `company`, `email`) and fall back to the record id. An object titled by any other field showed its record id on every activity row: an object titled by `company_name` read `Created Customer "RECORD_ID"`, with the raw record id, while its record page showed the company name. + + The label is now the value of the object's title field as ADR-0079 resolves it (`nameField`, then the deprecated `displayNameField`, then the same derivation the record page, the picker and the approvals inbox use). The record id stays the floor: when nothing resolves, when the title field is the primary key, a credential or a field declared `internal`, or when the record's title value is empty. An empty title no longer borrows another populated field. + + - Objects titled by `name`, `title` or `subject` are labelled as before. + - An object whose `nameField` names another field is now labelled by that field. An object whose only list match was not its resolved title is labelled by its title now; in the bundled examples that moves the CRM contact from `full_name` (a formula that declares no `returnType: 'text'`, so it is not derived as the title) to `first_name`, its registered title. + - The activity row records the resolved field as the label's source, so the read-side redaction keeps serving the label only to a reader served that field. + + Rows written before this change keep the label they were written with; nothing is backfilled. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [6ec54f0] +- Updated dependencies [a1ca156] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [be55fd2] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [fe10172] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [13a22d0] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 0fd6af3be38..31ddb803e23 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "main": "dist/index.js", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index 024e2ca8c86..969f02f2e84 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,284 @@ # Changelog +## 17.7.0 + +### Minor Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- 149153c: Membership under the `auto` policy is settled when the user is created, per ADR-0093 D7. + + Clause-②: yes (widening) + + - **At creation.** A user created under `auto` is bound to the default organization at creation, and the first session of that creating request carries it. Membership is not decided again when the user signs in later. + - **One-time backfill.** The ADR-0093 D6 backfill of pre-existing users runs once per deployment, and once per process even if its record cannot be written. Its verdict is recorded in the `sys_migration` ledger with id `adr-0093-membership-backfill`. A pass on a deployment with no organization at all records nothing, and the backfill runs again once the default organization is created. If the ledger is missing or cannot be read, the pass does not run and logs a warning. If the record cannot be written, that is logged as an error. `OS_SKIP_MEMBERSHIP_BACKFILL=1` still disables the pass. + - **Default organization owner.** The platform admin is bound as owner of the default organization once, by the bootstrap that first decides it, in both the single-org and the walled organizations wiring. The decision is recorded in the same ledger with id `adr-0093-default-org-owner-bind` and held for the rest of the process even if the record cannot be written. After that, a missing default organization is recreated without binding anyone. To recover, an administrator re-adds members, including themselves, through member management. On a kernel without the ledger, the owner is bound only when the bootstrap creates the default organization. If the ledger exists but cannot be read, that call binds nobody and the next trigger decides. + - **Full scan.** The backfill reads the user and membership tables page by page with no row cap. A scan that cannot read either table in full binds nobody and records nothing. With organizations present but no default target, as in multi-organization deployments, the refusal is recorded. + - **Upgrade.** The first boot of an upgraded deployment runs the backfill once. + - **Unchanged.** `invite-only` binds nobody. Multi-organization deployments get no automatic binding. Users created through sign-up, admin create-user, import or SSO are bound under `auto` as before. + - **Narrowed.** A `sys_user` row inserted straight through the data engine never passes through user creation. Once the backfill is recorded, a later `app:seeded` pass leaves it unbound. That includes users written by a seed that finishes after its inline budget. Code that inserts users this way must write their membership itself; the showcase approval-demo personas now do. + - **`keysetWalk` (`@objectstack/types`).** The walk now decides that a page did not advance only when it gets back the same cursor key or the same page again. It no longer compares keys in JavaScript string order, which disagrees with database collations and could report a healthy walk as truncated. + - **New public surface of `@objectstack/plugin-auth` (additive).** + - `createEnsureDefaultOrganizationOnce` and `EnsureDefaultOrganizationOnceOptions` are the gated bootstrap both wirings call. + - `ObjectQLAdapterFactoryOptions` adds `onRecordCreated`, passed as the new optional second argument of `createObjectQLAdapterFactory`. + - `EnsureDefaultOrganizationOptions` gains `bindOnlyOnCreate` and `bindOwner`. + - `EnsureDefaultOrganizationResult.reason` gains `'owner_bind_decided'`. + - `BackfillMembershipsResult.reason` gains `'scan-incomplete'`. + - Code that switches exhaustively over those reasons sees one more member. + - **`backfillMemberships` (exported) changed behaviour.** Its `limit` option used to cap the rows scanned (default 5000); it is now the page size of a full scan with no cap. The function now needs a reader that can page by `id`; a reader that cannot gets `scan-incomplete` and binds nobody, where it used to bind. A direct call is not gated by the one-time ledger and decides membership again on every call; call it through the one-time pass instead. + - **Policy switch.** Once a pass under `invite-only` is recorded, switching the policy to `auto` later does not backfill the users who existed then; they get membership through invitation or member management. + - **Deprecated, not removed.** The ungated `ensureDefaultOrganization`, both plugin-auth's helper and the `@objectstack/organizations` wrapper, is `@deprecated` in favour of `createEnsureDefaultOrganizationOnce`. +- 41a1135: fix(plugin-auth)!: implicit account linking on external sign-in requires the library's standard local-ownership condition; the platform identity provider keeps its documented exception; an unlink is honoured + + Clause-②: no (narrowing) + + + + **BREAKING for deployments that relied on external sign-in (OAuth, OIDC, SSO) linking implicitly to a local user whose email is not verified.** + + **What changed.** + + - An external sign-in links implicitly to an existing local user only when that local user's email is verified. Otherwise the sign-in is refused with `error=account_not_linked`, the same code better-auth's own refusal produces. No link is written and the local user stays unverified. A verified local user links as before. + - The platform's own identity provider (`objectstack-cloud`) keeps its documented exception and still links to an unverified local user, because it seeds the environment owner's row without a mailbox round-trip. + - After a user unlinks a provider, an implicit sign-in through it no longer links the identity again, for any provider. An explicit, signed-in link from account settings (`/link-social`) is still allowed and ends the refusal. If the unlink cannot be recorded, the unlink itself is refused and the provider stays linked. Deleting a user removes the user's unlink records. + - A deployment that passes `secondaryStorage` to the auth plugin now also keeps verification values in the database (`verification.storeInDatabase: true`). The cache still fronts them. This keeps the unlink records durable when the cache evicts entries. Deployments without `secondaryStorage` are unchanged. + - `account.accountLinking.requireLocalEmailVerified` now reads as follows. Unset (the default) means the rules above. `true` applies the strict check to every provider, including `objectstack-cloud`. `false` turns off only the local-verification check and keeps the unlink rule. + + **What to do after upgrading.** + + - A user refused this way signs in with their existing method, then links the provider from account settings, or verifies their email first. + - To let unverified local users link implicitly again, set `account.accountLinking.requireLocalEmailVerified: false`. Before you do, read the library's warning about account takeover. + - If you pass `secondaryStorage`: verification values written to the cache alone before the upgrade (password-reset links, one-time codes, magic links and email-verification links that were in flight at deploy time) can no longer be consumed afterwards. Users who hit this request a fresh link or code once. +- 80f9f7e: fix(runtime, plugin-auth)!: two access guards refuse, instead of admitting, when their own read cannot answer + + Clause-②: no (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention: a request that one of these two guards let through only because the guard's own read faulted is now refused. Nothing an author or caller writes changes shape. + + - **`@objectstack/runtime` — the dispatcher's environment-membership gate.** When its `sys_environment_member` read throws, or no ObjectQL engine resolves on the request's kernel, the request is refused with `503 SERVICE_UNAVAILABLE` — the `AuthzStoreUnavailableError` answer the identity step and the domain gates already give an authorization input they could not read. Before, the gate logged at debug level and let the request through. A member is still admitted, and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. An engine whose registry does not register `sys_environment_member` declares the gate inapplicable, and nothing is read. + - **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). When its `sys_organization` or `sys_environment` read throws, the organization update is refused with `503 SERVICE_UNAVAILABLE`. Before, the hook ended without refusing and the slug changed. A slug change while an active environment references the organization is still refused (`403 FORBIDDEN`), and any other change is still allowed. An engine that does not register `sys_environment` — the open-source composition, where it is a cloud-provided object — declares the guard inapplicable from its registry (`getSchema`): nothing is read and the update proceeds as before. Without a data engine the guard does not apply either. + + What changes for you: nothing in what you write. A `503 SERVICE_UNAVAILABLE` on these doors is a store outage that used to be hidden behind an admitted request; it clears when the store answers again. + +### Patch Changes + +- 1c3a4d9: A TOTP enrollment names the deployment, not the auth library. `/two-factor/enable` and `/two-factor/get-totp-uri` answered an otpauth URI whose issuer and label prefix were `Better Auth`, so every authenticator app listed the account under that name. They now carry the deployment's app name: `OS_APP_NAME`, else the configured `appName`, else `ObjectStack`. An explicitly set `branding.workspace_name` setting still outranks it. + + Clause-②: no + + - **Existing enrollments keep working.** The issuer is a display label. The stored enrollment holds only the encrypted secret, the backup codes and the confirmation flag, and the codes depend only on the secret, digits and period. An authenticator app enrolled under `Better Auth` keeps producing codes that verify. It keeps its old label until the user re-enrolls. + - **`@objectstack/plugin-auth`.** `AuthManager` passes its app name to better-auth as `appName`. In better-auth 1.7.3 that key names only these two otpauth URIs. No cookie name or stored value derives from it. + - **`@objectstack/cli`.** `objectstack serve` now passes the deployment app name to `AuthPlugin`. It is resolved by the same chain the email service's template context uses: `OS_APP_NAME` > `config.email.appName` > `config.email.defaultTemplateContext.appName` > `config.appName` > `ObjectStack`. Before, `serve` built `AuthPlugin` with no app name, so auth answered `ObjectStack` whatever `OS_APP_NAME` said. Auth emails were affected too: under `serve` they now name the deployment the way every other email already did. + - The issuer is read when the auth instance is built. A `branding.workspace_name` change made after that reaches new enrollments at the next restart or auth-settings change, while auth emails pick it up on their next send. +- a43d90a: Phone-number OTP with no deliverable SMS service now answers `400 SMS_SERVICE_REQUIRED` instead of a `500` with an empty body (#21793). + + Clause-②: yes (widening) + + - **`@objectstack/plugin-auth`.** `POST /api/v1/auth/phone-number/send-otp` on a deployment that turned phone sign-in on but has no SMS service that can deliver a code (none wired, or only the log transport in production) used to answer `500` with a `null` body: the send callback threw a plain `Error`, and better-auth's router turns anything but its own `APIError` into a bare 500. The login page had nothing to branch on and showed a generic failure. It now answers `400` with the body `{ "code": "SMS_SERVICE_REQUIRED", "message": "…" }`, a typed `APIError`, as the daily-quota branch of the same send already was. The message names the missing SMS delivery service and where an administrator configures it, and never carries the one-time code. `request-password-reset` is unchanged: it still answers `{ "status": true }` and sends nothing, so it reveals nothing about which numbers are registered. + - **`@objectstack/spec`.** `SMS_SERVICE_REQUIRED` is registered for `@objectstack/plugin-auth` in the ADR-0112 error-code ledger, beside its email sibling `EMAIL_SERVICE_REQUIRED`. `ErrorCode` (and so `ApiErrorSchema.code`) accepts one more value. Nothing that parsed before is refused now. + + **Action for clients.** A client that branched on the old `500` for this case should branch on `code === 'SMS_SERVICE_REQUIRED'` instead. The public config already advertises the capability as `features.phoneNumberOtp`, which stays `false` on such a deployment. +- dcb11c2: A user created after the default organization is deleted and recreated in the same process is now bound to the organization that exists, not to the deleted one's id. + + Clause-②: no + + - **What was wrong.** The `tenancy` service's `defaultOrgId()` memoized the default organization id for the life of the process and never checked it again. The single-org bootstrap recreates a missing `slug='default'` organization on the next `sys_user` write, under a new id. Users created under the `auto` membership policy after that were bound to the deleted id. Membership is decided once, at creation (ADR-0093 D7), so nothing repaired them later. + - **What changed.** Every call checks the memoized id against `sys_organization` with one read by primary key. If the organization still exists, it is returned and nothing is re-resolved. If it is gone, the id is resolved again by the same rule that set it (the `slug='default'` organization first, else the only organization), so the replacement is the one a fresh boot would pick. If none exists yet, the answer is `null`, and the next call resolves again. + - **A read the store cannot answer** (a failed read, or a reply that is not a row list) keeps the memoized id. It is not treated as proof that the organization is gone, because that would bind the next user to no organization at all. + - **Who sees it.** Every reader of `defaultOrgId()` gets the check: the membership bind at user creation and its first-session settle, the self-registration grant, the admin create-user path, the membership backfill, the anonymous public form doors and the check on organization-scoped form writes, and the email-template bootstrap. Each call with a memoized id costs one primary-key read of `sys_organization`. + - No public export, option or accepted input changes. Walled postures still answer `null` without reading anything. +- 131b937: Four producers that reached the data engine with no principal and no `isSystem` now carry the explicit system opt-in. Each is already authorized by its own door, so nothing it answers changes. + + Clause-②: no + + - **`@objectstack/plugin-auth` — the platform-admin OAuth client toggle route** (`POST /api/v1/auth/admin/oauth2/toggle-disabled`). Its `sys_oauth_application` read and write go through `withSystemContext`, the wrapper better-auth's adapter already writes those rows through. The platform-admin judge still runs first. The answers (`200`, `404 RESOURCE_NOT_FOUND`, the refusals) and the stored row are unchanged. One log line goes away: the engine's read-only `updated_at` warning on every toggle. The value it warned about was discarded before and the driver still stamps the column. + - **`@objectstack/plugin-auth` — `verifyScimBearerToken`.** The credential probe passes `isSystem: true` in the read's trailing options. It runs before any caller is known, and the digest equality is still all it matches. An unknown, inactive or expired bearer is still `null` (`401`). + - **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). Its `sys_organization` and `sys_environment` reads go through `withSystemContext`. The organization id stays in the `where`. A slug change while an active environment references the organization is still refused (`FORBIDDEN`), and any other change is still allowed. The catches around both reads are unchanged: a read that throws still ends the hook without refusing. + - **`@objectstack/runtime` — the dispatcher's environment-membership gate.** The `sys_environment_member` read carries `isSystem: true` as its query context. The caller's user id stays in the `where`. A member still passes and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. The catch around the read is unchanged: a read that throws still lets the request through. + + Why: the security middleware hands a context with no principal and no `isSystem` straight through (ADR-0096). That hand-through is not an authorization. A caller that is the platform acting for itself says so explicitly. ⛔ No new elevation API, no door's authorization moves, and no accept set changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [49524f6] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [a7ab047] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [76fec88] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/rest@17.7.0 + - @objectstack/service-messaging@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 60f73db1d05..262e626919a 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "main": "dist/index.js", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index a1ec4bc0d3c..2c6f7782eb1 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,209 @@ # @objectstack/plugin-dev +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [e3ad492] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [97239c3] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [ee75aae] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [ab52182] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [49524f6] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [8b123c0] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [520f66f] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [a7ab047] +- Updated dependencies [f9a8eb8] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [bd70706] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [aa0d4b9] +- Updated dependencies [5d0e4e2] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [a1ca156] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [417443e] +- Updated dependencies [be55fd2] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [234d1d8] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [c7a60e1] +- Updated dependencies [e83c9f6] +- Updated dependencies [3eb38ae] +- Updated dependencies [33f9791] +- Updated dependencies [1c3a4d9] +- Updated dependencies [025008a] +- Updated dependencies [50b5e03] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [c9be1f1] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [833d57c] +- Updated dependencies [607463d] +- Updated dependencies [088428f] +- Updated dependencies [cab6396] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [41a1135] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [5e0b489] +- Updated dependencies [07c842d] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [13a22d0] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/driver-memory@17.7.0 + - @objectstack/service-storage@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/plugin-auth@17.7.0 + - @objectstack/rest@17.7.0 + - @objectstack/plugin-hono-server@17.7.0 + - @objectstack/account@17.7.0 + - @objectstack/setup@17.7.0 + - @objectstack/service-i18n@17.7.0 + - @objectstack/service-realtime@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index bb22c675d00..4c4b503b6da 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "main": "dist/index.js", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index ef2c90bfa8e..0294a2ac16e 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,154 @@ # @objectstack/plugin-email +## 17.7.0 + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 08adfea: An email template edited through `PUT /api/v1/meta/email_template/:name` (the Studio editor's door) now keeps the admin's wording in `sys_email_template` across a restart. Before, the boot sweep wrote the package wording back over the sending row while `GET /meta` kept serving the admin's, so mail went out with the package wording after every boot. + + Clause-②: no + + - The cause: on a deployment with a Default Organization the admin's save is an org-scoped overlay, and boot hydration keeps org-scoped overlays out of the registry the sweep read. An env-wide overlay was already kept. + - `EmailServicePlugin`'s boot sweep now projects the effective template: the layered list `protocol.getMetaItems` serves, read in the organization `tenancy.defaultOrgId()` names. That is the Default Organization under the `single` posture. A host without a `protocol` service reads the registry as before. + - A failed effective read projects nothing for that boot, so the rows keep their last projection. It does not fall back to the package wording. + - Seed-not-clobber is unchanged. A row an admin created (`managed_by: 'admin'`) or edited through the data API (`customized: true`) is still never overwritten. + - The published API is unchanged. The exported `bootstrapDeclaredEmailTemplates` keeps its signature and still reads the registry, so a caller outside the plugin sees the same behaviour as before. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index e041bf23a1a..4e82ac33d6a 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index fa909e49255..7f8518a259b 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,134 @@ # @objectstack/plugin-hono-server +## 17.7.0 + +### Patch Changes + +- f5b8e29: Share-link passwords follow the platform's credential rules (#21839). + + - **The stored hash never leaves the server.** The share-link mint response (`POST /api/v1/share-links`, and `ShareLinkService.createLink`'s return value) no longer carries `password_hash`. The list and the redemption result are projected the same way. A client that reads a link's password state keeps reading it from the redemption route's `NEEDS_PASSWORD` answer, as before. + - **The stored form is the platform's slow password hash.** New passwords are hashed with scrypt at the parameters account passwords use, instead of one salted SHA-256. Links minted before this release keep working: a stored password in a legacy form still verifies, and it is re-hashed into the new form on its first successful redemption. Every comparison is constant-time. A deployment that injects its own `hashPassword` / `verifyPassword` pair is unaffected, and its stored forms are left alone. + - **The password travels in a header.** Both public share-link routes (`GET /api/v1/share-links/:token/resolve` and `/:token/messages`) accept the `x-share-password` request header, the preferred form, because a header is not part of the request URL. The `?password=` query parameter is still accepted for compatibility, so current consoles keep working until they move to the header. `/messages` accepted only the query parameter on this mount before. + - **Cross-origin clients can send the header.** `X-Share-Password` is in the default CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS` in `@objectstack/plugin-hono-server`, which the `@objectstack/hono` adapter also applies). A deployment that passes its own `allowHeaders` is unchanged; add the header to that list to let a cross-origin client use it. + - **Public share-link answers are not cached.** Both public routes answer with `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, on both mounts (the sharing plugin's routes and the runtime dispatcher's `/share-links` domain). The authenticated create, list and revoke routes are unchanged. + - **Hashing works in WebContainer.** On StackBlitz WebContainer, where `node:crypto.scrypt` is incomplete, the password is hashed with the pure-JS scrypt from `@noble/hashes` (now a dependency of `@objectstack/plugin-sharing`, as it already is of `@objectstack/plugin-auth`), at the same parameters and in the same stored form. A hash made on either runtime verifies on the other. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/observability@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 18959b4af1a..0a83383a1cd 100644 --- a/packages/plugins/plugin-hono-server/package.json +++ b/packages/plugins/plugin-hono-server/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-hono-server", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index 235c10e9bdc..c1c9143bc57 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,50 @@ # @objectstack/plugin-pinyin-search +## 17.7.0 + +### Patch Changes + +- Updated dependencies [c205b6c] +- Updated dependencies [41a3c8d] +- Updated dependencies [748b240] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [30af17e] +- Updated dependencies [c2cd651] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [ceb4a93] +- Updated dependencies [57cc695] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [5c9138b] +- Updated dependencies [a1ca156] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [be55fd2] +- Updated dependencies [8843505] +- Updated dependencies [5259a35] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [0728cbf] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [13a22d0] +- Updated dependencies [568dc0b] + - @objectstack/core@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 6eadfce3831..bea88075d0c 100644 --- a/packages/plugins/plugin-pinyin-search/package.json +++ b/packages/plugins/plugin-pinyin-search/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-pinyin-search", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index 0a608ff479e..329009209fd 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,367 @@ # @objectstack/plugin-security +## 17.7.0 + +### Minor Changes + +- 7aab759: fix(plugin-security)!: a row-level policy that compares a numeric column with a comparand that is not a number is refused at the RLS compile seam, read and write alike, as the engine's `where` refuses the same comparison + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows which row-level policies the RLS compile seam hands to its two consumers, the read and the write check. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed. One published type gains a member: `RlsFieldGuard`, the type of the optional `fieldGuard` argument of the root-exported `RLSCompiler.compileFilter`, gains the optional `number` member (each declared column's `type`, and a formula's `returnType`). It is an additive optional member, not a change of what is accepted. + + **What was accepted before.** A policy such as `record.amount <= '9999-12-31'` on a `number` column compiled, and its `using` and `check` both reached their consumers unjudged. Measured through `ObjectQL.insert` and `SecurityPlugin` on `SqlDriver` (better-sqlite3), as a member: the write of `amount: 5` was admitted (`@objectstack/formula`'s deleted whole-day copy read the number as an instant), and the read showed the stored row, because SQLite orders an integer before any text. That read was measured on SQLite only; PostgreSQL was not run for this change. The same comparison in a caller's `where` is refused `INVALID_FILTER` / 400 by the engine's number-comparand door. + + **What is refused now.** The seam runs the spec's number-comparand verdict (`numberComparandDoorVerdict`, `@objectstack/spec/data`), the one the engine's `where` door consults, on every compiled policy filter, after the shape door and before the comparand-type door. On a column the object declares numeric, a comparand that is not a number (a string the platform's numeric grammar does not read, such as `'9999-12-31'` or `'abc'`, a boolean, a `Date` or a list) drops the policy through the existing fail-closed route: the read is filtered by the deny sentinel and returns no rows, the write is refused `PERMISSION_DENIED` / 403, and a WARN line names the policy, the clause and the comparand. The line's detail is written for the clause it refused: for `check`, which the write check evaluates in-process, it names no driver bind. A granting sibling policy still grants. + + **Narrowed, as in `where`.** A numeric string (`'10'`, `'1e3'`) is replaced by the number it names before either consumer runs. So `record.amount == '10'` now matches a stored `10` on the write check, which compared the text with the number and refused it, while the read showed the row. + + **The remedy.** Compare a numeric column with a number: `record.amount <= 9999`, not `record.amount <= '9999-12-31'`. + + **Unchanged.** A numeric literal, a column that is not numeric, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one). +- 8b123c0: Row-level security policies and the analytics native-SQL path judge a comparand against a declared boolean field by the platform's boolean-comparand rule, the one the data engine's `where` already applies + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what two compilers outside the engine's `where` door accept. The RLS compile seam now drops a row-level policy, and the analytics native-SQL face now refuses a query, when either compares a declared boolean field with a comparand outside the accepted set. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. + + - **Row-level security (`@objectstack/plugin-security`).** A compiled `using` / `check` predicate on a `boolean` or `toggle` column (or a `formula` returning `boolean`) is judged by `booleanComparandDoorVerdict` from `@objectstack/spec/data`, in the same pass as the number rule. `'true'` / `'false'`, `'1'` / `'0'` and `1` / `0` are read as the boolean each names. Anything else the rule refuses (a string such as `'yes'`, `'TRUE'` or `''`, a number other than `1` / `0`) drops the policy as a refused comparand: the read is filtered by the deny sentinel, the write is refused 403, and the WARN line names the clause, the field and the position. Before, `record.flag != 'true'` kept every row on SQLite and the write check admitted every row, so the exclusion the author wrote was not applied. + - **Analytics native SQL (`@objectstack/service-analytics`).** The query's `where` (and the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter` are judged by the same rule before the statement compiles. An accepted spelling is read as its boolean, and anything else the rule refuses is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. The native strategy now answers what the engine-aggregate strategy answers. Before, `{ flag: 'true' }` counted no rows on SQLite, `{ flag: { $ne: 'true' } }` counted every row, and `{ flag: 'yes' }` answered 200. + - **What you may notice.** A policy or analytics filter that compared a boolean field with a value outside the accepted set now refuses instead of answering. Write `true` / `false`. A policy `record.flag == 1` now admits writing a `true` row, which its read already showed. + - **Unchanged.** A boolean literal, a column that is not boolean, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one). +- 3eb38ae: A user who can edit a record may delete another user's attachment on it, as the attachment gate declares (#21729). + + Clause-②: yes (widening) + + - **What was refused.** The attachment gate's delete rule is "the uploader OR a user who can edit the parent record". For every member holding `org_member`, the platform's row-level delete floor in `member_default` (`owner_only_deletes`: only the rows you created) answered first, so a parent editor's delete of someone else's attachment was refused with `PERMISSION_DENIED` before the gate ran. + - **`@objectstack/service-storage`** contributes a delete-only alternate match for `sys_attachment` (`sys_attachment_parent_editor_delete`, every row) when it installs the attachment gate, and only then. The gate decides: a parent editor's delete answers 200, and a caller who can read the attachment but neither uploaded it nor can edit the parent is refused with `ATTACHMENT_DELETE_DENIED`. A caller who cannot read the parent cannot see the attachment, and is still refused with `PERMISSION_DENIED` before the gate runs, so the parent is not named to them. + - **Without `@objectstack/service-storage`** nothing is contributed. A deployment that registers `sys_attachment` without the storage service keeps the floor, and only a row's creator may delete it. + - **The edit limb is unchanged.** Editing another user's attachment row is still refused by the floor for a member it binds, a parent editor included. + - **`@objectstack/plugin-security`** gains the seam: `contributeOwnershipFloorAlternates(plugin, alternates)` on the registered `security` service, an extension of `ISecurityService` that callers feature-detect. Each alternate names one object (never `'*'`), one floor limb (`update` or `delete`; `all` is refused) and a `using` predicate. It lands beside each enabled floor policy of that limb, in that policy's own `positions` domain, so it reaches only the principals the floor binds. A plugin's second call replaces its first, and an empty list withdraws it. A contribution that breaks these rules throws. + + Nothing that was admitted before is refused now. No principal outside the floor's domain, and no other object or operation, changes. +- 53021e3: fix(plugin-security)!: on the write doors, a row the caller cannot read answers what a nonexistent id answers + + Clause-②: no (narrowing) + + + + **BREAKING**: a by-id update or delete of a row the caller cannot read now answers `404 RECORD_NOT_FOUND`, with exactly the body an id that names no row gets, for every principal class. On the write doors, "hidden" and "gone" are now one answer to a caller who cannot read the row. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new. + + **What changed.** The answer used to depend on which gate saw the row first. Where a write-class row filter binds the caller, the by-id write pre-image check answered `403 PERMISSION_DENIED`. Where none binds it, a later gate answered with its own 403: `FORBIDDEN` from record sharing, or a parent-derived gate's code on attachments and comments. Meanwhile a nonexistent id answered `404`. So the write door could tell a hidden row apart from a missing one. The pre-image check now asks the read door's own question first, for the by-id write the caller addressed: a by-id read in the caller's context, every data middleware's visibility included. A row that read does not return gets the read door's not-found producer. A store fault propagates as raised, and a read-time policy refusal is not treated as absence. + + **What is refused now that was not.** A principal that no write-class row filter binds could have its by-id write admitted on a row the read door hides from it. One example is the uploader of an attachment, or the author of a comment, whose parent record they can no longer read. That write is now refused with the not-found answer, as it already was for every principal a row filter binds. + + **FROM → TO.** A by-id update or delete of a row hidden from the caller: FROM a `403` (`PERMISSION_DENIED`, `FORBIDDEN`, or a parent-derived gate's code) → TO `404 RECORD_NOT_FOUND`, the body a nonexistent id gets. + + **If you are affected.** A client that read a by-id write's `403` as "the row exists, but you may not change it" should read `404 RECORD_NOT_FOUND` the way the read door means it: no row you can see has this id. + + **Unchanged.** + - A caller who can read the row but may not write it keeps its 403. They already see the row. + - By-id writes the platform issues under the caller's context keep their previous answer, because the caller never named their target: the engine's cascade delete of a dependent row, a hook's write, and the referential clear of a lookup. + - Writes that are not routed by id are unchanged. + + `security/explain` follows enforcement. Its record verdict for an update or delete of a record the principal cannot read is now the missing-record shape: `visible: false`, with no decider. +- 833d57c: The packaged-permission-set lock refusal carries its guidance as `userMessage`, so the console tells the admin to clone the set instead of showing its generic "You don't have permission to save this record." (#21794). + + Clause-②: yes (widening) + + - **`PackagedPermissionSetLockedError.userMessage`**, a new `readonly` member. A save that targets a permission set an installed package ships still answers `403 NOT_OVERRIDABLE` with the same `message`. That holds at the data door (`PATCH` / `POST /api/v1/data/sys_permission_set`) on every kernel. It also holds at the metadata door (`PUT /api/v1/meta/permission/:name`) on a kernel with no environment id, such as a self-hosted app server, where this lock is the refusal that answers. On an environment kernel the metadata protocol's own package-door refusal answers that `PUT` first, and it is unchanged. The error envelope now also carries `userMessage`, the field `ApiErrorSchema` already declares and the console renders verbatim. An edit is told to clone the set with the Clone action and edit the clone. A new set named like a packaged one is told to choose a different name, or clone. + - **`PackagedPermissionSetProvenanceUnknownError.userMessage`**, the same member on the fail-closed refusal (the platform could not tell whether a package ships the set). It tells the admin to try again, or clone. The unreadable source stays in `message`. + - The texts name no set, package, id or API path; `message` keeps that diagnostic for logs and developers. They are English, like every platform refusal. + + Nothing that was accepted is refused now, and nothing that was refused is accepted. Both refusals keep their `code`, `status` and `message`. +- cab6396: fix(plugin-security)!: a predicate-scoped update or delete matches only the rows the caller can read + + Clause-②: no (narrowing) + + + + **BREAKING**: a predicate-scoped (`multi: true`) update or delete now matches only the rows the caller can read. A row the read door would not return to the caller is not written, not counted and not refused, so a predicate that reaches only such rows answers exactly what a predicate that matches nothing answers: success, zero rows. This is the by-id write doors' rule ("hidden" and "gone" are one answer to a caller who cannot read the row) carried to the predicate door. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new. + + **What changed.** The rows a predicate write matched came from its write scope alone. A row the caller cannot read was matched whenever that scope reached it, for example through a write-class row-level policy wider than the read policy, or on an object whose read visibility follows a parent record. A per-row gate then refused the whole write with a `403`, or the row was written and counted. Either answer told a hidden row apart from no row. The write middleware now asks the read door which rows the caller's own predicate returns, through a read in the caller's context that every data middleware's visibility applies to, and narrows the write to those rows: readable ∩ writable. A read the read door refuses (no read grant on the object) keeps the write's previous answer. A store fault on that read propagates as raised. + + **What is refused or narrowed now that was not.** + - A predicate write no longer writes, counts or refuses rows its caller cannot read, including rows its write scope reaches. + - A predicate write whose predicate matches more than 10 000 rows the caller can read is refused with `400 INVALID_FILTER`, before anything is written, rather than narrowed by a cut-off list. The limit is the platform's existing row ceiling for one predicate write. + + **FROM → TO.** + - A predicate update or delete reaching rows the caller cannot read: FROM a per-row gate's `403`, or those rows written and counted → TO those rows not matched; success with zero rows when no readable row matches. + - A predicate update or delete whose readable match exceeds 10 000 rows: FROM attempted → TO `400 INVALID_FILTER`, nothing written. + + **If you are affected.** An operator who needs a user to change rows grants that user read access to them first. A caller that read a predicate write's `403` as "a row exists here" reads the result as the count of rows it can see. A predicate whose readable match is over the ceiling is narrowed and written in batches. + + **Unchanged.** + - A caller who can read a matched row but may not write it keeps its answer. + - Writes the platform issues under the caller's context keep their previous answer, because the caller never addressed them: a cascade, a hook's own write, and the referential clear of a lookup. + - By-id writes keep their answers. System-context writes are not narrowed. + - `security/explain` takes no predicate, so it has no predicate-write verdict to change. + +### Patch Changes + +- e3ad492: Security refusals, explain details, field help and log lines no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings the security plugin shows to administrators, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - The curated capability-name refusal says a curated name is refused at authoring so that no admin-authored row can collide with the row the platform seeds for it. + - The two delegation anchor refusals say the business-unit anchor roots the delegate's business-unit visibility, so a delegation may only narrow it. + - The `managed_by` field help on `sys_permission_set` and `sys_position`, in every shipped locale, says capabilities, permission sets and positions all share one platform / package / admin vocabulary. + - The explain details for an unresolvable security posture and for the View/Modify All Data bypass drop their citations; those sentences already said that access fails closed and that the write path consults the same bypass. + - The derived-capability boot warning says the derivation refreshes a row's label and description only when it can prove the row is the platform's own, and that the seeder neither adopts a row it cannot prove is its own nor backfills provenance on the operator's behalf. + - The fail-closed log lines say what each denial protects: a `controlled_by_parent` child is readable and writable only where its master is, and a chain the derivation cannot resolve admits no child; only a resolved sharing allow (Modify All Data or an edit-level share) may replace the platform ownership floor; an authored-policy verdict that cannot be resolved never lifts the sharing refusal; a path that bypasses the engine middleware never runs without the owner and share scope a direct read applies; a delegated read is never scoped wider than its delegator's own; an unreadable posture never defaults to public or uncontracted. + - The public-form line says an anonymous submission cannot set ownership, tenancy or audit columns; the uninstall line says a package's permission rows are removed by `package_id`, so no grant outlives the package; the platform-owner wall-bypass line says only the declared platform owner's reads cross the wall and writes stay walled for everyone. The org-scoping entitlement, masking-rule, permission-set resolution, vocabulary-normalization and service-registration lines drop their citations, and the log lines that carried a tracker number in their `[security/…]` prefix now open with `[security]`. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix or prefix) needs the new spelling. +- 1878ef9: fix(plugin-security,platform-objects): an org member reading a colleague's `sys_user` row is no longer served the identity object's `Admin` field group, directly or through the activity stream (#21237) + + Clause-②: no + + - **What a member was served.** The platform baseline `member_default` opens every org peer's `sys_user` row (the `sys_user_org_members` policy) and declared no field-level security on it. An org member reading a colleague's row was therefore served the whole `Admin` field group: the sign-in trail, the lockout state, the ban reason and expiry, the password and MFA stamps, the legacy platform role scalar and the AI-seat flag. With object-level read on `sys_activity`, the colleague's activity metadata carried the same fields, because the activity field redaction serves exactly what the data plane serves. + - **What changes.** `member_default` and `viewer_readonly` now declare the `Admin` group `readable: false` through the permission set's existing `fields` entries. The withheld set is built from the identity object's declaration, so a field the declaration adds to the group is withheld from the day it is declared. `admin_full_access` and `organization_admin` (and so `organization_admin_no_bypass`) declare the group readable and editable, the same state as a field no set names, so an administrator's reads and writes are unchanged. `member_default` is the additive baseline every authenticated user resolves, and field grants merge most-permissively, which is why the admin sets carry that keeping entry. + - **What a member sees now.** On the direct read, the list read and the activity metadata, a member is served no `Admin`-group field of a colleague's row. The directory fields (name, email, image) are still served. Field-level security does not distinguish rows, so the member's own row read through the generic data API is withheld the group too; every platform reader of those fields on a member's own row (the auth gates, the sign-in stamps, the session, the AI-seat resolution) reads under system or auth context and is unaffected. A member's query that filters or sorts on a withheld field is refused (`403 PERMISSION_DENIED`, the filter-oracle rule). A member's user-context write that names a withheld field is refused by the field-level write gate (`403 PERMISSION_DENIED`), and a payload mixing such a field with profile fields no longer lands partially. + - **The deactivation flag is directory data.** `sys_user.banned` moves from the `Admin` field group to the `Account` group in `@objectstack/platform-objects`, so members are still served it. Every user picker filters its candidates on it, and a filter on a withheld field would be refused. Its reason and expiry stay in the `Admin` group. In a record form the field now renders in the `Account` section. + + **Migration.** None for shipped apps. A custom permission set that grants an org member read on `sys_user` and is meant to show them the `Admin` group must name those fields `readable: true` in its `fields` entries. A client that filtered members' `sys_user` queries on an `Admin`-group field must drop that predicate or run it with an administrator's grant. +- 97239c3: fix(plugin-security): a row-level `check` refuses an operator the read refuses on a field declared JSON-stored, with the read's `INVALID_FILTER` / 400, so a policy whose read is refused no longer admits writes (#21254) + + Clause-②: no + + The read a row-level policy scopes refuses a scalar comparison, an ordering or a text operator (`@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, and implicit equality) on a field the object declares JSON-stored: a structured-JSON type (`json`, `address`, …), or a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `lookup` / `user` / `file` / `image` flagged `multiple: true`). The write `check` evaluated the same operators against the stored list instead. Measured through `ObjectQL.insert` with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set, with the same predicate as `using` and `check`: + + | `check` | written | write, before | read | + |---|---|---|---| + | `record.tags != 'x'` | `['x']` or `'x'` | admitted, stored `["x"]` | 400 | + | `!(record.tags in ['x'])` | `['x']` | admitted, stored `["x"]` | 400 | + | `record.tags == 'x'` / `record.tags in ['x']` | `['x']` | 403 | 400 | + | `record.tags > 'a'` | `['x']` | 400 | 400 | + | `record.meta != 'x'` / `record.meta == 'x'` (`meta` is `json`) | a scalar | admitted, stored | 400 | + + Now the write check refuses every one of these with the read's answer: `INVALID_FILTER` / 400 and the read's words, which withhold the field and the operator. The refusal reads the object's declaration, never the record, so a policy is refused for every row or for none, on the insert, a by-id update and a predicate update. The diagnostic, which names the field, the operator and the policy, goes to the server log. Rows that already refused still store nothing; their answer is now the read's. + + Unchanged: `contains` and its negation (`$contains` / `$notContains`), and the presence checks (`== null`, `!= null`), answer on such a field as before; a field declared neither way keeps every operator; an object whose schema cannot be loaded is judged as before. To repair a refused policy, test membership with `contains` (for example `!record.tags.contains('x')`). +- ee75aae: fix(plugin-security): `security/explain` answers the read's `INVALID_FILTER` / 400 for a row-level policy that aims an operator the read refuses at a field declared JSON-stored, instead of a "visible" verdict for a request enforcement refuses (#21319) + + Clause-②: no + + The read a row-level policy scopes refuses a scalar comparison, an ordering or a text operator (`@objectstack/core`'s `JSON_COLUMN_INCOMPATIBLE_OPERATORS`, and implicit equality) on a field the object declares JSON-stored: a structured-JSON type (`json`, `address`, …), or a multi-valued field (`tags`, `multiselect`, `checkboxes`, or a `select` / `radio` / `lookup` / `user` / `file` / `image` flagged `multiple: true`). The row-level write `check` refuses them too, by the same rule. `security/explain` (the `security` service's `explain()` and `POST /api/v1/security/explain`) evaluated them in JS instead. Measured with `SecurityPlugin` on two SQLite driver families, as a member resolving a permission set whose `using` is the predicate: + + | `using` | find | explain, before | + |---|---|---| + | `record.tags != 'x'` (`tags` is `tags`, multi-valued) | 400 | `visible: true`, decided by `rls` | + | `record.meta == 'x'` (`meta` is `json`) | 400 | `visible: true`, decided by `rls` | + | `!(record.tags in ['x'])` | 400 | `visible: true`, decided by `rls` | + | `record.owners != 'x'` (a `select` or `lookup` flagged `multiple`) | 400 | `visible: true`, decided by `rls` | + + The report without a record id said `allowed: true`, and a record id no row carries was reported `visible: false`. Now explain answers every one of these with the read's refusal, `INVALID_FILTER` / 400 and no verdict, for every operation, the answer it already gives a policy comparing two fields of different classes; a by-id update or delete is itself refused 403, at the row-level gate whose pre-image re-read is the refused read. The message leads with the full diagnostic, which names the field and the operator and says how to repair the policy, then the policy that carries it; the error's `cause` carries the read's refusal, with the find's code, status and message. The rule is the one the write check applies, and it reads the object's declaration, never the record. + + Unchanged: `contains` and its negation (`$contains` / `$notContains`), and the presence checks (`== null`, `!= null`), answer on such a field as before; a field declared neither way keeps every operator; an object whose schema cannot be loaded is judged as before. To repair a refused policy, test membership with `contains` (for example `!record.tags.contains('x')`). +- ab52182: fix(cloud-connection,plugin-security): a package installed into a running runtime fires its record-change flows and has its permission sets in `sys_permission_set` right away, not after a restart + + Clause-②: no + + **Before**, `os package install ./dist/objectstack.json` into a running `os start` (the install-local route) registered the package, bound its script actions and body hooks, and stopped there. Two things the boot does for a package happen at `kernel:ready`, and that moment had already passed. The automation engine binds flows at `kernel:ready`, so the package's record-change flows never fired: a task updated to `done` wrote no note. The security plugin seeds declared permission sets at `kernel:ready`, so the package's set had no `sys_permission_set` row. `/meta/permission` listed the set, but an admin could not grant it. A restart fixed both, because the restart re-registers the package before those two steps run. Nothing in the CLI output or the install response said a restart was needed. + + **Now** the install route announces `metadata:reloaded` once the package is registered, bound, persisted and seeded. That is the same event a Studio package publish, a per-item publish and an artifact reload already announce. The automation engine already re-syncs its flows on it. The security plugin now re-runs its declared-permission seeding on it: the same function and organization passes as the boot, with the same provenance rules (`managed_by: 'package'`, `package_id`). Right after the install, the flow fires and the set's row exists, with the same state a restart gives. The seeding is idempotent and writes nothing when no permission set changed. It runs only after the boot's own pass has finished. A failed re-sync does not fail the install. It is logged at `warn` with the restart that repairs it. + + **Unchanged.** The restart path (the ledger rehydrate) announces nothing and behaves as before. The install response and the CLI output keep their fields and text. A package's `defineStack({ jobs })` are still not scheduled by install-local, on install or after a restart, because a job's handler is code from the artifact's runtime module and an inline install carries only the JSON. +- 520f66f: `PermissionDeniedError` declares its 403 as `status` as well as `statusCode`, so a permission refusal answers 403 at every door (#21405). + + Clause-②: no + + The class declared `statusCode` alone, unlike every other error class in `errors.ts`, and a door that reads `status` alone derived no status from it. On a showcase boot, a plain member's `POST /api/v1/share-links` on a record they cannot read answered `500` with code `PERMISSION_DENIED` through `plugin-sharing`'s route door, while the runtime dispatcher's `/share-links` domain answered the same refusal with `403`. Both doors now answer `403 PERMISSION_DENIED`. The code, the message and `statusCode` are unchanged. +- f9a8eb8: The seed-ownership claim now runs whenever a seed settles, on every boot, not only on the boot that promotes the first platform admin. + + Clause-②: no + + - **Before:** a later boot whose seed replay inserted rows into a database that already had a platform admin left those rows `owner_id` NULL for good. An in-budget seed settles before `kernel:ready`, and the bootstrap that runs there finds the existing admin (`already_have_admin`) and promotes nobody, so neither path reached the claim. A `readScope: 'own'` grant never saw those rows. + - **Now:** when a seed settles (`app:seeded`) before this boot's bootstrap has named a claim target, the handler resolves the target itself: the existing platform admin, by the bootstrap's own `already_have_admin` rule. The claim then hands the replayed rows to that admin. The handler subscribes in `init()`, so a seed that settles before this plugin's `start()` is heard too. That happens on any composition that registers the app first. + - Unchanged: the claim's predicates (`owner_id` NULL or `usr_system`), its object filter and the first-boot promotion path. A row someone else owns is never touched. Under a walled tenancy posture no claim runs, as before. + - Log lines: the claim report reads `handed N seeded record(s) to platform admin USER_ID`, where it used to say `first admin`. Its provisional and failure lines now say when the claim actually runs next: the next seed settle, on this boot or a later one, or the next platform-admin promotion. `os meta resync` is not such a run. +- 234d1d8: fix(plugin-security): a permission set the environment cloned no longer logs `permission_set_declaration_unowned` on every boot (#21669) + + Clause-②: no + + The declared-permission seeding pass walks every `permission` item in the engine registry. That registry also holds the permission sets an environment authored itself, which boot hydration loads from their `sys_metadata` rows. A set made with Setup's **Clone** action is one of them, and it carries no package id because it has none. The pass judged "no owning package" before it looked at the set's row, so on every boot, and on every `metadata:reloaded`, it logged one warning per cloned set: `[permission_set_declaration_unowned] declared permission set "…" has no owning package — not materialized … the Setup admin surface reads sys_permission_set and cannot see this set`. That is false for a clone. Its row exists (`managed_by: admin`), and Setup lists it and edits it. + + The pass now checks the row first. A registry item with no package id whose `sys_permission_set` row the environment owns (`managed_by` other than `package`) is the environment's own set. It is counted as `skippedEnvAuthored`, the count a package declaration over an environment row already gets, and no warning is logged. The pass reads that row from the existence read it already makes, so no query is added. On a per-organization pass, the environment door's organization-less row counts too. + + Unchanged: a declaration with no owning package and no environment row still logs `permission_set_declaration_unowned`, with the same text, and still counts as `skippedUnowned`. If the row could not be read, the warning still fires, because an unreadable row does not prove the environment owns the set. The publish-time materializer is unchanged. Nothing is written or granted differently: only the false warning stops, and the clone moves from the `skippedUnowned` count to the `skippedEnvAuthored` count in the pass's summary line. +- c7a60e1: `POST /api/v1/security/explain` with `{ object, operation: 'read', recordId }` now credits read depth for a row that only the caller's read depth admits. + + Clause-②: no + + - **Before.** The object's OWD is private. The caller's read depth is wider than `own` (`own_and_reports`, `unit`, `unit_and_below` or `org`). The caller reads a row they do not own and hold no share on. Explain answered `decidedBy: 'sharing'` for that row, with the sharing layer `admitted` and the detail "0 share(s) attached; access is granted for this record." No share granted anything; the read depth did. `depth` is a member of the published `decidedBy` enum, and no answer ever produced it. + - **Now.** That row answers `decidedBy: 'depth'`. The `depth` layer carries a `record` block: `admitted`, naming the depth and, below `org`, the owner it reached. The sharing layer is `not_evaluated`, and its detail says that no share grants the row and that read depth already admits it. + - **The sharing detail names a grant only when one exists**, meaning a share that names the caller. A filter that admits a row with no such share now says which part of the sharing service let the row through. + - **A row a share admits keeps `decidedBy: 'sharing'`** and its "N share(s) attached; access is granted for this record." detail, also when the read depth admits it too. Of the two layers that admit it, sharing comes last in the pipeline, and the `decidedBy` contract names the last layer to admit. + - **No access decision changes.** `visible` is the same for every record; only the layer the report credits moves. No key is added. `depth` and `not_evaluated` are existing values. + - **The `depth` layer's `record` block is new on every record-grained request.** On a write it is `not_evaluated`, because the write depth is judged inside the sharing service's per-record write gate together with ownership and shares, and this report does not separate them. +- c9be1f1: A permission set an organization owns, a clone of a packaged set, and a set saved into a writable runtime package are no longer locked as if a code package shipped them + + Clause-②: no + + The packaged-permission-set lock decides "is this set shipped by a code package?" from the engine registry. The registry also holds the stored definition rows, and a metadata list read (`GET /api/v1/meta/permission`, which every Studio page load issues) stamps a stored row's package binding onto it. A set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=`) therefore looked code-shipped after the first list read, and every later edit of it answered `403 NOT_OVERRIDABLE` at both the metadata door and the data door. The lock now skips a stored row by its provenance (`_provenance: 'org'`, which every stored row carries), the same test the platform's code-artifact check applies, so those edits are accepted again. + + The read had the matching defect. The security plugin keeps a marked in-memory copy of each stored definition for the permission evaluator, and the layered read (`GET /api/v1/meta/permission/:name/layers`) serves that copy as the item's `code` layer. The copy carried no provenance, so an org's own set, a clone and a runtime-package set all reported a `code` layer with no `provenance`, which the console's permission-matrix editor renders as "locked by a code package" while the server accepted the save. The copy now carries `_provenance: 'org'` exactly when the lock judges the set not code-shipped, so the layered read reports `provenance: 'org'` for those sets. + + Unchanged: a set a code package ships is still refused at both doors with `403 NOT_OVERRIDABLE` and the same message naming the clone path, and its layered read still reports `provenance: 'package'`, its package id and `editable: false`. No error code, route or field moves. +- 5e0b489: Discard Overlay no longer deletes the stored definition of a permission set saved into a writable runtime package + + Clause-②: no + + The Discard Overlay action (`POST /api/v1/security/permission-sets/:id/discard-overlay`) is declared to refuse any set that is not package-declared, so that it can never destroy a set the environment authored. It decided that by asking whether any engine-registry item of the set's name carried a package id. The registry also holds the stored definition rows, and a metadata list read (`GET /api/v1/meta/permission`, which every Studio page load issues) stamps a stored row's package binding onto it. A set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=`) therefore passed the check after the first list read: the action answered `200` and deleted the set's only `sys_metadata` row. It now asks the same classifier the packaged-permission-set lock's write doors ask, and refuses every set that classifier does not judge shipped by a code package (including when it cannot decide), with the action's existing `403 PERMISSION_DENIED`. + + The drift diagnostics behind the record's `drift_status` / `drift_detail` (whose `overlay_shadow` detail names Discard Overlay as the remedy) read the package id the same way. They now judge the same population: only sets a code package ships. + + Unchanged: a set a code package ships still has its overlay discarded and its record resynced to the shipped artifact, its drift is still reported as before, and no error code, route or field moves. The refusal's message now says the set is not shipped by any installed code package. +- 07c842d: A data-door edit of a permission set saved into a writable runtime package updates that set's stored definition instead of forking it + + Clause-②: no + + Setup saves a permission set through the data door (`PATCH /api/v1/data/sys_permission_set/:id`), which redirects the edit into the metadata store. A stored definition row is keyed by its package as well as its name, and the redirected save named no package. For a set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=`), whose only stored row is bound to that package, the save therefore created a second, package-less row carrying the edit and left the package's row untouched: two active definitions for one name, with the package's copy no longer receiving the organization's edits. The save now goes into the package the edited row is bound to, read from that row through the metadata door's own item read, so the edit lands on the set's one row and the row stays in its package. + + Unchanged: a set with no package binding still saves with none, and a set a code package ships is still refused with `403 NOT_OVERRIDABLE` before anything is read or written. The edit answers `200` as before; no error code, route or field moves. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 1947a69dee9..501e177b5a3 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index c80327310eb..0d257d82503 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,260 @@ # @objectstack/plugin-sharing +## 17.7.0 + +### Minor Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- 4c8363f: feat(plugin-sharing): the record owner and an explicit Modify-All holder may mint a share link on a record the data door refuses them (ADR-0111 D8 rule 1, ruling A′) (#21329) + + Clause-②: yes (widening) + + - **Who may mint.** `ShareLinkService.createLink` admits the caller when they can see the record, **or** own it, **or** hold `modifyAllRecords` on the object. The object's `publicSharing` opt-in is still checked first, and `publicSharing.eligibility` still last. On an object declared `access: { default: 'private' }` no wildcard grant covers the record, so its owner's own read is refused; the owner can now share it anyway. A member who neither sees nor owns the record is refused exactly as before, with the same envelope. + - **Who still needs visibility.** A hierarchy manager whose write depth covers the record's owner manages the record's shares (revoke, grant, list), but is not admitted to mint without seeing the record: a link creates access. + - **The organization wall.** Under the `group` and `isolated` tenancy postures the owner and Modify-All alternatives are withheld and visibility alone admits, as before this release. A member who left an organization still owns the records they created there, and must not be able to publish them by link. + - **A required capability.** Neither alternative applies past a capability the object requires (`requiredPermissions`). An owner or Modify-All holder who lacks it is refused with the capability gate's own refusal, as before this release; an owner who holds it, refused only because no permission set grants the object, mints. The verdict is read from the `required_permissions` layer of `ISecurityService.explain`, so a security service the sharing service reaches must implement `explain`. If it does not, the two alternatives are withheld. + - **API.** `SharingService.canMintWithoutVisibility(object, recordId, context)` answers the two alternatives with the owner and Modify-All branches `canManageShares` reads. `ShareLinkServiceOptions.canMintWithoutVisibility` is the late-bound probe `createLink` asks once the visibility read refuses, and `SharingServicePlugin` wires it. A host that constructs `ShareLinkService` itself without it keeps the visibility rule alone. The probe slice `SharingServiceOptions.securityService` returns gains an optional `explain`, the part of `ISecurityService.explain` the capability verdict reads. + - **`@objectstack/spec` (documentation only).** The `IShareLinkService.createLink` TSDoc states who may mint, replacing "you may only link-share a record you can yourself see". The `ISharingService.canManageShares` TSDoc describes the hierarchy-manager branch, which is implemented, and says it is not mint authority. No schema, key, type or export changes. + +### Patch Changes + +- 4916168: Sharing refusals and log lines, and the audit write-failure line, no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these two packages show to administrators and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/plugin-sharing`: the orphan-sweep line for record shares says every share on a deleted record goes, whatever its source, so a reused record id cannot inherit it; the same line for share links says a share link is a bearer token, so a reused record id must not inherit it; the write-gate failure line says a failed lookup is a refusal, never an abstention, because an abstention would hand the row to the other write authorities, which may admit it; the authored-row-write probe line says only an app-authored row-level policy that positively admits the row may lift the sharing refusal; the hierarchy-scope line says the resolver contract makes a resolver fail closed on a missing organization. The two sharing-rule refusals (no active organization; deleting a platform-global rule) drop their citations, since each sentence already says why. The `OrphanSweepSubject.issue` member's doc comment now says the member carries that reason in words. + - `@objectstack/plugin-audit`: the missing-table fix in the audit write-failure line says that on a fresh `os dev` boot the table exists in the sibling telemetry file and not in the primary one, so look there before concluding it was never created. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- db3fee3: A plain member's share-link list now answers: `GET /api/v1/share-links` is self-scoped for every signed-in caller, as ADR-0111 rules it + + Clause-②: no + + `ShareLinkService.listLinks` read `sys_share_link` under the caller's context. Both share-link doors force the list's `createdBy` to the caller, but the read still needed an object-level grant on `sys_share_link`, and the platform's member baseline does not grant one. So every plain member's list was refused, with or without an object filter, and the Share dialog, which loads this list when it opens, showed an error for them on every record. An admin's list answered. + + - The caller's own list is now read under the system context. This happens only when the caller has a non-empty user identity and the creator filter equals it. The read is constrained server-side to that identity, and each row it returns must pass the creator rule before it leaves. + - One creator rule now serves both `listLinks` and `revokeLink`. It never matches a caller with no user identity. Neither HTTP door reaches that case, because both answer 401 first, so for an internal caller with no user identity, `revokeLink` now refuses a link whose `created_by` is absent or empty instead of treating it as theirs. + - Every other list shape keeps the caller's context, as before: no creator filter, another user as creator, no user identity, or an admin listing someone else's links. A system caller keeps its bypass. + - The rows carry the same columns as before. The token comes back so the console can build the link URL, and the password hash never does. + - `@objectstack/spec`: the `IShareLinkService.listLinks` doc comment now describes the self-scoped own list. It previously said every listing is read under `context`. This is a doc comment only, with no type or export change. + - ⛔ No permission set changes, and no new grant on `sys_share_link`. +- f5b8e29: Share-link passwords follow the platform's credential rules (#21839). + + - **The stored hash never leaves the server.** The share-link mint response (`POST /api/v1/share-links`, and `ShareLinkService.createLink`'s return value) no longer carries `password_hash`. The list and the redemption result are projected the same way. A client that reads a link's password state keeps reading it from the redemption route's `NEEDS_PASSWORD` answer, as before. + - **The stored form is the platform's slow password hash.** New passwords are hashed with scrypt at the parameters account passwords use, instead of one salted SHA-256. Links minted before this release keep working: a stored password in a legacy form still verifies, and it is re-hashed into the new form on its first successful redemption. Every comparison is constant-time. A deployment that injects its own `hashPassword` / `verifyPassword` pair is unaffected, and its stored forms are left alone. + - **The password travels in a header.** Both public share-link routes (`GET /api/v1/share-links/:token/resolve` and `/:token/messages`) accept the `x-share-password` request header, the preferred form, because a header is not part of the request URL. The `?password=` query parameter is still accepted for compatibility, so current consoles keep working until they move to the header. `/messages` accepted only the query parameter on this mount before. + - **Cross-origin clients can send the header.** `X-Share-Password` is in the default CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS` in `@objectstack/plugin-hono-server`, which the `@objectstack/hono` adapter also applies). A deployment that passes its own `allowHeaders` is unchanged; add the header to that list to let a cross-origin client use it. + - **Public share-link answers are not cached.** Both public routes answer with `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, on both mounts (the sharing plugin's routes and the runtime dispatcher's `/share-links` domain). The authenticated create, list and revoke routes are unchanged. + - **Hashing works in WebContainer.** On StackBlitz WebContainer, where `node:crypto.scrypt` is incomplete, the password is hashed with the pure-JS scrypt from `@noble/hashes` (now a dependency of `@objectstack/plugin-sharing`, as it already is of `@objectstack/plugin-auth`), at the same parameters and in the same stored form. A hash made on either runtime verifies on the other. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [6ec54f0] +- Updated dependencies [a1ca156] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [be55fd2] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [fe10172] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [13a22d0] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 1e2ab500803..5c08c67e3a3 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index 27508c69eee..e7927df8dba 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,153 @@ # @objectstack/plugin-webhooks +## 17.7.0 + +### Patch Changes + +- 76fec88: Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off. + + Clause-②: yes (widening) + + - **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes. + - **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too. + - **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`. + - **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in. + - **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt. + - **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged. + - What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects. + - ⛔ No new export on any package entry, and no new elevation API. +- 568dc0b: Record-change payloads apply the same credential mask and internal-field omission as write responses. + + Clause-②: yes (widening) + + - **`data.record.created` / `data.record.updated` events.** The engine projects the event's `after` and `changes` bodies through `omitInternalFieldsFromWriteResponse` (`@objectstack/core`), the helper every external write response already uses: credential-class fields (`secret`, and `password` outside the exempt `managedBy` buckets) carry `SECRET_MASK` (or `null` when unset), and `internal: true` fields are omitted. The engine's own write result is unchanged, so a privileged in-process caller that reads the stored value back off `insert` / `update` still sees it. + - **Approval request snapshot.** The record snapshot an approval request stores (`payload_json`) applies the same rule when the request is opened. + - **Outbound webhook body.** The delivered body, and the delivery row that stores it, apply the same rule to `before`, `after` and `changes`. + - **Knowledge index documents.** `recordToDocument` takes the object definition as an optional fourth argument and skips credential-class and `internal` fields, under `'*'` and when a source names one explicitly. `KnowledgeService` passes the definition from the bound engine. + - **New public surface of `@objectstack/service-knowledge` (additive):** `recordToDocument` accepts the object definition as an optional fourth argument; existing three-argument calls behave as before. + - **Receivers see masked values.** Webhook receivers and realtime clients now get `SECRET_MASK` (or `null` when unset) for credential-class fields and no key for `internal` fields. + - **Existing rows are not rewritten.** Approval snapshots, webhook delivery rows and knowledge documents written before this change keep their stored bodies; reindexing a knowledge source refreshes its documents. + - The audit trail already masked these fields and is unchanged. No other accept set or public schema changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [76fec88] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/service-messaging@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index c41fa98abc1..33098b5b40c 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "17.6.0", + "version": "17.7.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 4a73eb8a3a5..5cfd28f3cbf 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,247 @@ # @objectstack/dogfood +## 0.0.47 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [6091136] +- Updated dependencies [f9bcd08] +- Updated dependencies [cc07862] +- Updated dependencies [e3ad492] +- Updated dependencies [4916168] +- Updated dependencies [f9f9f91] +- Updated dependencies [44072fc] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [97239c3] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [69a12a0] +- Updated dependencies [222ecc2] +- Updated dependencies [1caa603] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [fbe2deb] +- Updated dependencies [ee75aae] +- Updated dependencies [6e33b67] +- Updated dependencies [ab52182] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d487d2] +- Updated dependencies [6d67ad5] +- Updated dependencies [d7d5b4f] +- Updated dependencies [ca0dfb6] +- Updated dependencies [8b123c0] +- Updated dependencies [5e58193] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [3bddd4a] +- Updated dependencies [68c5ab7] +- Updated dependencies [520f66f] +- Updated dependencies [b793010] +- Updated dependencies [6f17d1d] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [81e69ca] +- Updated dependencies [85e29b8] +- Updated dependencies [086ad0a] +- Updated dependencies [0b82391] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [88fb5e8] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [f9a8eb8] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [2df621a] +- Updated dependencies [41b1333] +- Updated dependencies [1ca1eb0] +- Updated dependencies [bee8d1c] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [6cf1154] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [6ec54f0] +- Updated dependencies [5d095a0] +- Updated dependencies [a1ca156] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [1968d5e] +- Updated dependencies [417443e] +- Updated dependencies [be55fd2] +- Updated dependencies [31e3e00] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [234d1d8] +- Updated dependencies [fe10172] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [c7a60e1] +- Updated dependencies [e83c9f6] +- Updated dependencies [3eb38ae] +- Updated dependencies [33f9791] +- Updated dependencies [1c3a4d9] +- Updated dependencies [50b5e03] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [08adfea] +- Updated dependencies [07e933b] +- Updated dependencies [c9be1f1] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [833d57c] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [0fe0a59] +- Updated dependencies [cab6396] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [25eb7de] +- Updated dependencies [41a1135] +- Updated dependencies [255a777] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [5e0b489] +- Updated dependencies [07c842d] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [bc7747c] +- Updated dependencies [b238856] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [76fec88] +- Updated dependencies [13a22d0] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/connector-mcp@17.7.0 + - @objectstack/plugin-email@17.7.0 + - @objectstack/service-storage@17.7.0 + - @objectstack/trigger-record-change@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/plugin-approvals@17.7.0 + - @objectstack/plugin-audit@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/plugin-sharing@17.7.0 + - @objectstack/service-analytics@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/trigger-schedule@17.7.0 + - @objectstack/plugin-auth@17.7.0 + - @objectstack/mcp@17.7.0 + - @objectstack/verify@17.7.0 + - @objectstack/service-messaging@17.7.0 + - @objectstack/plugin-webhooks@17.7.0 + - @objectstack/example-crm@4.0.99 + - @objectstack/example-multi-package@0.0.6 + - @objectstack/example-showcase@0.3.21 + - @objectstack/connector-openapi@17.7.0 + - @objectstack/connector-rest@17.7.0 + - @objectstack/plugin-pinyin-search@17.7.0 + ## 0.0.46 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index b6a46783fc4..31f8331af34 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.46", + "version": "0.0.47", "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 0e416c4e6a8..3373271fb8b 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,112 @@ # @objectstack/downstream-contract +## 0.0.45 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 0.0.44 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index 5db56384641..8bfae3ba548 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.44", + "version": "0.0.45", "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 e320a30de94..f0cb683aa87 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,18 @@ # @objectstack/http-conformance +## 0.1.7 + +### Patch Changes + +- Updated dependencies [c205b6c] +- Updated dependencies [30af17e] +- Updated dependencies [eb9ef79] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [a0176ef] +- Updated dependencies [d16b9fb] + - @objectstack/core@17.7.0 + ## 0.1.6 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 696d6bd3d4a..9ec69ddd3d2 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.6", + "version": "0.1.7", "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 6c065f187ed..b83086a8a1e 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,192 @@ # @objectstack/rest +## 17.7.0 + +### Patch Changes + +- 49524f6: Withdrawing a public form from anonymous intake now takes effect on every intake door + + Clause-②: no + + When an administrator withdraws a public form, both anonymous form routes (`GET /forms/:slug` and `POST /forms/:slug/submit`) now answer `404 FORM_NOT_FOUND` and no record is created. Republishing the form restores both routes. If a service the routes need to resolve the form is registered but cannot be reached, both routes refuse the request instead of serving the form. +- 83b3d32: Public forms on a walled tenancy posture: saving or publishing a view whose public form cannot take anonymous intake now tells the author why, on the response. + + Clause-②: yes (widening) + + On a walled posture (`group` or `isolated` in force), an open public form whose object is walled by an organization column cannot take an anonymous submission: the submission carries no organization, and an insert without one into a walled object is refused. The two anonymous form endpoints already answer such a form as a withdrawn one (`404 FORM_NOT_FOUND`), and the administrator's read of the view (`GET /meta/view/:name`) already states why in `_diagnostics.warnings`. + + - **`@objectstack/metadata-protocol`**: saving the view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and a package's batch publish) now answers success with one `warning` advisory per such form, under `advisories`, with rule `public-form-intake-unavailable`. It is located at the form's `sharing` (for example `views[0].formViews.contact.sharing`), its `message` is the same text the administrator's read states, and its `hint` is the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. The write is never refused. The advisory reads the posture in force from the `tenancy` service, which is what the anonymous endpoints read: a single-posture deployment, a deployment whose walled posture is degraded to `single`, a deployment with no tenancy service, and a form bound to a tenancy-disabled object get no advisory, and a draft save is not judged. The publish refusal for an unstamped platform schedule flow still reads the requested posture, as before. + - **`@objectstack/metadata-core`**: the intake-availability rule moved here from `@objectstack/rest` and is exported, so the anonymous endpoints, the administrator's read and the publish advisory read one answer: `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` (`null` when the form can take intake, otherwise the object, the posture and the wall column; it judges the object's effective schema, with the injected `organization_id`), `anonymousFormIntakePosture(tenancy)` (the posture in force, as a tenancy service reports it), `anonymousFormIntakeUnavailableMessage` and `anonymousFormIntakeUnavailableRemedy` (the reason and its remedy), `anonymousFormSharingPath` and `anonymousFormObjectName`, and the type `AnonymousFormIntakeUnavailable`. + - **`@objectstack/rest`**: the anonymous form endpoints and the administrator's read import that rule instead of holding their own copy. Their answers are unchanged. +- a7ab047: Public forms on a walled tenancy posture: a form whose object is walled by an organization column is no longer offered to anonymous visitors. An anonymous submission carries no organization, and on a walled posture an insert into such an object without one is refused, so the form used to render and then answer `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` on every submit. Both anonymous form endpoints (`GET /forms/:slug` and `POST /forms/:slug/submit`) now answer it exactly as they answer a withdrawn form (`404 FORM_NOT_FOUND`), so an anonymous caller learns nothing about the deployment's tenancy. The administrator's read of the form (`GET /meta/view/:name`) states why in `_diagnostics.warnings`, located at the form's `sharing`, with the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. Forms bound to tenancy-disabled objects, and single-posture deployments, are unchanged. + + Clause-②: no +- e6dc7a2: The object-schema field mask (ADR-0106 D1) judges an action param that names another object's field through `objectOverride` against that object, not the one being served. + + Clause-②: yes (widening) + + **What a user saw.** A `delegated_admin` may invite members, and the invite door admits them, but `GET /meta/object/sys_user` served that principal no `invite_user` action. The action's `role` param is `{ field: 'role', objectOverride: 'sys_member' }`: it names `sys_member.role`. The mask read every param's `field` as a field of the served object, so a caller denied `sys_user.role` lost the whole action. A plain `member` lost it the same way. The member is now served the action too, and still not offered it: the action's `requiresMembershipReach` predicate excludes the member grade. + + **The rule.** A param whose `objectOverride` names another object reads that object's field. It is judged against the caller's readable fields on that object, and it is not a reference to the served object's fields. The action is still dropped when the caller cannot read the field there, and when that object's readable fields cannot be determined (no answer from the security service, a security service that throws, or an object that does not exist). Nothing about the other object is served on a guess. The rest of the param is still read against the served object: `visible`, an option's `visibleWhen`, `defaultValue`, and an explicit `name` that differs from `field`. A `name` that only repeats `field` is read as that field. With `defaultFromRow`, the param also reads `field` from the served object's row, so `field` is judged against the served object too. An exempt caller (platform admin, `isSystem`) is served the whole schema, as before. + + **The API (`@objectstack/metadata-core`), additive.** + + - `relateObjectSchemaMaskPosture(posture, ...documents)` completes a `project` posture for the documents it is about to mask. It reads the caller's readable fields on each other object their action params name through `objectOverride`. It runs after the fetch, because only the document names those objects. It returns every other posture, and any document with no such param, unchanged, and it never throws. + - The `project` member of `ObjectSchemaMaskPosture` gains two optional fields. `relate` asks the posture's question (same caller, same security service) about another object. `resolveObjectSchemaMaskPosture` sets it. `related` holds the answers. A `project` posture built without `related` gets no answers, so `applyObjectSchemaMask` drops every action with such a param. + - `applyObjectSchemaMask` folds each related read it withholds into the fingerprint, written as `object.field`. Two callers who are denied the same fields on the served object but differ on the other object get different validators. An unrestricted caller's ETag is unchanged. + - The shared contract fixture `FLS_CONTRACT_OBJECT` (`@objectstack/metadata-core/testing`) gains two actions whose params read `contact` fields through `objectOverride`. The contract's projection cases now require the readable one to be served and the denied one to be dropped. An exit that never relates its posture fails the contract by name. + + **Every exit relates its posture (`@objectstack/rest`, `@objectstack/runtime`).** These exits relate the posture after the fetch, before the projection: the shared item, layered and list chains, `RestServer`'s cached read and published read, and the runtime dispatcher's mask. The `/meta` diff route masks `fields` only and needs no relate step. + + **Measured on a showcase boot.** We read every object schema (78 objects, by-name read and list read) as five principals: a platform admin, an org owner, an admin, a `delegated_admin` and a `member`. Before and after this change, the only served action that moved is `sys_user.invite_user`, which is now served to the `delegated_admin` and the `member`. This repository has two authored params with `objectOverride`: `sys_user.invite_user`'s `role` and `sys_member.invite_user`'s `email` (on `sys_invitation`). The second was served to all five principals before and after. +- 3c7785d: A public form's explicit intake withdrawal at any metadata layer now holds: layering can only narrow anonymous intake, never re-open it + + Clause-②: yes (widening) + + - **What counts as a withdrawal.** A withdrawal keeps the form's `publicLink` and sets `sharing.enabled: false` or `sharing.allowAnonymous: false`. Only an explicit `false` counts: a switch that is absent is not a withdrawal. Removing the `sharing` block, clearing the `publicLink`, or deleting the view at one layer is not a withdrawal either. A sharing that names no public link withdraws nothing. + - **Organization-scoped saves and publishes.** A `view` save or draft promotion in the organization the anonymous form doors read is refused with `403 NOT_OVERRIDABLE` if it would leave open a form that the environment-wide definition withdraws. This check judges by the stored row: the organization's body is compared with the env-wide body of the row it is keyed by (the active env-wide row, else the package's artifact), and also with the env-wide view list the way the doors read it (a container-shaped body is expanded the way the list read expands it). Inside the row, a withdrawn form matches by its place (`form`, the same `formViews` entry, or `config`) or by its public slug, and either match is enough. So a renamed `formViews` key, a `form.name`, a move to another place, a listViews collision rename in the expansion, and a new or re-cased slug are all judged as the same form. A form that differs from every withdrawn form in both place and slug, such as a sibling in the same container, stays independent. The check also covers an organization copy that was already open before the withdrawal, the next time it is saved. The message names the remedies: save the overlay withdrawn, or publish the form from its environment-wide definition. An organization-scoped save that keeps the form withdrawn is still accepted. + - **Anonymous form doors.** `GET /forms/:slug` and `POST /forms/:slug/submit` judge by the name of the view item they serve. Beneath the organization's read they read the env-wide view list, and they serve a form only when the env-wide item of the same name does not explicitly withdraw a form in the same place or with the same slug. A withdrawn form answers `404 FORM_NOT_FOUND` on both doors and creates no record. A form that is open at every layer is served as before. A form that only an organization carries is still served there. A different view that uses the same slug is a different form, and the two never close each other. + - **Package-shipped forms.** A package's form is part of the env-wide definition, not a separate layer beneath it. A package artifact that was parsed by the stack schema (strict `defineStack`, the default) carries the schema's default `enabled: false`, so a shipped form that keeps its link without switching `enabled` on is an explicit withdrawal (fail closed). An artifact that reached the runtime without that parse (`defineStack(..., { strict: false })` or a hand-built manifest) is judged as written: there a switch it omits is absent, which is not a withdrawal. The env-wide definition is the administrator's switch: an env-wide save may open a form the package ships closed. + - **Known limit: packages and names.** A withdrawal of a view name closes that name in every package. When two packages ship a view of the same name, one package's withdrawal also closes the other package's form of that name: it may over-close, never under-close. Per-package precision is tracked in #21934. A publish judges the draft it promotes under the same package key: with two packages holding a draft of the same view in one organization, each draft is judged on its own publish. + - **Known limit.** The doors match by served item name, and the save check runs only on an organization-scoped save or publish. An organization overlay that was stored before the env-wide withdrawal, or that a rollback or commit-revert restores, can still be served if it keeps the form open under a different key or place than the env-wide definition. Withdraw the form in that overlay to close it. Rollback and commit-revert restores are not gated by the save check. + - **Behaviour change.** Between 17.6.0 and this fix, an organization overlay that published a form the environment-wide (package) definition withdrew was honoured: the doors served the organization's copy. That behaviour never shipped in a release, and it is reversed on purpose. The environment-wide withdrawal now wins. + - **`@objectstack/metadata-core`** exports the shared judgement `anonymousFormIntakeWithdrawnIn` (a new, additive public export). Both the doors and the save path read it. +- 6dd99b8: Public forms: every declared means of withdrawing a form from anonymous intake is now honoured by every anonymous form door. Which forms a `view` opens to anonymous intake is now decided by one rule, `anonymousFormIntakeCandidates` (new in `@objectstack/metadata-core`, alongside `anonymousFormIntakeSlugs`, `anonymousFormIntakeSlug` and `publicFormSlug`), read by both the anonymous form endpoints in `@objectstack/rest` and the organization-scoped `view` write check in `@objectstack/metadata-protocol`, so the two can no longer disagree. A form is served anonymously only when its `sharing` config declares public sharing as `SharingConfigSchema` defines it: `sharing.enabled: true`, `sharing.allowAnonymous: true` and a `sharing.publicLink` slug. `enabled` defaults to `false`, so a form that set only `allowAnonymous` and `publicLink` is no longer served on the anonymous endpoints (`404 FORM_NOT_FOUND`). Migration: add `enabled: true` to the form's `sharing` block (and to any stored overlay of it) to keep it public; see the public forms guide. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [0e10be6] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/service-package@17.7.0 + - @objectstack/observability@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 93e187fa09a..9e4f9c9f914 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.6.0", + "version": "17.7.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 89b20b88d0a..7a7ca6cd088 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,691 @@ # @objectstack/runtime +## 17.7.0 + +### Minor Changes + +- 909229e: A job pulls a mapping's connector source by declaration — `pull: { mapping }` — and every job runs as the `organization` it declares (#20281). + + Clause-②: yes (widening) + + - **`JobSchema.pull`** (`@objectstack/spec/system`). A third run form beside `body` and `handler`: `{ mapping: '' }`. On each run the platform pulls that mapping's `connectorSource` and writes the rows through the import runner. It carries no code. The key is refused beside `body` or `handler`, because one of the two run forms would never run. `body` with `handler` stays legal, and the body still wins. A job must now declare one of `body`, `handler` or `pull`. `pull` is closed: an unknown key inside it is refused. + - **`JobSchema.organization`**. The organization a job runs as. It applies to the body's `ctx.api`, to the handler's new `executionContext`, and to the pull's reads and writes. The value shape is the scheduled flow's: a non-empty `sys_organization.id`. A near-miss spelling (`organizationId`, `orgId`, `tenantId`, …) is refused at parse and pointed at the key. + - **`defineStack`, and so `os validate`**, refuses a job whose `pull` names a mapping the stack does not declare, or a mapping with no `connectorSource`. The refusal is the existing `STACK_CROSS_REFERENCE_INVALID` envelope. + - **`IAutomationService.pullConnectorSource`** (`@objectstack/spec/contracts`, with `ConnectorSourcePullRequest`, `ConnectorSourcePullResult` and `ConnectorSourcePullSummary`). The connector sync executor is now on the `automation` service. `@objectstack/service-automation`'s engine serves it from the executor `AutomationServicePlugin` attaches at init (`AutomationEngine.setConnectorPullSource`). A bare engine refuses with `SERVICE_UNAVAILABLE` (503). + - **The job binder** (`@objectstack/runtime`, `scheduleAppArtifactJobs`) schedules a `pull` job on every door: the boot, and `os package install` on install and rehydrate. Each run calls `pullConnectorSource` through the service registry. A refused pull fails the run, and `retryPolicy` applies. A pull whose rows the import runner refused records the run `degraded`, with the counts. A pull naming a mapping the artifact does not carry is not scheduled, and neither is one whose mapping has no `connectorSource`, nor one on a kernel whose `automation` service cannot pull. Each case is logged at `warn` with the reason. `collectJobsWithoutBody` does not name a `pull` job that binds, so `os package install` installs one. The result gains `pulls` and `missingOrganization`. + - **The organization, judged at bind** by the posture rule scheduled flows use (`resolveScheduledWorkPolicy`). Every run carries `{ isSystem: true, tenantId: }`, or `{ isSystem: true }` for a job that declares none. Under `single` the key is not required. Under `group` it is optional; an undeclared job is scheduled and named once at `warn`, because a tenant-scoped row it writes is refused. Under `isolated`, with package-authored scheduled work switched on, it is **required**. **Action on such a deployment:** declare `organization` on each packaged job, or the job is not scheduled; the error log names the job. Until now such a job was scheduled, and every tenant-scoped write it made was refused at the write. An unrecognized `OS_TENANCY_POSTURE` withholds every job (`scheduled-work-policy-unreadable`) instead of guessing whether a declaration is required. + - **Texts this makes true.** The `mapping.connectorSource` description, the `connector.syncConfig` tombstone prescription and the `connector-sync-keys-retired` upgrade entry said "nothing schedules a pull yet". They now name the `job` `pull` that drives it. + + Nothing that parsed before is refused now. Every new refusal falls on a key that did not exist before this change. +- 96a9719: feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790) + + Clause-②: yes (widening) + + A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it. + + **⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '' (): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification. + + What else changes: + + - **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`. + - **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential. + - **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials. + - **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing. + - **Refused now, loudly**: + - With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again. + - The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret. + + +- 1d0600b: An app installed with `os package install ` now runs its `type: 'script'` action bodies and its body hooks, and MCP `list_actions` lists a script action only when `run_action` can run it (#21321). + + Clause-②: yes (widening) + + - **`@objectstack/runtime`.** New export `bindAppArtifactHandlers(ql, bundle, { appId, logger, source? })`. It binds every action `body` of an artifact through `ql.registerAction`, and every hook `body` and bundle function through `ql.bindHooks`, all under the owner `app:`. `appArtifactHandlerOwner(appId)` returns that owner key. Each call first removes the action handlers and hooks the same owner bound before. A reinstall therefore leaves one handler per action, and an action or hook that the new version dropped stops running. `AppPlugin.start` now binds through this function, with the same log lines and the same results for a boot artifact. + - **`@objectstack/runtime`, MCP `list_actions`.** A `script` action is listed only when the engine has a handler registered for it. The check reads `listRegisteredActions()` and uses the same object and key order as `run_action`. Before, a declared `target` or `body` was enough to be listed, so `list_actions` could list an action that `run_action` refused with "No handler registered". An engine without `listRegisteredActions` gets no script actions listed. Declarative update actions and `flow` actions are listed as before. + - **`@objectstack/cloud-connection`.** The install-local plugin calls `bindAppArtifactHandlers` on `POST /api/v1/marketplace/install-local` and when it rehydrates its ledger at `kernel:ready`. Before, an installed package's script actions answered REST `404 RESOURCE_NOT_FOUND` and MCP "No handler registered", before and after a restart, and its body hooks never ran. The same artifact booted with `os start --artifact` was not affected. +- b206403: The CLI's one-shot commands no longer write to the database as a side effect of booting. No `os migrate *`, `os meta resync`, `os secret orphans` or `os storage orphans` run loads the app's inline seed data, apply and delete modes included, and every mode that writes nothing now boots read-only. + + Clause-②: yes (narrowing) + + + + **BREAKING** — a no-write run of `os migrate value-shapes`, `os migrate recorded-by`, `os migrate resume`, `os secret orphans` or `os storage orphans` at a database that lacks a table it reads now exits 1, where it used to exit 0. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What was wrong.** Eight commands booted the full data stack in a mode their documentation says writes nothing: `os migrate value-shapes` (scan), `summary-nulls`, `files-to-references` and `recorded-by` (dry run), `os migrate resume` (list), `os secret orphans` and `os storage orphans` (report), and `os meta resync` without `--yes`. That boot ran schema sync and the app's inline seed loader. The seed loader upserts every seeded row, so each run bumped `updated_at`, stamped `organization_id` on seeded rows that had none, and put an operator's edit to a seeded row back to the seed's value. On `examples/app-crm` that was all 28 seeded rows on every run. On a database behind the app's schema, the boot also added columns and created tables. The apply and delete modes ran the same seed loader alongside the write the operator confirmed. + + **What changes for an operator.** + + - Every mode that writes nothing boots the way `os migrate plan` does: the schema sync is held back, no seed rows are written, and a SQLite file that does not exist is not created. The database is left byte-identical, and the report is the same as before. + - No one-shot CLI boot loads the app's inline seed data. `--apply`, `--delete`, `os migrate resume --run` and `os meta resync --yes` write what they report and nothing else. Seeding stays with `os dev` and `os serve`. + - The deferred schema sync now covers every SQL datasource the boot connects, not only the default one. `os migrate plan` lists a second datasource's pending tables, and `os migrate apply` creates them after you confirm. + - One edge changes: a no-write run pointed at a database that lacks a table it reads (a SQLite file that does not exist, a database that was never booted, or the wrong `--database-url`) refuses and exits 1 instead of creating the table and reporting nothing. Point `--database-url` at the deployment's database, or boot the deployment once first. `os secret orphans --json` answers that refusal with `"error": "scan_failed"`. + - `os migrate value-shapes --json` prints one JSON document when the scan fails its gate. It used to print a second one, `{"error":"EEXIT: 1"}`. + + **For embedders of `@objectstack/runtime`.** `createStandaloneStack` accepts `armLifecycleSweep` (default `true`). With `false`, the ADR-0057 lifecycle sweep (rotation, retention reaping, archiving and the dangling-reference audit that rides its clock) is never armed on that boot, and an explicit `sweep()` call on it returns an empty report. The CLI passes `false` on every one-shot boot. +- 2f837a5: fix(runtime)!: the in-process reader contexts refuse the stored-metadata-body family's EVALUATE shapes and serve what a write returns, the way the generic data door does (#21454) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what an action or hook body's object API, an action handler's scoped API and an action handler's engine handle accept when they read the two stored-metadata tables. A read there that filters, sorts or groups on the stored body column or on a content-hash column, a read that names one of those columns in an explicit search-field list, and a `count` carrying such a filter, ran before this release and now answer the generic data door's `400 INVALID_FIELD` before the query runs. The route: filter, sort, group and search those tables by their scalar columns (the type, the name, the state and the like), and read the bodies with a plain list, which is served projected — the body as its type's read projection, the content hash in keyed form. A default search with no field list is not refused: it is narrowed to the columns the door serves. Every other column of the two tables, and every other object, is unchanged. It ships as `minor` under the launch-window convention for accept-set narrowings. + + - **`@objectstack/metadata-protocol`** now exports the generic data door's four evaluate-refusal predicates — `storedMetadataBodyGroupingRefusal`, `storedMetadataBodyPredicateRefusal`, `storedMetadataHashEvaluateRefusal` and `storedMetadataSearchRefusal` — so the `@objectstack/runtime` reader-context seam refuses the same shapes through the door's own predicates rather than a second copy. Additive: nothing that imported the package before is changed. + - **`@objectstack/runtime`** extends the stored-metadata reader-context seam (`ctx.api.object(...)` for action and hook bodies, a handler's `ctx.api`, and `ctx.engine.find`): a filter, sort, grouping or search that would evaluate the stored body or content hash of `sys_metadata` / `sys_metadata_history` is refused with the door's `INVALID_FIELD` / 400 before the query runs (a `count` with such a predicate included); a default `$search` is narrowed to the door's served field set rather than refused; and the row a write verb returns is served projected and keyed. The engine's own action verb (`ScopedRepo.execute`) is unreachable from a served body and is left untouched. +- 6c5697d: fix(runtime,cloud-connection)!: a job's sandboxed `body` is scheduled on every door that brings an artifact in, and install-local refuses an enabled job with no `body` (#21489) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package that declares an **enabled job with no `body`**. Such a job names its code only through `handler` — a `defineStack({ functions })` entry, which travels in the artifact's runtime module and never in the package JSON this door installs — so it used to install with a 200 and never run, hot or after a restart, with nothing saying so. + + - **Job bodies run.** A job's sandboxed `body` (`JobSchema.body`, the hook body shape) is now scheduled on every door that brings an artifact in: the boot (`os start --artifact`, a `defineStack` config) and install-local, on install and on every rehydrate after a restart. One binder does it for all of them. With both `body` and `handler` declared, the `body` wins. The body runs in the QuickJS sandbox with `ctx.api` (as system: a job has no caller), `ctx.log` and `ctx.crypto` behind its declared `capabilities`. The job's `timeoutMs` is its one time limit; with none, a job body gets a 5000 ms CPU budget. A body may return `{ outcome: 'degraded', reason }` to report a run that did not do its work. + - **A package's jobs stop with it.** Re-scheduling a package's jobs replaces its set: a reinstall whose new version drops, disables or can no longer run a job cancels that job, and a version with no jobs cancels them all. Uninstalling a package cancels its scheduled jobs through a new uninstall cleanup, `runtime.package-jobs`, on the protocol's uninstall-cleanup registry, so install-local's `DELETE` and the protocol's package uninstall both stop them and report it in `cleanups`. Another package's jobs are never touched. + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, names each refused job and the function its `handler` declares, and installs nothing: nothing is registered, persisted or scheduled. A disabled job (`enabled: false`) is not judged. A package installed by an earlier version keeps rehydrating; its handler-only job is reported at `warn` and does not run. + - **CLI.** `os package install` prints a refusal's code beside its status (`Install failed (422 VALIDATION_ERROR): …`), for every refusal alike. + - **Spec.** The shipped liveness ledger records `job.body` (`language`, `source`, `capabilities`, `memoryMb`) as live, so `os validate` / `os build` no longer warn that a job's `body` is planned and not read yet. `body.timeoutMs` stays refused on a job. `JobSchema.body`'s description and the `defineJob` example no longer say to keep a `handler` until the runtime runs job bodies. + - **Unchanged:** a `handler` job on a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) still runs its `functions` entry; a package without jobs installs exactly as before. + + The route for a refused package: give each enabled job a `body` (sandboxed JS that reaches data through `ctx.api`), or boot the artifact with `os start --artifact`, which loads its runtime module. It ships as `minor` under the launch-window convention for accept-set narrowings. +- 9a4182a: fix(spec,runtime,cli)!: the in-memory (mingo) engine is no longer a boot store — every boot door refuses it and names SQLite instead (#21492, #21572) + + Clause-②: yes (narrowing) + + + + **BREAKING**: the in-memory (mingo) engine can no longer be selected as the store a server, a migration or an embedded stack boots on. It refuses every tenant-scoped read by design, so a boot on it signed a user in and then answered `503` to every data request; there was nothing working to keep. The retirement is made at the declaration: `@objectstack/spec`'s driver table withdrew `memory`, `mingo` and `in-memory` from its selection face (they stay on the config-contract face beside `inmemory`), and every boot door refuses the engine with one sentence that names the replacement. + + - **`@objectstack/spec`** — `DATABASE_DRIVER_SELECTION_ALIASES` no longer lists `memory`, `mingo` or `in-memory`; `DATABASE_DRIVER_SELECTION_IDS` no longer lists `memory`; `resolveDatabaseDriverId` answers `undefined` for all four spellings. `resolveDriverId`, `DRIVER_ID_ALIASES`, `BUILTIN_DRIVER_IDS` and the `memory` config contract are unchanged. + - **`@objectstack/cli`** — `--database-driver memory` is refused while the flags parse (`os dev`, `os start`); `OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory` is refused before `os dev` or `os start` prints its Database row; `os serve`'s legacy path refuses the spellings and the `memory://` / `mingo://` schemes as a fatal boot error. The help no longer offers `memory://`. + - **`@objectstack/runtime`** — `createStandaloneStack`, `createDefaultHostConfig` and `resolveStandaloneDatabase` (every ordinary `os dev` / `os start` / `os serve` boot and every `os migrate` subcommand) refuse the spellings, the `memory://` and `mingo://` schemes, and a project whose default datasource is declared with `driver: 'memory'`. `resolveProjectDatabaseUrl` refuses a retired driver selection ahead of every rung, and its `ProjectDatabaseUrlSource` type no longer has the `'memory-driver'` member. `ResolvedStandaloneDatabase.driver` never names `memory`. Two exports are added for hosts that refuse the engine themselves: `namesRetiredMemoryEngine` and `retiredMemoryEngineMessage`. + - **Unchanged:** the `@objectstack/driver-memory` package; a declared non-default datasource with `driver: 'memory'` and a directly constructed `InMemoryDriver`, both still built; SQLite's dev step-down, whose last rung is still this driver. + + Migration — one flag change: + + - FROM `os dev --database-driver memory` (or `OS_DATABASE_DRIVER=memory`) TO `os dev --fresh` for a throwaway database deleted on exit. + - FROM `OS_DATABASE_URL=memory://…` / `--database memory://…` / `databaseUrl: 'memory://…'` TO `:memory:` (SQLite's own in-memory database), e.g. `OS_DATABASE_URL=:memory:`. + - FROM a default datasource declared `{ driver: 'memory' }` TO a SQLite one, e.g. `{ driver: 'sqlite', config: { filename: ':memory:' } }`. + + No shipped example selects the engine. It ships as `minor` under the launch-window convention for accept-set narrowings. +- bd70706: fix(runtime)!: an app-authored body may not bind a hook to, or write, the stored-metadata tables (#21520) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what an app-authored body may do with the two stored-metadata tables, `sys_metadata` and `sys_metadata_history`. For an app-authored body, the metadata protocol is now their only writer: a change to metadata goes through the metadata API, where it is validated and its provenance is recorded. + + - **Binding.** A hook with a sandboxed `body` whose `object` names either table, alone or in a list, is no longer bound. The refusal is made at registration, at the one point every body hook becomes a handler, so it holds on every door a hook binds by: a code bundle or boot artifact, an installed artifact, and a hook authored at runtime through the metadata door. It carries `PERMISSION_DENIED` / 403, names the metadata API, and is recorded against the hook in the bind log at `error` (thrown under strict binding). A wildcard (`'*'`) body hook still binds; its body is not run for either table's events, and the bind says so once at `info`. + - **Writing.** A sandboxed action or hook body's write of either table through `ctx.api` — every write verb, inside a transaction or not, with or without elevation — answers `PERMISSION_DENIED` / 403 before the write runs, so nothing lands and the answer does not depend on what the write names. + - **Unchanged:** a body's reads of the two tables (still served as the generic data door serves them); host code that registers its own action handlers or hooks; the platform's own hooks, which are code and still fire on the metadata door's save; and every other object. + + The route: change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) rather than from a body, and bind hooks to the objects an app owns. No shipped example binds a body hook to either table or writes one from a body. It ships as `minor` under the launch-window convention for accept-set narrowings. +- 045b946: fix(runtime,cloud-connection)!: install-local refuses a hook with no `body` and a job `body` that does not bind, and withholds such a hook on rehydrate (#21585) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses two more kinds of package it used to install with a 200: + + - **A hook with no `body`.** A hook in the deprecated function-name `handler` form names code that travels only in an artifact's runtime module, never in the package JSON this door installs. Such a hook used to install and then either never fire or bind by name to a function the package does not ship. Every hook is judged, since a hook has no on/off switch. A hook that carries both a `body` and a `handler` installs as before: its `body` wins. + - **An enabled job whose `body` does not bind.** The door used to judge only that a job `body` was present. It now judges that the body binds, by the declaration's own parse of `JobSchema.body`, the same parse the scheduler binds by. So a job whose `body` is an expression (L1) body, or carries `body.timeoutMs`, is refused instead of installed and never scheduled. + + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, the answer the door already gives an enabled job with no `body`. One answer names everything the door cannot run: each hook and the function its `handler` names, each job and its handler, and each refused job `body` with the key the declaration refuses. Nothing is installed: nothing is registered, persisted, bound or scheduled. `os package install` exits non-zero and prints the code beside the status. + - **Rehydrate.** A package installed by an earlier version keeps rehydrating after a restart. Its body hooks bind as before. A hook of it with no `body` is reported at `warn` by name and is **not bound**: this door carries no runtime module, so the hook's `handler` can never name the package's own code. Its job with no runnable `body` is reported and not run, as before. + - **Runtime.** The binder exports the two judgements the door reads: `collectHooksWithoutBody`, and `collectJobsWithoutBody`, which also names a job whose `body` does not bind. `bindAppArtifactHandlers` takes `withholdHooksWithoutBody`, which a door that carries no runtime module sets, and reports the hooks it withheld as `withheldHooks`. + - **Unchanged:** a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) binds an app's handler hooks to its own functions exactly as before. Hooks authored through the metadata API are unchanged too. A package whose hooks carry a `body` and whose enabled jobs carry a valid `body` installs exactly as before. + + The route for a refused package: give each hook a `body` (sandboxed JS, the form actions and jobs use), and correct each job `body` to the declared shape. That shape is a sandboxed JS body whose time limit is the job's own `timeoutMs`, and `os validate` reports the same refusal. Alternatively, boot the artifact with `os start --artifact`, which loads its runtime module. This ships as `minor`, under the launch-window convention for narrowings of an accept set. +- 316be32: fix(runtime)!: an app-authored body may not read the stored-metadata tables either; it reaches them through the metadata API only (#21594) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what an app-authored body may do with the two stored-metadata tables, `sys_metadata` and `sys_metadata_history`. With the binding and write refusals already in this release, an app-authored body now reaches them through the metadata API only. + + - **Reading.** A sandboxed hook, action or job body's read of either table through `ctx.api` answers `PERMISSION_DENIED` / 403 before the read runs. That covers `find`, `findOne`, `count` and `aggregate`, inside a transaction or not, with or without elevation, and whatever filter, sort, grouping, search or projection the read carries. A body is served nothing of these tables, neither the stored row nor a projection of it, and the answer does not depend on what the read asks. A hook body that reads them fails the write that fired it. + - **Subject record.** An action body is not handed a row of either table as its subject record either. The `/actions` door loads an action's subject row before it dispatches. When that row is from either table, the call answers the same `PERMISSION_DENIED` / 403 before the body runs, instead of handing the body the row as `ctx.record`. That covers an action declared on either table (through a bundle, an installed package or the metadata door) and an object-less action addressed under one. A call that carries no record hands the body nothing and runs as before. + - **The route.** A body that read either table through `ctx.api.object(...)` was served the body projected and the content hash keyed; read metadata through the metadata API instead: `GET /api/v1/meta/:type/:name` for a definition, and `GET /api/v1/meta/:type/:name/history` for its versions. The refusal names that route. No shipped example reads either table from a body. + - **This supersedes, for bodies, two earlier entries of this release:** the served read of these tables, and the evaluate-shape refusals (`INVALID_FIELD` / 400) and default-search narrowing of that read. For a body, all of these now give way to this refusal. It also supersedes the binding-and-write entry's note that a body's reads are unchanged. + - **Unchanged:** host code that registers its own action handlers (its `ctx.api`, `ctx.engine.find` and subject record are still served as the generic data door serves these tables, with the door's evaluate refusals); the binding and write refusals; the platform's own readers; the generic data door and the metadata API; and every other object. + + It ships as `minor` under the launch-window convention for accept-set narrowings. +- 83e2fee: fix(runtime,cloud-connection)!: install-local refuses an enabled job whose `pull` does not bind, as it refuses a job `body` that does not bind (#21672) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package whose enabled job declares a `pull` that does not bind. It used to install such a package with a 200, and the job was never scheduled; only a server warn said so. + + - **What does not bind.** The `pull` names a mapping the package does not declare, or a mapping with no `connectorSource`, or the job declares `body` or `handler` beside its `pull`. The door judges this with the scheduler's own judgement, so the door and the scheduler cannot disagree. `defineStack` and `os validate` already refuse the same `pull`, so only a hand-edited package reaches the door with one. + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, the answer the door already gives an enabled job whose `body` does not bind. One answer names everything the door cannot run, and gives each such job the reason its `pull` does not bind, prefixed with the key it names (`pull.mapping: …`). Nothing is installed: nothing is registered, persisted or scheduled. `os package install` exits non-zero and prints the code beside the status. + - **Unchanged.** A pull job naming a declared mapping with a `connectorSource` installs and is scheduled as before. A disabled pull job does not block its install. A package installed by an earlier version still rehydrates after a restart, and its pull job that does not bind is not scheduled, with a warn naming the job and the reason, as before. + - **Runtime.** `collectJobsWithoutBody` now names an enabled job whose `pull` does not bind, and `JobWithoutBody` gains an optional `pullRefusal`: the reason the scheduler gives when it does not schedule the job. Such a job carries no `bodyRefusal`. + + The route for a refused package: declare the mapping the job's `pull` names in the package, with a `connectorSource` naming the `rest` or `openapi` connector it reads from, or correct the `pull` as the refusal says. `os validate` refuses the same `pull`. This ships as `minor`, under the launch-window convention for narrowings of an accept set. +- 309224d: A refused flow resume answers the engine's own code, on the REST resume door and the MCP `resume_run` tool alike, as the 17.1.0 release notes, `client.automation.resume()`'s documentation and the flows guide already state (#21724). + + Clause-②: yes (widening) + + - **What moves on the wire.** `POST /api/v1/automation/:name/runs/:runId/resume` used to hand the error builder a status and no code for the engine's refusals, so `error.code` was derived from the status. It now carries the engine's code: + + | Refusal | Status | `error.code` before | `error.code` now | + |---|---|---|---| + | a screen input that breaks the screen's declared fields | 400 | `VALIDATION_ERROR` | `INVALID_SCREEN_INPUT` | + | a signal that writes an engine-reserved `$` name | 400 | `VALIDATION_ERROR` | `INVALID_SIGNAL` | + | an unknown run, a run whose flow is gone, or a run whose paused node was edited away | 404 | `RESOURCE_NOT_FOUND` | `RUN_NOT_FOUND` | + | the suspended-run store is unreadable | 503 | `SERVICE_UNAVAILABLE` | `STORE_UNAVAILABLE` | + | another resume already holds the run | 409 | `RESOURCE_CONFLICT` | `RESUME_IN_PROGRESS` | + + `PERMISSION_DENIED` (403) is unchanged. No status moves, nothing that was accepted is refused, and a refused resume still leaves the run paused, so a corrected resume still completes it. A caller that branched on the status-derived code reads the documented one instead: `err.code` from `client.automation.resume()` is now `INVALID_SCREEN_INPUT`, `INVALID_SIGNAL` or `RUN_NOT_FOUND`, which is what lets it tell a bad screen value from a reserved signal name. + - **`@objectstack/spec` — `minor`.** The ADR-0112 error-code ledger registers `INVALID_SCREEN_INPUT` under `@objectstack/service-automation`, beside `INVALID_SIGNAL` and `RUN_NOT_FOUND`. The engine already returned it and the docs already promised it, but `ErrorCode`, and therefore `ApiErrorSchema`, refused it. The published vocabulary gains one member and loses none. + - **`@objectstack/runtime` — `minor`.** The resume door's answer set gains the five codes above. Both doors read one classifier, so the REST route and `resume_run` answer the same code for the same engine result. +- e83c9f6: A package install refused by the ADR-0087 D1 protocol handshake now answers `422 OS_PROTOCOL_INCOMPATIBLE`, with its structured diagnostic in `error.details`. It used to answer the `500` server-fault fallback, with the diagnostic only inside the message. + + Clause-②: yes + + - **`POST /api/v1/packages`:** a manifest whose declared range (`engines.protocol`, then `engines.platform`, then `engine.objectstack`) excludes this runtime's protocol major answers `422`, with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'` and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. `error.message` is unchanged, and the command after its `Run:` equals `migrateCommand`. Nothing is installed, so a later `GET /api/v1/packages/{id}` still answers `404`. + - **The no-protocol-service fallback:** in a composition without a `protocol` service, `POST /api/v1/packages` used to install such a manifest. It now runs the same handshake and answers the same `422`. + - **`@objectstack/metadata-core`:** `ProtocolIncompatibleError` declares `status` and `statusCode` `422`, with `code` as the literal `'OS_PROTOCOL_INCOMPATIBLE'`. It also carries a `Symbol.for` brand, and the new `isProtocolIncompatibleError(e)` recognises it across module instances, where `instanceof` would not. Any other caller that resolves the error (the boot-time `AppPlugin` load included) reads `422` rather than the `500` fallback. + - **`@objectstack/spec`:** the error-code ledger's `OS_PROTOCOL_INCOMPATIBLE` row states its status (422) and the door that carries the diagnostic. The vocabulary does not change. + + A client that branched on `500` for this code should branch on `422`, or on `error.code`. None was found in this repository, the SDK or the console. + + +- 025008a: fix(runtime,cli): a plain `os dev` now self-heals safe schema drift on restart and provisions the `telemetry` sibling database, as `content/docs/deployment/cli.mdx` already says (#21733) + + Clause-②: yes (widening) + + - **What was broken.** A config with no instantiated `plugins[]` (every fresh scaffold) boots through the standalone stack. Its `default` datasource was built without `autoMigrate: 'safe'`: only the config-load fallback that a host config or `OS_MODE=off` takes carried it. So safe drift was never applied on restart. An example is a per-organization unique index that an older release left non-NULL-safe. Meanwhile the driver's drift line and `os migrate plan` both said the change was "auto-applied at boot under dev autoMigrate: 'safe'". The same boot never provisioned the `.telemetry.` sibling either. + - **The fix.** The dev self-heal decision now lives in one place, `devAutoMigrateConfig` in `@objectstack/runtime`. That is the driver kinds whose connection contract declares `autoMigrate` (sqlite, postgres, mysql), on a dev boot. The standalone stack, the CLI's config-load fallback and the telemetry sibling all read it, so no kind gains or loses the self-heal relative to the host path. The telemetry provision is one helper (`provisionTelemetryDatasource`) that both serving paths call, under the same `resolveTelemetryDbPath` rule: dev default-on for a file-backed SQLite primary, `OS_TELEMETRY_DB=0` to opt out, `OS_TELEMETRY_DB=` to opt in anywhere. + - **Only a serving boot self-heals.** The standalone stack arms the self-heal on an explicit `dev: true`. That is what `os dev` passes. It does not arm it on the `NODE_ENV=development` default that its sqlite step-down still takes. A one-shot command (`os migrate *`, `os meta resync`, …) passes no `dev`, so it never applies drift its operator did not confirm, whatever `NODE_ENV` says. Production boots are unchanged: the definition carries no `autoMigrate`, and the SQL driver refuses it under `NODE_ENV=production` anyway. + - **Why minor.** `@objectstack/runtime` gains two exports on its only entry, `devAutoMigrateConfig` and its `DevAutoMigrateConfig` type. That is the widening: the existing decision moved out of the CLI so that the CLI reads it rather than keep a second copy. No config key, schema or accept set moves. `@objectstack/cli` is a `patch`: its fix restores documented behaviour and adds no public surface. +- 753e7a1: fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host's `default` (#21922, #21944) + + Clause-②: no (narrowing) + + A code-defined datasource (one the installed artifact declares in `*.datasource.ts`, or the host's own `default`) is read-only: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin service states "code wins on collision". The boot restore broke both. It registered every stored `datasource` row in `sys_metadata` over whatever the runtime had registered from code, so after a restart a row left under a code-defined name was served by the admin door, editable there when it carried `origin: 'runtime'`, and handed to pool rehydration. A stored `default` row opened a second live pool named `default` on the row's own connection. The metadata door also still saved edits to `default`, the one code-defined datasource no package declares. + + The runtime now keeps one in-memory set of the datasource names it registers from code, on the kernel service `code-datasource-names`: `AppPlugin` adds the datasources the artifact declares and `DefaultDatasourcePlugin` adds `default`, both in `init()`, so the set is complete before any `start()` runs. The boot restore skips a stored row under a name in that set, and the metadata door's code-datasource check reads the same set. + + **BREAKING — what moves for consumers.** + + - After a restart over a stored row under a code-defined datasource's name, `GET /api/v1/datasources` serves the code definition (`origin: code`) instead of the row, and `PATCH /api/v1/datasources/:name` answers `400 DATASOURCE_ADMIN_ERROR` ("… is code-defined and cannot be edited at runtime.") where it answered 200 for a row that carried `origin: 'runtime'`. + - No live pool is opened from such a row at boot. + - `PUT /api/v1/meta/datasource/default` answered 200 and now answers `403 NOT_OVERRIDABLE`. `DELETE /api/v1/meta/datasource/default` with no stored row answered 200 and now answers the same `403`. The refusal's remedy names the host's database configuration (the database URL the server starts with), which is what defines `default`; every other code-defined datasource's refusal still names its `*.datasource.ts` source. + - The skipped row is kept, and the boot logs one warning naming it. + + **Remedy.** + + - To change a code-defined datasource, change its code definition and redeploy: its `*.datasource.ts` source, or the host's database configuration for `default`. + - A row the boot warning names is removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it. + + **Unchanged.** A runtime datasource with no code twin restores, saves and deletes through both doors as before. A host that composes neither `AppPlugin` nor `DefaultDatasourcePlugin` registers no set, and its stored rows restore as before. While a stored row exists under a code-defined name, `GET /api/v1/meta/datasource/:name` still serves that row (the metadata door reads its stored overlay first); after the `DELETE` above it serves the code definition, in the same boot. + + +- 80f9f7e: fix(runtime, plugin-auth)!: two access guards refuse, instead of admitting, when their own read cannot answer + + Clause-②: no (narrowing) + + + + **BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention: a request that one of these two guards let through only because the guard's own read faulted is now refused. Nothing an author or caller writes changes shape. + + - **`@objectstack/runtime` — the dispatcher's environment-membership gate.** When its `sys_environment_member` read throws, or no ObjectQL engine resolves on the request's kernel, the request is refused with `503 SERVICE_UNAVAILABLE` — the `AuthzStoreUnavailableError` answer the identity step and the domain gates already give an authorization input they could not read. Before, the gate logged at debug level and let the request through. A member is still admitted, and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. An engine whose registry does not register `sys_environment_member` declares the gate inapplicable, and nothing is read. + - **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). When its `sys_organization` or `sys_environment` read throws, the organization update is refused with `503 SERVICE_UNAVAILABLE`. Before, the hook ended without refusing and the slug changed. A slug change while an active environment references the organization is still refused (`403 FORBIDDEN`), and any other change is still allowed. An engine that does not register `sys_environment` — the open-source composition, where it is a cloud-provided object — declares the guard inapplicable from its registry (`getSchema`): nothing is read and the update proceeds as before. Without a data engine the guard does not apply either. + + What changes for you: nothing in what you write. A `503 SERVICE_UNAVAILABLE` on these doors is a store outage that used to be hidden behind an admitted request; it clears when the store answers again. + +### Patch Changes + +- 50e1c65: fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal` + + Clause-②: no (narrowing) + + + + **BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.** + + **What changed.** + + - The audit plugin's CRUD mirror now omits every field declared `internal: true` from the + rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an + update, delete `old_value`, and the activity row. It already masked `secret` and `password` + fields; `internal` is the same contract the generic data path already enforces ("never + returned on the generic data path"). An update that changes only an `internal` field still + writes its row, with neither value. + - These platform fields are now declared `internal: true`, so neither the generic data path + nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential + columns of the one-time verification object (`sys_verification`), the two-factor secret and + backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh + token columns, the OAuth client secret digest, the SCIM credential digest, the share link's + token and password hash, and the approval action-token digest. API key digests and email + headers were already `internal`; the ledger now honours that too. + - Every built-in consumer that needs one of these values reads it back through the engine's + privileged accessor rather than the generic path: JWT signing, password reset and the other + one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret + migration, OAuth client authentication, share-link redemption (the password gate is held) + and the creator's share-link list, which keeps returning each link's token. The runtime's + share-link resolve route (the dispatcher twin of the plugin's) still answers "password + required" for a protected link rather than the unknown-link shape. + - The one-time verification object's record title is now the fixed label `Verification`; it no + longer shows the identifier column. + - `@objectstack/objectql` exports two helpers from its main and `/core` entries: + `collectInternalReadFields` (the names of an object's `internal` fields) and + `readInternalColumn` (recovers one `internal` column for rows already read, through the + engine's privileged accessor, and fails closed when the value cannot be recovered). + + **What to do after upgrading.** + + - **Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten + (the ledger is append-only), so a signing key that existed before the upgrade may have a copy + in the ledger. Rotate the keys so that copy signs nothing. + - **Revoke and re-mint share links that must stay private.** A share link's token is a + capability that stays valid until the link expires or is revoked, and links minted before this + release may have a copy in the ledger. + - A copy of a one-time verification credential is usable only while that credential is still + outstanding: once it is consumed or expires, its copy names nothing that will be accepted. + - An integration that read any of these columns through `GET /api/v1/data/...` no longer + receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO + providers through their auth routes. +- 1fd5664: fix: when the store refuses an uninstall's `sys_packages` delete, the uninstall now answers the failure and removes nothing else, instead of answering success and coming back after the next restart (#21276) + + Clause-②: no + + **`@objectstack/metadata-protocol`.** `deletePackage` now deletes the package's `sys_packages` row first, before its `sys_metadata` rows, its tables, its registry entry and the rows the uninstall cleanups own. When the `package` service refuses that delete, whether it returns `{ success: false }` or throws, `deletePackage` throws and nothing else is removed. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, the refusal was logged as a warning, and `DELETE /api/v1/packages/:id` answered `200` after the package's metadata, tables and grants had been removed. The package then came back after the next restart. + + Before that store delete, `deletePackage` now also asks the registry whether the uninstall would be refused because another package extends an object this package owns (ADR-0029). If so, it throws the registry's own refusal with nothing removed. A registry without the new question is not asked, and the refusal then surfaces at the registry withdrawal, as before. + + **`@objectstack/objectql`.** New: `SchemaRegistry.assertPackageUninstallable(packageId)`. It throws the refusal `unregisterObjectsByPackage` and `uninstallPackage` raise for an object another package extends, with the same message, and it changes nothing. `unregisterObjectsByPackage` now calls it, so there is still one copy of that check. + + **`@objectstack/runtime`.** `DELETE /api/v1/packages/:id` now asks `deletePackage` before it touches anything. It checks that the package exists with a read, and it withdraws the package from the running registry and clears its saved disable record only after `deletePackage` has answered. So when the store refuses, the door answers `500`, the same process keeps serving the package, and a package that was disabled stays disabled after a restart. Before this, the door withdrew the package and cleared its disable record first. A refused delete then left the package missing until a restart, and brought a disabled package back enabled. + + An uninstall refused because another package extends an object this package owns still answers `500` with nothing changed: the stored rows, the registry entry and the disable record all stay as they were, in the same process and after a restart. That refusal is now decided before the store delete, instead of by the door withdrawing the package first. An ordinary uninstall, and a host with no `package` service, are unchanged. +- abe8f28: fix(runtime): a sandboxed body or an action handler that reads the stored-metadata tables is served what the generic data door serves (#21454) + + Clause-②: yes + + The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and a content hash computed over it. The generic data door serves such a row with the body as its type's read projection, with the stored credential material withheld, and the hash in keyed form. Three in-process reader contexts served the same rows as stored: + + - a sandboxed action or hook body that reads through `ctx.api.object(...)`, inside `ctx.api.transaction(...)` too; + - an action handler that reads through `ctx.engine.find(...)`; + - an action handler that reads through `ctx.api.object(...)`. + + An action body and an action handler run elevated, so the stored form reached whoever could invoke the action, a member included. + + **What changes.** A read of either table through any of these contexts now answers the data door's form: the projected body, and the content hash under the same key the data door uses. That key is the crypto provider's, or the process-scoped ephemeral key when no provider is registered. `find`, `findOne` and `aggregate` are served this way, and so is every context the scoped API derives: `sudo()`, `withRunAs(...)`, a `transaction(...)` callback's context, and the context `beginTransaction()` returns. A hook body that copies what it read into another record can now copy only the projected form. A projection that names the body column without the type column reads the type beside it and drops it again, as on the data door. + + **What does not change.** Every other object, every write and `count` behave as before. The platform's own readers of these tables still read the stored form, because the projection is applied at the reader contexts and not in the engine. + + `@objectstack/metadata-protocol` now exports the data door's stored-row serve, so these contexts consume it and keep no copy: `storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows`, `ephemeralStoredHashDigest` and the `StoredHashDigest` type. The exports are additive. + + The four functions `storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows` and `ephemeralStoredHashDigest`, and the type `StoredHashDigest`, are new public API of `@objectstack/metadata-protocol`, and `@objectstack/runtime` consumes them. +- aa0d4b9: `os migrate resume`, `os migrate recorded-by` and `os migrate value-shapes` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 with "The database refused to run this query" (#21529) + + Clause-②: no + + Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing. On a never-booted database (or a `--database-url` that points at one), every default run failed: + + - `os migrate resume` exited 1, naming `sys_migration_journal`; + - `os migrate recorded-by` exited 1, naming `sys_metadata_history`; + - `os migrate value-shapes` reported every scanned object as unreadable, kept the gate closed and exited 1, over data that does not exist. + + Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so: + + - `os migrate resume` lists no interrupted runs (`{"interrupted": [], "count": 0}`), exit 0; + - `os migrate recorded-by` reports `pending: 0`, nothing to convert, exit 0; + - `os migrate value-shapes` completes a clean scan of zero records, exit 0, and names the objects it did not read because they have no table yet (on stderr under `--json`). + + Human mode says the table is not there yet, instead of implying the command looked through one. `--json` documents have the same shape as on a booted database with nothing to do. The write modes (`--run`, `--apply`) are unchanged: they boot with the schema sync, so their tables exist before they read. + + `MigrationRecoveryPlugin` (`@objectstack/runtime`), which every one of these boots composes, scans the migration journal at boot. On such a database it logged "Migration journal scan failed; interrupted migrations (if any) were NOT detected" on every run. It now treats a missing journal table as "no runs" and says nothing. It recognises that case only with the shared `isMissingTableError` predicate, asked about `sys_migration_journal` itself. Any other failure of the scan still warns. + + There is nothing to migrate. +- 5d0e4e2: fix(metadata-protocol)!: the generic data door refuses a stored-metadata filter that reads the body or a content hash through a cross-field comparand or below its depth backstop, and exports its one filter-field collector and one search narrowing for the reader-context seam (#21544) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the generic data door (`GET /api/v1/data/:object`, `POST /api/v1/data/:object/query` and the in-process `findData`) accepts when it reads `sys_metadata` or `sys_metadata_history`. Two filter shapes read the stored body column or a content-hash column (`checksum`, `previous_checksum`, or the history table's `change_note`) without the family's refusal ever seeing them, and both ran before this release: + + - a cross-field comparand naming one of those columns — `{ "name": { "$ne": { "$field": "metadata" } } }`, in `where` or in an aggregation's `filter`, under `$not` included. The SQL drivers evaluate it row by row, so row presence disclosed the column's value; + - a filter on one of those columns nested more than 32 combinators deep, which the door's field collector stopped reading at. A body `$contains` of a stored credential answered the row and a wrong guess answered none. + + Both now answer the door's `400 INVALID_FIELD`, naming the column, before the query runs — the answer the same filter already gets when it names the column directly. The route: filter those tables by their scalar columns (the type, the name, the state and the like), compare scalar columns with each other, and read the bodies with a plain list, which is served projected. Every other column of the two tables, and every other object, is unchanged; a dotted key into one of those columns was, and stays, refused by the door's dotted-path rule. It ships as `minor` under the launch-window convention for accept-set narrowings. + + - **`@objectstack/metadata-protocol`** exports two module functions the generic data door now calls itself: + - `collectStoredMetadataFilterFields(object, query)` — the family's one filter-field collector: every column a read query's filters read (`where`, the engine's `filter` alias and each aggregation filter): each key's head and each cross-field `{ $field }` comparand, at any depth. `[]` outside the family. + - `narrowStoredMetadataSearch(object, query, schema, wireSpelling?)` — the family's one default-search narrowing: an explicit search-field list naming the body or a hash column is refused, a default search is narrowed to the searchable set without them (returned for the caller to run as `searchFields`), and a set that narrows to nothing is refused. The `StoredMetadataSearchSchema` type it reads is exported beside it. + - **`@objectstack/runtime`**: the stored-metadata reader-context seam (`ctx.api.object(...)` for action and hook bodies, a handler's `ctx.api`, and `ctx.engine.find`) calls those two functions instead of its own copy of the narrowing and `@objectstack/plugin-security`'s condition walk, so the seam and the door answer every family filter and search identically. A `count` through the seam now runs the query the guard returns. The seam's accept set is unchanged: every shape it refused before it still refuses, now through the door's collector. +- 6946f2f: fix(runtime): when two packages declare a job with the same name, both jobs now run. Uninstalling one stops only its own job. + + Clause-②: no + + The metadata registry keys a packaged item by package and name (`:`), so two packages may each declare a job called, say, `nightly_sync`. The job service keys a job by one string and replaces any job with the same name. Before this fix, installing the second package (`os package install`, or a second app on one boot) silently replaced the first package's job. That job stopped running while both installs reported success. + + - **Both jobs run.** A job is scheduled under its authored name unless another package already holds that name on the job service. In that case it is scheduled under the registry's package-scoped identity, `:`, and an `info` line names the package that holds the name. A package's job body and its `handler`'s `jobId` still see the authored name. + - **What an operator sees.** The Background Jobs catalogue (`sys_job`) and run history (`sys_job_run`) list the job under the name it is scheduled under. That is the authored name, or `:` for a package whose job name another package already holds. A reinstall keeps the name the job already has. + - **Each package cancels only its own job.** When a reinstall drops a job, or the package is uninstalled (the `runtime.package-jobs` uninstall cleanup), the job is cancelled under the name it was scheduled under. Another package's job with the same name keeps running. + - **Unchanged:** a runtime in which no two packages declare the same job name schedules every job under its authored name, so its catalogue and run history read exactly as before. No schema, export, `IJobService` contract or accept-set change. +- 75ddcd1: `POST /api/v1/marketplace/install-local` now runs the ADR-0087 D1 protocol handshake. A manifest whose declared range excludes this runtime's protocol major is refused with `422 OS_PROTOCOL_INCOMPATIBLE`, the answer `POST /api/v1/packages` already gives. It used to install with a `200` (#21762). + + Clause-②: yes (widening) + + - **Install.** The handshake runs after the manifest id is parsed and before anything is registered, written or synced. The range is read from `engines.protocol`, then `engines.platform`, then `engine.objectstack`. The refusal answers `422` with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'`, the handshake's own `error.message`, and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. It is the same on the inline-manifest branch and the cloud-snapshot branch. No ledger file is written, and an installed earlier version stays as it was. A manifest with no range, or a range the handshake cannot read, still installs, and the handshake's warning goes to the plugin's logger. + - **Restart.** On `kernel:ready`, a ledger entry whose range excludes this runtime's major is not loaded. Nothing is registered, synced, bound or seeded for it. One `error` line names the package, `OS_PROTOCOL_INCOMPATIBLE` and the replay command (`objectstack migrate meta --from N`). The boot continues with the other entries. The entry stays in the ledger, so `DELETE /api/v1/marketplace/install-local/{id}` still removes it, and installing a compatible version replaces it. Before, it was registered and its schemas synced, with no warning. + - **`@objectstack/metadata-core`:** a new export, `protocolIncompatibleAnswer(err)`, with its return type `ProtocolIncompatibleAnswer`. It turns a `ProtocolIncompatibleError` into the status, code, message and five-member `details` an HTTP door answers. Both install doors call it, so their answers are the same bytes. + - **`@objectstack/runtime`:** `POST /api/v1/packages` answers through that helper. Its response is unchanged. + + A client that relied on install-local accepting a package built for another protocol major gets `422` now. Install a version built for this runtime's protocol, or migrate the package with the `migrateCommand` in the refusal. +- a0176ef: Credential-class field values are now masked on every write response, as on reads. + + Clause-②: no + + - A `secret` field, and a `password` field on an object that is not `managedBy: 'better-auth'`, already read back as `SECRET_MASK` (`null` when unset) on the generic read path (ADR-0100). Every write response that returns a record (REST, batch and MCP) now answers the same way. + - The shared write-response helper every write door already calls (`omitInternalFieldsFromWriteResponse`, `@objectstack/core`) now applies the credential mask before it omits `internal: true` fields. New exports beside it: `maskCredentialFieldsInWriteResponse` and `collectCredentialWriteResponseFields`, which read the same `isMaskedOnReadFieldType` declaration as the engine's read mask. + - `callData`'s fallback create and update arms (`@objectstack/runtime`, used when no protocol service is registered) now pass their response record through the same helper. + - Unchanged: the engine's own write results still return the stored row whole to privileged server-side callers, and the echoed-mask write guard still treats a `SECRET_MASK` value as "leave unchanged", so a client that saves back a write response does not overwrite the stored credential. +- 088428f: A declared `PATCH` AI route is reachable over HTTP: the dispatcher's `/ai/*` method wildcards mount `patch` beside `get`, `post`, `put` and `delete`, so `PATCH /api/v1/ai/conversations/:id` (the SDK's `ai.conversations.update`, the console's conversation rename) reaches its handler instead of answering `405` (#21806). + + Clause-②: no + + - **Where it failed.** On a host where the wildcards are the only door into `/ai/**`, a `PATCH` never reached the dispatcher. The server adapter answered `405 METHOD_NOT_ALLOWED` with `Allow: DELETE, GET, HEAD, POST, PUT`, because the path matched the wildcards under the four other verbs. Both bases the wildcards serve are fixed: `/api/v1` and `/api/v1/environments/:environmentId`. + - **An undeclared method still answers `405`.** The AI route table now tells its two misses apart. A method the table does not declare on a path it does declare answers `405 METHOD_NOT_ALLOWED`, with an `Allow` header that names exactly the declared methods, and no handler runs. A path the table declares under no method still answers `404 ROUTE_NOT_FOUND`. The rule is the same for every verb. + - **What a caller sees change.** A `GET`, `POST`, `PUT` or `DELETE` that names a declared AI path under the wrong method used to answer `404 ROUTE_NOT_FOUND`. It now answers `405` with `Allow`. A `PATCH` to an AI path the table does not declare at all used to answer the adapter's `405`. It now answers `404 ROUTE_NOT_FOUND`. No request that was refused before is served now, except a `PATCH` to a route the table declares. +- f5b8e29: Share-link passwords follow the platform's credential rules (#21839). + + - **The stored hash never leaves the server.** The share-link mint response (`POST /api/v1/share-links`, and `ShareLinkService.createLink`'s return value) no longer carries `password_hash`. The list and the redemption result are projected the same way. A client that reads a link's password state keeps reading it from the redemption route's `NEEDS_PASSWORD` answer, as before. + - **The stored form is the platform's slow password hash.** New passwords are hashed with scrypt at the parameters account passwords use, instead of one salted SHA-256. Links minted before this release keep working: a stored password in a legacy form still verifies, and it is re-hashed into the new form on its first successful redemption. Every comparison is constant-time. A deployment that injects its own `hashPassword` / `verifyPassword` pair is unaffected, and its stored forms are left alone. + - **The password travels in a header.** Both public share-link routes (`GET /api/v1/share-links/:token/resolve` and `/:token/messages`) accept the `x-share-password` request header, the preferred form, because a header is not part of the request URL. The `?password=` query parameter is still accepted for compatibility, so current consoles keep working until they move to the header. `/messages` accepted only the query parameter on this mount before. + - **Cross-origin clients can send the header.** `X-Share-Password` is in the default CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS` in `@objectstack/plugin-hono-server`, which the `@objectstack/hono` adapter also applies). A deployment that passes its own `allowHeaders` is unchanged; add the header to that list to let a cross-origin client use it. + - **Public share-link answers are not cached.** Both public routes answer with `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, on both mounts (the sharing plugin's routes and the runtime dispatcher's `/share-links` domain). The authenticated create, list and revoke routes are unchanged. + - **Hashing works in WebContainer.** On StackBlitz WebContainer, where `node:crypto.scrypt` is incomplete, the password is hashed with the pure-JS scrypt from `@noble/hashes` (now a dependency of `@objectstack/plugin-sharing`, as it already is of `@objectstack/plugin-auth`), at the same parameters and in the same stored form. A hash made on either runtime verifies on the other. +- e6dc7a2: The object-schema field mask (ADR-0106 D1) judges an action param that names another object's field through `objectOverride` against that object, not the one being served. + + Clause-②: yes (widening) + + **What a user saw.** A `delegated_admin` may invite members, and the invite door admits them, but `GET /meta/object/sys_user` served that principal no `invite_user` action. The action's `role` param is `{ field: 'role', objectOverride: 'sys_member' }`: it names `sys_member.role`. The mask read every param's `field` as a field of the served object, so a caller denied `sys_user.role` lost the whole action. A plain `member` lost it the same way. The member is now served the action too, and still not offered it: the action's `requiresMembershipReach` predicate excludes the member grade. + + **The rule.** A param whose `objectOverride` names another object reads that object's field. It is judged against the caller's readable fields on that object, and it is not a reference to the served object's fields. The action is still dropped when the caller cannot read the field there, and when that object's readable fields cannot be determined (no answer from the security service, a security service that throws, or an object that does not exist). Nothing about the other object is served on a guess. The rest of the param is still read against the served object: `visible`, an option's `visibleWhen`, `defaultValue`, and an explicit `name` that differs from `field`. A `name` that only repeats `field` is read as that field. With `defaultFromRow`, the param also reads `field` from the served object's row, so `field` is judged against the served object too. An exempt caller (platform admin, `isSystem`) is served the whole schema, as before. + + **The API (`@objectstack/metadata-core`), additive.** + + - `relateObjectSchemaMaskPosture(posture, ...documents)` completes a `project` posture for the documents it is about to mask. It reads the caller's readable fields on each other object their action params name through `objectOverride`. It runs after the fetch, because only the document names those objects. It returns every other posture, and any document with no such param, unchanged, and it never throws. + - The `project` member of `ObjectSchemaMaskPosture` gains two optional fields. `relate` asks the posture's question (same caller, same security service) about another object. `resolveObjectSchemaMaskPosture` sets it. `related` holds the answers. A `project` posture built without `related` gets no answers, so `applyObjectSchemaMask` drops every action with such a param. + - `applyObjectSchemaMask` folds each related read it withholds into the fingerprint, written as `object.field`. Two callers who are denied the same fields on the served object but differ on the other object get different validators. An unrestricted caller's ETag is unchanged. + - The shared contract fixture `FLS_CONTRACT_OBJECT` (`@objectstack/metadata-core/testing`) gains two actions whose params read `contact` fields through `objectOverride`. The contract's projection cases now require the readable one to be served and the denied one to be dropped. An exit that never relates its posture fails the contract by name. + + **Every exit relates its posture (`@objectstack/rest`, `@objectstack/runtime`).** These exits relate the posture after the fetch, before the projection: the shared item, layered and list chains, `RestServer`'s cached read and published read, and the runtime dispatcher's mask. The `/meta` diff route masks `fields` only and needs no relate step. + + **Measured on a showcase boot.** We read every object schema (78 objects, by-name read and list read) as five principals: a platform admin, an org owner, an admin, a `delegated_admin` and a `member`. Before and after this change, the only served action that moved is `sys_user.invite_user`, which is now served to the `delegated_admin` and the `member`. This repository has two authored params with `objectOverride`: `sys_user.invite_user`'s `role` and `sys_member.invite_user`'s `email` (on `sys_invitation`). The second was served to all five principals before and after. +- faf8dce: An import over a code-defined datasource is held to the namespace of the package that declares it (ADR-0028), and the draft door answers the prefixed name (#21889). + + Clause-②: no + + - **Before.** `POST /api/v1/datasources/:name/external/tables/:remote/import` with an explicit `name` that carried no namespace prefix answered `201` and saved an unprefixed federated object, and `POST …/external/tables/:remote/draft` answered the bare remote table name with a `TODO(namespace)` note. Measured on the showcase's `showcase_external` under `objectstack dev` and `objectstack start`. + - **`@objectstack/runtime`.** `AppPlugin` registers each code-defined datasource through `applyProtection` with the id and version of the package body that declares it, so the item carries `_packageId`, `_packageVersion` and `_provenance: 'package'`. On an ADR-0130 `packages[]` artifact each datasource takes its own body's id, never the artifact's top-level manifest id. A top-level datasource that no body declares keeps its registration under the artifact's own id, and a warning names it. + - **`@objectstack/service-datasource`.** The federation service reads the datasource's package record from the engine registry (`registry.getPackage` on the `objectql` service), the store the runtime publish gate reads for the same check. It used to ask the `metadata` service, which holds no package records in any composition, so no datasource resolved a namespace. The package id still comes only from the stamped `_packageId`. + - **What a caller sees now.** On a datasource whose package declares `manifest.namespace`, an unprefixed import `name` answers `400 EXTERNAL_IMPORT_ERROR` with ADR-0028's message, which names the prefixed name to use. An import with no `name` override saves the prefixed name the draft derives (for example `showcase_customers` instead of `customers`). `GET /api/v1/meta/datasource` lists the three provenance keys on a code-defined datasource; all three are declared on `DatasourceSchema`. The datasource admin list (`GET /api/v1/datasources`) is unchanged, and the admin door still refuses to edit or remove a code-defined datasource. + - **Unchanged.** A datasource that carries no `_packageId` (the host `default` is one) and a package that declares no namespace resolve no namespace, so their imports and drafts answer as before. +- 131b937: Four producers that reached the data engine with no principal and no `isSystem` now carry the explicit system opt-in. Each is already authorized by its own door, so nothing it answers changes. + + Clause-②: no + + - **`@objectstack/plugin-auth` — the platform-admin OAuth client toggle route** (`POST /api/v1/auth/admin/oauth2/toggle-disabled`). Its `sys_oauth_application` read and write go through `withSystemContext`, the wrapper better-auth's adapter already writes those rows through. The platform-admin judge still runs first. The answers (`200`, `404 RESOURCE_NOT_FOUND`, the refusals) and the stored row are unchanged. One log line goes away: the engine's read-only `updated_at` warning on every toggle. The value it warned about was discarded before and the driver still stamps the column. + - **`@objectstack/plugin-auth` — `verifyScimBearerToken`.** The credential probe passes `isSystem: true` in the read's trailing options. It runs before any caller is known, and the digest equality is still all it matches. An unknown, inactive or expired bearer is still `null` (`401`). + - **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). Its `sys_organization` and `sys_environment` reads go through `withSystemContext`. The organization id stays in the `where`. A slug change while an active environment references the organization is still refused (`FORBIDDEN`), and any other change is still allowed. The catches around both reads are unchanged: a read that throws still ends the hook without refusing. + - **`@objectstack/runtime` — the dispatcher's environment-membership gate.** The `sys_environment_member` read carries `isSystem: true` as its query context. The caller's user id stays in the `where`. A member still passes and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. The catch around the read is unchanged: a read that throws still lets the request through. + + Why: the security middleware hands a context with no principal and no `isSystem` straight through (ADR-0096). That hand-through is not an authorization. A caller that is the platform acting for itself says so explicitly. ⛔ No new elevation API, no door's authorization moves, and no accept set changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [13a24ec] +- Updated dependencies [fd5a1cd] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [f9bcd08] +- Updated dependencies [e3ad492] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [7aab759] +- Updated dependencies [0e10be6] +- Updated dependencies [1c52a5e] +- Updated dependencies [97239c3] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [ee75aae] +- Updated dependencies [6e33b67] +- Updated dependencies [ab52182] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [49524f6] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [535d1d2] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [8b123c0] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [520f66f] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [35dfb81] +- Updated dependencies [e9dec3d] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [ce53218] +- Updated dependencies [44defd4] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [a7ab047] +- Updated dependencies [440cd32] +- Updated dependencies [f9a8eb8] +- Updated dependencies [6c5697d] +- Updated dependencies [74281a8] +- Updated dependencies [9a4182a] +- Updated dependencies [550f4cc] +- Updated dependencies [41b1333] +- Updated dependencies [5dbcee8] +- Updated dependencies [ec390ec] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [5d0e4e2] +- Updated dependencies [e367002] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [6ec54f0] +- Updated dependencies [5d095a0] +- Updated dependencies [a1ca156] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [7b07749] +- Updated dependencies [eea82af] +- Updated dependencies [be55fd2] +- Updated dependencies [a2aadab] +- Updated dependencies [ced217c] +- Updated dependencies [8843505] +- Updated dependencies [ff16740] +- Updated dependencies [234d1d8] +- Updated dependencies [fe10172] +- Updated dependencies [5259a35] +- Updated dependencies [7fd2c34] +- Updated dependencies [c43a8ae] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [cf60dbc] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [c7a60e1] +- Updated dependencies [e83c9f6] +- Updated dependencies [3eb38ae] +- Updated dependencies [1c3a4d9] +- Updated dependencies [da40a5f] +- Updated dependencies [b7a13c7] +- Updated dependencies [045f764] +- Updated dependencies [18c2ddc] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [07e933b] +- Updated dependencies [c9be1f1] +- Updated dependencies [e1790fd] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [833d57c] +- Updated dependencies [607463d] +- Updated dependencies [18fe681] +- Updated dependencies [3237b4a] +- Updated dependencies [2e78046] +- Updated dependencies [0fe0a59] +- Updated dependencies [cab6396] +- Updated dependencies [87712ab] +- Updated dependencies [e864db5] +- Updated dependencies [25eb7de] +- Updated dependencies [41a1135] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [5e0b489] +- Updated dependencies [07c842d] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [bc7747c] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [9cc2c79] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [76fec88] +- Updated dependencies [13a22d0] +- Updated dependencies [753e7a1] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/driver-memory@17.7.0 + - @objectstack/driver-sql@17.7.0 + - @objectstack/driver-turso@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/plugin-auth@17.7.0 + - @objectstack/rest@17.7.0 + - @objectstack/driver-sqlite-wasm@17.7.0 + - @objectstack/observability@17.7.0 + - @objectstack/service-cluster@17.7.0 + - @objectstack/service-i18n@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index bf00ac6182d..c8df80222aa 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.6.0", + "version": "17.7.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 3c06dd4dc83..9a5a5eef7cf 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,52 @@ # @objectstack/sdui-parser +## 17.7.0 + +### Minor Changes + +- 99e1912: The metric sub-caption is retired at both ends. A dashboard widget keeps one authored description, `widget.description`, which renders as the card-header subtitle and is translated by the widget's `description` translation key. The widget translation key `subCaption` is refused, and the server no longer writes a widget's `options.description`. + + Clause-②: no (narrowing) + + + + **What is retired.** `dashboards.DASHBOARD.widgets.WIDGET.subCaption` in a translation bundle (`defineTranslationBundle`, `stack.translations`, the platform bundle) and in a registered `translation` item. It overlaid a caption under a metric's value onto the widget's `options.description`. The dashboard schema never declared `options.description`, and no authored widget wrote it, so `translateDashboard`'s overlay was the key's only writer. That overlay is removed: `translateDashboard` now translates a widget's `title` and `description` and carries `options` through untouched. + + **BREAKING** — an accept-set narrowing, shipped as `minor` under the launch-window convention. + + ### FROM → TO + + | wrote | write instead | + | --- | --- | + | `dashboards.DASHBOARD.widgets.WIDGET.subCaption: 'TEXT'` | delete the entry. If the copy belongs on the card, put it in the widget's `description` and translate it under `dashboards.DASHBOARD.widgets.WIDGET.description`. | + | `dashboards.DASHBOARD.widgets.WIDGET.subtitle: 'TEXT'` | `subtitle` was only ever a rename suggestion for `subCaption`. Card-header copy goes under `description`; a caption under the value has nowhere to render, so delete it. | + + **The one-line fix: delete every `subCaption:` entry under `dashboards.*.widgets.*` in your translation bundles.** `os migrate meta --from 17` lists the mechanical edits for existing sources; stored `translation` items are converted when they are read. + + **What an author now sees.** Writing `subCaption` fails `tsc` (its input type is the retired-key mark) and fails the parse with a prescription naming the widget's `description`. Writing `subtitle` on a widget translation fails the parse with both readings named, instead of a rename suggestion onto a key that is refused next. `os validate`, `os build` and `os lint` now raise the `unconsumed-widget-option` warning on an authored widget `options.description`, like any other options key the dataset-bound render path does not read. It is a warning, so none of the three fails on it. + + **Measured producers: none.** Zero `subCaption` entries and zero authored widget `options.description` in the four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and in the bundles `@objectstack/platform-objects` ships, so no shipped exit code changes. + + ### The retirement kit + + - **Tombstone.** `subCaption` is a `retiredKey()` tombstone on the widget translation node, so the refusal carries the prescription on all three faces the node is spread into (per-app bundle entry, platform bundle entry, `translation` item). The node sits under two records (`dashboards`, `widgets`), below the authorable-surface walk, so it has no `RETIRED_KEYS_BY_MAJOR` row, the same as the `submitLabel` component-copy key before it. + - **The former alias.** The `subtitle` → `subCaption` rename suggestion moves to the node's `guidance` table. An alias whose target is a tombstone is the shape the alias-integrity audit refuses, and repointing it at `description` would silently change what the word is taken to mean. + - **Conversion.** `translation-widget-sub-caption-removed` (protocol 18) strips the key from bundle entries and bare translation items as a lossless delete. It is retired from the load path, so authors are refused at parse while stored rows and `os migrate meta` replay it. Its D3 record is the semantic entry `translation-widget-sub-caption-retired`. + - **`@objectstack/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `description`, its one undeclared member, which existed only because the overlay wrote it. `check:widget-option-census`'s `NON_DECLARED_MEMBERS` ledger is now empty, so the census asserts that nothing writes an undeclared key into `options`. +- a4fd82a: A `kind: 'html'` page that hands a component input a literal of the wrong type now fails to compile, instead of compiling with a warning. + + Clause-②: no (narrowing) + + + + **BREAKING**: an accept-set narrowing on the html page compiler, shipped as `minor` under the launch-window convention for accept-set narrowings. No export, type or diagnostic code changes. + + **What changed.** `compile()` used to grade a `type-mismatch` as an `error` only when the input declared an `enum` arm, and as a `warning` otherwise. Every value the type check sees is a literal written in the source: a quoted attribute is a string, a bare attribute is `true`, and a braced value is the exact literal written. (A braced value that is not a literal is reported separately as `inert-expression`, which stays a warning.) A literal's type is known when the page compiles, so a mismatch is certain, and it is now an `error` for every declared type. `member-type-mismatch`, the same check applied to the members of an array or map, follows the same rule. Codes and messages are unchanged. + + **Why.** `aggregate="count"` on an `object-metric` passed `os build` with one warning. The tile reads `aggregate.function` and `aggregate.field`, received a string, and drew no number. + + **What an author now sees.** `os validate`, `os build` and `os lint` fail on the page, and the save door refuses it when the host has a component manifest, with the existing message, for example ` prop "aggregate" expected an object`. To fix the page, write the value in the type the input declares: braces with JSON for an object (`aggregate={{"function":"count"}}`), braces for a number or a boolean (`limit={50}`, `invert={true}`), and braces with an array for an array (`fields={["name","amount"]}`). A string-typed input still takes a quoted value. + ## 17.6.0 ### Minor Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index bdd4e03f9bd..a5cd02b541d 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "main": "dist/index.js", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index c656b532cd7..eefb4a159d3 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,448 @@ # Changelog — @objectstack/service-analytics +## 17.7.0 + +### Minor Changes + +- 99589f9: fix(service-analytics)!: both analytics strategies refuse a cube measure whose `type` names no aggregate, in the spec's words — the custom-SQL `EXPRESSION_METRIC_TYPES` partition is gone with the three types it named (#21000) + + **BREAKING** — `@objectstack/spec` retired the cube metric types `number`, `string` + and `boolean` from `AggregationMetricType` (a measure's `sql` is a column reference, + so they had nothing left to compute). Every door that parses a cube refuses them; + this release removes the runtime branches that still served them for a cube that + reached the analytics service WITHOUT meeting that parse — one a host registers + in-process from a literal, through `AnalyticsServicePlugin({ cubes })` or + `AnalyticsService({ cubes })` (the registry never parses). + + | | before | now | + | --- | --- | --- | + | `NativeSQLStrategy`, a measure typed `number` / `string` / `boolean` | served: the column emitted UNAGGREGATED in the statement (`amount AS "m"` beside `GROUP BY`) | refused, nothing executed | + | `ObjectQLStrategy`, the same measure | refused `INVALID_FIELD` / 400 | refused, nothing executed | + | either strategy, a type the spec never declared (`median`) | native: refused; ObjectQL: forwarded to `executeAggregate` as the method (the auto-bridge refused it; a host's own executor received it), and `/analytics/sql` echoed `MEDIAN(amount)` | refused, nothing executed | + + **The one refusal** is `aggregateOfMeasure`'s, shared by both strategies and both + doors (`POST /analytics/query` and `POST /analytics/sql`): it names the measure and + the cube, then quotes the spec's own verdict on the type — for a retired type the + retirement prescription (the six aggregates to choose from, and where a per-row or + derived value goes instead), for anything else zod's message listing the six. It is + a bare `Error`, the undeclared-500 tier this package assigns to a cube that never + met the parse, so the HTTP answer is `500` with the message readable in the body + (measured through the dispatcher's analytics route), never a caller-blaming `400`. + The ObjectQL envelope for the three retired types therefore moves from + `INVALID_FIELD` / 400 to that tier. + + **The fix:** give the measure one of the six aggregate types — `count`, `sum`, + `avg`, `min`, `max`, `count_distinct` — or parse the cube through `CubeSchema` + before registering it, which refuses the same types with the same prescription. + + **Removed export:** `EXPRESSION_METRIC_TYPES` from + `strategies/native-sql-strategy.ts` (internal to the package; not re-exported from + its entry point). **Unchanged:** every aggregate measure on both strategies, the + auto-bridge's own parse of an engine method (still pinned, driven directly), and + `GET /analytics/meta`, which keeps publishing each registered measure's `type` as + registered. + + Clause-②: no (narrowing) + + +- 713b0fa: fix(metadata-protocol)!: a metadata body's stored content hash is served and compared only in keyed form, never copied, and never evaluated (#21207) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what the metadata doors serve and accept for the stored content hash of a metadata body — a hash over the whole stored body, withheld credential material included. Served beside the projected body it let a reader confirm a guess at that material offline; filtered on, it confirmed one online. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **Three things change for callers and operators.** + + 1. **A held version token gets one `409 METADATA_CONFLICT`.** Every door that hands out a metadata version token — the save, publish, package-publish and rollback receipts and the history read — now hands out a keyed digest of the stored hash instead of the hash itself, and the save and reset doors compare a token they are sent in that same form. The key is the crypto provider's; a host that registers none keys under a process-scoped ephemeral key instead, so a token is always issued and never empty. A token a client held from before the upgrade is refused once; take the token from the next read or receipt and retry. On a host with no provider the same happens after a restart, and on any host when a provider is first registered. An empty, withheld, raw or stale token is refused with the same `409`; it is never read as "no pin". + 2. **Filter, sort and group on the two stored content-hash columns, and on the version history's change note, now answer `400 INVALID_FIELD`** — on the generic data door, the MCP stdio reader and the analytics door, before the engine runs. The change note is included because a draft promotion that stated no message of its own recorded the draft's stored hash in it; the publish door now always states a hash-free message, and a note written before this release is served with the quoted hash in keyed form. A data-door search over the two stored-metadata tables no longer scans those columns or the stored body column, and an explicit search-field list naming one answers the same `400`. Every other column of the two tables is served, filtered, sorted and grouped as before, and every other object is unchanged. + 3. **Operators run `os migrate audit-metadata-bodies` once after upgrading, dry run first.** The audit ledger, the activity feed and the metadata decision-audit trail no longer copy the stored hash. The extended command drops it from the copies already written and withholds it in the decision-audit notes and their copies: a dry run by default, `--apply` to rewrite, idempotent. The version history stays the lineage. + + **What else changes.** The data door serves the two hash columns of the stored-metadata tables in keyed form, under the same key as the version tokens. The MCP stdio reader serves them keyed under the crypto provider's key, and omits them on a host with no provider. A `409` conflict refusal carries keyed values or none. The ObjectQL engine gains a read accessor for the registered provider's keyed digest; it is additive. A member's read of these tables is refused as before. +- 1caa603: fix(service-analytics)!: an analytics `order` key that names no member the query selects is refused with `INVALID_FIELD` / 400 at the analytics door, on both strategies, before either runs + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes. + + **The rule.** Each `order` key must be a column the answer carries: one of the query's own `dimensions` entries, one of its `measures` entries, or a `timeDimensions` entry that carries a `granularity`, spelled exactly as it is selected (a `.`-qualified measure keeps its qualifier in the answer, so the bare spelling names no column beside it, and the other way round). A `timeDimensions` entry with only a `dateRange` bounds the rows and is not a column. Any other key is refused with `400 INVALID_FIELD`, naming every such key and the members the query does select, and nothing is executed. The thrown error carries `param: 'order'` and `field` (the first such key). + + **Before**, measured through `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14, for a cube that declares no join over an object whose lookup target also declares `note`: + + - `dimensions: ['owner.email']` with `order: { note: 'asc' }`: native-SQL strategy `500` on both drivers (PostgreSQL 42702, `note` is ambiguous); ObjectQL strategy `200`. + - `dimensions: ['note']` with `order: { amount: 'asc' }`: native-SQL strategy `200` on SQLite, ordered by an arbitrary row's `amount`, and `500` on PostgreSQL (42803, must appear in GROUP BY); ObjectQL strategy `200`. + - `dimensions: ['note']` with `order: { 'owner.email': 'asc' }`: native-SQL strategy `500` on both drivers (PostgreSQL 42703, no such column); ObjectQL strategy `200`. + + **Now** each of those answers `400 INVALID_FIELD` on both strategies and both drivers, and `POST /api/v1/analytics/sql` refuses them the same way instead of returning a statement whose `ORDER BY` cannot run. + + **What to write instead.** Add the key to the query's `dimensions` (or `measures`), so the answer carries it, or drop it from `order`. + + **Who is affected.** A caller that posted an `order` key it did not select. On the native-SQL strategy those queries were already a 500 everywhere but the one SQLite shape, whose order was arbitrary. No example app, shipped dashboard, report, dataset or cube authors such a key, and the console's analytics adapter sends no `order` to this route. + + **Unchanged.** Ordering by a selected dimension, a selected measure or a bucketed time dimension; the dataset door (`POST /api/v1/analytics/dataset/query`), which already refused an unselected `selection.order` key with `400 DATASET_INVALID` and pushes an `order` down only when the selection selects every key; and a key naming a field the caller may not read, which keeps the `403 PERMISSION_DENIED` the field-level read gate answers for every position. +- 8b123c0: Row-level security policies and the analytics native-SQL path judge a comparand against a declared boolean field by the platform's boolean-comparand rule, the one the data engine's `where` already applies + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what two compilers outside the engine's `where` door accept. The RLS compile seam now drops a row-level policy, and the analytics native-SQL face now refuses a query, when either compares a declared boolean field with a comparand outside the accepted set. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. + + - **Row-level security (`@objectstack/plugin-security`).** A compiled `using` / `check` predicate on a `boolean` or `toggle` column (or a `formula` returning `boolean`) is judged by `booleanComparandDoorVerdict` from `@objectstack/spec/data`, in the same pass as the number rule. `'true'` / `'false'`, `'1'` / `'0'` and `1` / `0` are read as the boolean each names. Anything else the rule refuses (a string such as `'yes'`, `'TRUE'` or `''`, a number other than `1` / `0`) drops the policy as a refused comparand: the read is filtered by the deny sentinel, the write is refused 403, and the WARN line names the clause, the field and the position. Before, `record.flag != 'true'` kept every row on SQLite and the write check admitted every row, so the exclusion the author wrote was not applied. + - **Analytics native SQL (`@objectstack/service-analytics`).** The query's `where` (and the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter` are judged by the same rule before the statement compiles. An accepted spelling is read as its boolean, and anything else the rule refuses is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. The native strategy now answers what the engine-aggregate strategy answers. Before, `{ flag: 'true' }` counted no rows on SQLite, `{ flag: { $ne: 'true' } }` counted every row, and `{ flag: 'yes' }` answered 200. + - **What you may notice.** A policy or analytics filter that compared a boolean field with a value outside the accepted set now refuses instead of answering. Write `true` / `false`. A policy `record.flag == 1` now admits writing a `true` row, which its read already showed. + - **Unchanged.** A boolean literal, a column that is not boolean, a `{ $field }` reference, and an object whose declaration cannot be read (nothing is judged without one). +- 81e69ca: fix(service-analytics)!: the analytics read scope, the `where` tree and the draft preview take the shared lowering's bound and NULL guards; their own whole-day and NULL-polarity copies are deleted (ADR-0053 D-D1 items 7 to 9) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows the rows the native analytics strategy and the draft preview (`queryDataset` with `previewDrafts`) select for a bare-day upper bound on a column the host declares as neither `datetime` nor `date` — a `text` column, for example. It ships as `minor` under the launch-window convention for answer narrowings. No export, published type, accepted input or error code changes. + + **What is deleted.** The native SQL strategy no longer reads a bare `YYYY-MM-DD` `$lte`, a `$between` maximum or an explicit `dateRange` end as "through that whole day" on every column, and no longer drops such a bound on `9999-12-31` whatever the column holds. The whole-day rule is applied once, by the shared `lowerFilterCondition` (`@objectstack/spec/data`), with the column's declared type, the reader the plugin already wires from the engine's registry (`sourceFieldMeta`): a declared `datetime` column keeps the whole day, and every other declared column is compared as written, as the engine compares it. The `/analytics/sql` echo renders the same lowering. + + **The native face now agrees with the engine.** Measured through `AnalyticsService.query` (what `POST /api/v1/analytics/query` relays) in the plugin's own composition, on SQLite and on PostgreSQL 16, over a `text` column `note` holding `'2026-07-27'`, `'2026-07-28'`, `'2026-07-28 late'`, `'n'` and no value: + + - `{ note: { $lte: '9999-12-31' } }` counted every row with a value (4). It now counts 3, the rows the engine's `find` returns: `'n'` sorts above `'9999-12-31'`. + - `{ note: { $lte: '2026-07-28' } }` counted 3, the `'2026-07-28 late'` row included. It now counts 2. + - `$between ['2026-07-28', '2026-07-28']` and a `dateRange` window of the same day counted 2; they now count 1. Their negation through `$not` gains the row the bound lost. + + On a declared `datetime` or `date` column every answer is unchanged, on both strategies. + + **A host with no typed reader** (a strategy context with no `declaredFieldType` hook, or an `AnalyticsService` built without `sourceFieldMeta`) reads every column type-blind, as ADR-0053 D-D1 item 7 prescribes for a seam that cannot read declarations: its native answers do not move. Pass `sourceFieldMeta` (the README shows how) to get the engine's answer on a non-temporal column. + + **The `/analytics/sql` echo.** A `dateRange` window on a declared `date` column now prints the inclusive `<=` the engine runs, where it printed `<` the next day; on a column the host names no type for, it prints the bound the ObjectQL strategy hands the engine, as written. A preset window that stops before its end (`today`, `this_month`, …) now prints `<` its end instant with that instant bound, where it printed `<=` with no value bound. The NULL guards print once where they printed two or three nested copies of the same guard; every row set is unchanged. + + **The draft preview now agrees with the engine too.** `queryDataset` with `previewDrafts` evaluates drafted seed rows in memory; it kept its own whole-day copy, read on every column. It now hands the evaluator the drafted object's declared types (`sourceFieldMeta`), and the shared lowering applies the rule with them: a declared `datetime` column keeps the whole day, any other declared column is compared as written, and a column the host names no type for is read type-blind (ADR-0053 D-D1 item 7). Measured through the plugin's own composition over the same rows, five of the preview's `note` cells moved, each onto the engine's answer: `$lte` a day 3 to 2, `$between` and a window of one day 2 to 1, a window to `9999-12-31` 3 to 2, and the `$not` gains the row. Its `$lte` and `$between` to `9999-12-31` already gave the engine's answer and are unchanged. Every `datetime` and `date` cell is unchanged. + + - A preview window is now the `{ $gte, $lte }` pair the ObjectQL strategy hands the engine, matched like the same bounds in a `where`. Its end used to be read with a `'~'` suffix ("that instant and its own sub-values"), a reading no other face gives. Measured on a `datetime` column over SQLite, a canonical end (`…T10:00:00.000Z`) answers as before and as the engine. An end spelled shorter than the stored value is compared as text, as the preview's `where` already compared it: an end of `…T10:00` or `…T10:00:00` now leaves out the row stored at exactly that instant (the engine keeps it), and leaves out the rows inside that minute or second (the engine leaves them out too; the old reading kept them). Write a window end in full (`2026-07-28T10:00:00.000Z`) to get the engine's rows on the preview. + - A window over rows that hold a `Date` (the BSON storage form a MongoDB-backed draft reads back) is compared as instants, like the preview's `where`; it was compared as the `Date`'s display text. + - A host that wires no `sourceFieldMeta` (or an object the registry does not hold yet) reads every column type-blind. On a `text` column holding a value that sorts above `'9999-12-31'` (`'n'`), a `$lte` or `$between` maximum of `9999-12-31` now keeps that row, as every other type-blind seam does; the deleted copy left it out. + + **Unchanged.** Every answer on a declared `datetime` or `date` column, on the native strategy, the ObjectQL strategy and the draft preview; every answer of the ObjectQL strategy; every answer of the read scope. +- 086ad0a: The analytics native-SQL path judges a comparand against a declared number field by the platform's number-comparand rule, the one the data engine's `where` already applies + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the analytics native-SQL face accepts. A query or dataset that compares a declared number field with a comparand the number-comparand rule refuses used to answer 200 with a count on the native face (a 500 on PostgreSQL for a non-numeric string). It now refuses `INVALID_FILTER` / 400 before any statement runs, which is what the engine-aggregate face already answered. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. + + - **What changed.** A comparand against a `number`, `currency`, `percent`, `rating`, `slider`, `progress` or `summary` column is judged by `numberComparandDoorVerdict` from `@objectstack/spec/data` before the native statement compiles. This covers the query's `where` (including the dataset query's `runtimeFilter`, which is merged into it), each measure's own `filter` and a dataset's own `filter`. The rule runs in the same pass as the boolean rule. + - A numeric string (`'12'`, `'1e3'`) is bound as the number it names, which is what the engine binds. + - Anything else the rule refuses (a string with no numeric reading such as `'abc'`, `''` or `'+5'`, a boolean, or a list where one number belongs) is refused `INVALID_FILTER` / 400 with the rule's own message, before any statement runs. + - A relationship-path member is judged at the related object's declared column. + - **Before.** The native strategy bound the comparand as written. So `{ amount: 'abc' }` counted no rows on SQLite and answered a 500 on PostgreSQL, `{ amount: true }` bound `1` and answered 200, and `{ amount: { $lte: '9999-12-31' } }` counted every row. The engine-aggregate strategy refused all three with 400. + - **What you may notice.** An analytics query or dataset that compared a number field with a value outside the rule's accepted set now refuses instead of answering. Write a number, or a string of exactly that number's JSON spelling (`'12'`). + - **Unchanged.** A number, `null` (the null test), a `{ $field }` reference, a column that is not a number or a boolean, and a host that relays no declared field types (nothing is judged without one). +- 0b82391: fix(service-analytics)!: a caller-named analytics measure whose inferred source names no field (`_sum`, `*`, `*_sum`, an empty spelling) is refused with `INVALID_FIELD` / 400 at the analytics door, naming the spelling sent, on both strategies, before any statement is built (#21437) + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what `POST /api/v1/analytics/query` and its dry run `POST /api/v1/analytics/sql` accept in `measures`, on both strategies and every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, published type or error code changes. + + **The rule.** A `measures` entry the cube does not declare is inferred: the bare `count` counts rows (`COUNT(*)`), and any other spelling aggregates one of the object's own fields, named before an aggregation suffix (`_sum`, `_avg`, `_average`, `_min`, `_max`, `_count_distinct`) or, with no suffix, by the whole spelling. The bare `count` is now the only spelling that reads the row wildcard `'*'`. A spelling whose source is empty or is `'*'` names no field, and it is refused with `400 INVALID_FIELD` before anything is executed. The error names the spelling as it was sent (`member`, with `param: 'measures'` and `cube`); a `.` qualifier is kept in the name. + + **Before**, measured through `POST /api/v1/analytics/query` on SQLite, on the native-SQL and the ObjectQL strategy, on an ad-hoc cube and on an authored cube that does not declare the member: + + - `_sum`, `_avg`, `_average`, `_min`, `_max`, their `.`-qualified forms, `*`, `*_sum`, `*_avg` and the empty spelling `''` answered `500 DATABASE_ERROR`, after a statement reached the database (`SUM(*)`, `AVG(*)`, `SUM()`). + - `_count_distinct` and `*_count_distinct` answered `500 DATABASE_ERROR` on the native-SQL strategy (`COUNT(DISTINCT *)`). On the ObjectQL strategy the engine answered `400 INVALID_QUERY` after the aggregate was called. + - The qualifier alone (`.`) answered `403 PERMISSION_DENIED` from the member-shape gate. It now answers the same `400 INVALID_FIELD`, because it names no field either. + + **Now** each of those answers `400 INVALID_FIELD`, and no statement and no engine aggregate runs. `POST /api/v1/analytics/sql` refuses the same spellings instead of returning a statement that cannot run. + + **What to write instead.** Ask for `count` to count rows, or put the field's name before the suffix: the sum of `amount` is `amount_sum`. + + **Who is affected.** A caller that sent a measure spelling with nothing before the suffix, or the row wildcard itself. Every such request was already a 500. No example app, shipped dashboard, report, dataset, cube, doc or skill in this repository sends one. The console's analytics adapter composes a measure as the value field, an underscore and the aggregate function, so a widget whose value field is empty posts `_sum`. At the pinned `.objectui-sha` that adapter reads a 500 as an unknown failure and answers with its own client-side aggregation; it reads the 400 as a rejected request and surfaces it as an error. + + **Unchanged.** The bare `count`; a field-prefixed spelling such as `amount_sum`; the no-suffix spelling of a field (`amount`); a measure a cube declares, including one declared under a key such as `_sum`, which is the cube's own vocabulary and is never inferred; and the authored-position twin of this rule, the `@objectstack/spec` parse refusal of `'*'` outside a `count` on a cube or dataset measure (#21409). +- 35dfb81: fix(service-analytics): the ObjectQL face echoes a date-bucketed dimension in the bucket expression the driver itself groups by, so SQLite runs the statement it prints + + Clause-②: yes (widening) + + **Before**, the ObjectQL strategy printed every date-bucketed dimension as `date_trunc('', col)` in the `sql` it echoes and in the `POST /analytics/sql` body, on every dialect. The native strategy declines a granularity, so every bucketed query lands on this face. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` in the default composition: the rows were right. On SQLite the echo failed with `no such function: date_trunc` (month, quarter and week). On PostgreSQL 16.14 it ran but answered `2026-01-01T00:00:00.000Z` where the face answers `2026-01`. The driver groups by `strftime('%Y-%m', …)` on SQLite and `to_char((…)::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM')` on PostgreSQL. + + **Now** the echo prints the driver's own expression, so it runs on that dialect and answers the face's bucket keys. + + - **`@objectstack/driver-sql`**: `SqlDriver.dateBucketSql(objectName, field, granularity)` returns the expression `aggregate` groups by, rendered as SQL text: the existing `buildDateBucketExpr`, unchanged, with each identifier quoted by the dialect. It returns `null` for a granularity the dialect buckets in memory (`week` on SQLite). The MySQL arm (`date_format(convert_tz(…))`) is checked by code read only, because no MySQL server was available. + - **`@objectstack/service-analytics`**: the new optional `AnalyticsServiceConfig.dateBucketSql` hook carries the expression to the ObjectQL strategy. `AnalyticsServicePlugin` wires it from the driver that serves the object, as it wires `sqlDialect`. + - **`@objectstack/driver-turso`**: a comment that said `SqlDriver` buckets with `date_trunc` now names the SQLite `strftime` expression it emits. The inherited `dateBucketSql` answers on the remote face too: it renders the same SQLite expression with no connection, and libSQL runs it. + + **Unchanged.** The rows every face answers. The echo keeps `date_trunc(…)` where nothing answers: a host that wires no hook, a driver with no bucket expression (memory, MongoDB), a granularity the driver buckets in memory, and a query with a non-UTC `timezone`, which the engine buckets in memory on that zone's calendar. +- 1ca1eb0: fix(service-analytics)!: the analytics read scope and the draft preview compare a temporal comparand in the column's storage form, as the engine does (ADR-0053 D-A1 / D-A2) (#21505) + + Clause-②: yes (narrowing) + + + + **BREAKING**: this changes the rows two analytics faces select for a value comparison on a declared temporal column, in both directions, onto the rows `engine.find` selects for the same filter: on some filters fewer rows than before, on others more. The faces are the row-level read scope compiled into the native statement, and the draft preview (`queryDataset` with `previewDrafts`). It ships as `minor` under the launch-window convention for answer changes. No export is removed, no accepted input is refused and no error code changes. + + **The read scope.** `compileScopedFilterToSql` takes two new optional members in its options, `coerceTemporalFilterValue(field, value)` and `coerceTemporalFilterColumn(field, columnSql)`. Together they are the driver's `temporalFilterValue` / `temporalFilterColumnSql` pair, bound to the object the scope reads. After the shared lowering, every value comparison binds its comparand through the first and reads its column through the second: equality, `$ne`, the four orderings, `$in`, `$nin` and `$between`. Null tests, `$empty` and the text operators read the column as stored. An absent member is identity: the comparand and the column stay as written, which is what a host that passes neither got before. `NativeSQLStrategy` (the read scope merged into the native statement) and the `ObjectQLStrategy` echo (`/analytics/sql`) pass the context's pair, which `AnalyticsServicePlugin` wires to the driver. Before, the comparand was bound as written and the database read it by its own rules, on SQLite and on PostgreSQL whatever the server's time zone. + + **The draft preview.** It has no driver, so each value comparison on a column the host declares `datetime`, `date` or `time` now puts both sides in the storage form `@objectstack/core`'s `temporalStorageForm` gives: the comparand, and the drafted row's value, as `driver-memory` reads them. Before, it compared the two spellings as text. A column the host names no type for is compared as written, as before. + + A `date` column answered the engine's rows on both faces before and still does when both sides are spelled as days. No `@objectstack/spec` contract changes and no dependency edge is added. A host that calls `compileScopedFilterToSql` directly gets the coercion by passing the pair from its driver. + +### Patch Changes + +- f9f9f91: Analytics filter refusals, the no-strategy diagnostic and the cube-gate warning no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings the analytics service shows to callers, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - The two field-reference refusals (a `{ $field }` comparand the SQL lowering cannot render, and a `{ $field }` used as a `$between` bound) say the engine path's driver enforces the cross-field rules (declared same-table columns only, never the tenant-isolation column, one comparison class) with metadata it owns, so those rules are enforced in one place. The bound refusal also says `FieldReferenceSchema` was removed from the `$between` endpoint union rather than implemented there, since nothing asked for it. + - The no-strategy diagnostic for a cross-field filter on a deployment with no aggregate bridge says the same about the engine path. + - The `where` refusals: an undefined comparand is refused rather than read as null, on the SQL drivers and on this door alike; a field constraint with zero operators is refused on every backend, because neither "every row" nor "no row" is the author's intent; a field constraint mixing `$` operators with bare keys is refused by both doors in the package; and the two filter-array refusals say a filter array is lowered at every door or refused, never dropped, so it means the same rows whichever door it enters. Where the undefined-comparand refusal cited a tracker number for the silent widening, it now says that a dropped predicate widens the query; the mixed-wrapper refusal already said so and only drops its citation. + - The dotted-measure refusal drops its citation; the sentence already says measures do not traverse relationships and that the prefix used to be dropped silently. + - The warning logged when no object-registry hook is configured says the inactive gate is the one that answers 404 `CUBE_NOT_FOUND` for a name that is neither a registered cube nor a registered object. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 44072fc: The read-scope comparand refusals, the native-SQL cross-field backstop and the two display-SQL echo refusals no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings the analytics service shows to operators and callers pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - The read-scope compiler's undefined-comparand refusal says an undefined comparand is refused rather than read as null, on the SQL drivers and on this door alike. Its refusal of a non-boolean `$null`, `$exists` or `$empty` comparand says a non-boolean comparand for any of the three is refused rather than coerced, on every driver and on this door alike. Both still say they fail closed, and that the producer to fix is whoever built the read scope, never the caller of the query. + - The native-SQL strategy's cross-field backstop and the `/analytics/sql` echo's refusal of a field-reference comparison say the engine path's driver enforces the cross-field rules (declared same-table columns only, never the tenant-isolation column, one comparison class) with metadata it owns, so those rules are enforced in one place, next to the metadata they read. + - That echo refusal and the echo's unmapped-operator refusal say the echo renders every predicate the query runs with, or refuses. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 41a3c8d: Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it + + Clause-②: no + + `driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships: + + - `@objectstack/spec`: + - The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it. + - `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`). + - `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did. + - A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it. + - `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too. + - `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`. + - `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- fbe2deb: fix(service-analytics): the ObjectQL strategy applies a query's `order`, then its `offset` and `limit`, to the aggregated answer, as its echoed `sql` says + + Clause-②: no + + **Before**, the ObjectQL strategy passed none of the three keys to `engine.aggregate`, which has no ordering or window grammar, and applied none of them itself. Every date-bucketed query lands on that strategy, because the native-SQL strategy declines `granularity`. Measured through `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14: + + - `timeDimensions: [{ dimension: 'closed_on', granularity: 'month' }]`, `order: { closed_on: 'desc' }`, `limit: 1` answered every month, unordered (ascending on SQLite, `04, 03, 05` on PostgreSQL). + - A selected dimension with `order: { note: 'desc' }`, and a selected measure with `limit: 2, offset: 1`, answered every group in the engine's order. + + The echoed `sql` and `POST /api/v1/analytics/sql` rendered `ORDER BY … LIMIT … OFFSET …` for all three. + + **Now** the strategy orders the answer by `order`, in the key order given, and then applies `offset` and `limit`. This happens on the direct path and on the cross-object (FK-expand) path, after the re-bucket. A bare `limit` with no `order` slices the engine's order, as `LIMIT` without `ORDER BY` does. Where the native-SQL strategy answers the same query, the two answer the same rows for numbers and for text of single-case ASCII letters. The comparison is the dataset door's own `applyOrdering`, which sorts NULL and `''` last in both directions, while SQL places NULL by driver (lowest on SQLite, highest on PostgreSQL), so the two faces can still order NULL, `''`, numeric text and mixed-case text differently. + + **Dataset door.** `POST /api/v1/analytics/dataset/query` pushes a single query's `order`, `limit` and `offset` down to the strategy, and then windowed the answer a second time, so `offset` was applied twice. `limit: 2, offset: 1` over five groups answered one row, the third, on the native-SQL strategy. It now windows only a grid it could not push down. The ObjectQL strategy answered that page correctly before, because it dropped the window; it still does. + + **Unchanged.** A query with no `order`, `limit` or `offset` answers exactly the engine's aggregate rows. Which `order` keys are accepted is unchanged: the analytics door still refuses a key the query does not select. The dataset door's own ordering is unchanged too: label sort keys, derived measures, the implicit dimension order for a bare `limit`, and the chronological default. +- 6d67ad5: fix(spec)!: an analytics query's `limit` and `offset` are non-negative integers, and the native face runs an `offset` with no `limit` on SQLite + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing of a published request schema, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads it: the `/analytics` doors, which parse every body with `AnalyticsQueryRequestSchema` (`POST /analytics/query`, `POST /analytics/sql`) or `DatasetSelectionSchema` (`POST /analytics/dataset/query`), and answer `400 VALIDATION_FAILED` before any engine runs. + + **`@objectstack/spec`** + + - **`AnalyticsQuerySchema.limit` and `.offset`** were a bare `z.number()`. They are `z.number().int().nonnegative()` now. A negative number, a fraction, and an integer above `Number.MAX_SAFE_INTEGER` are refused at the member. `limit: 0` stays legal and answers no rows. + - **`DatasetSelectionSchema`** reads the same two declarations off `AnalyticsQuerySchema.shape`, so the dataset door holds the same accept set with no second copy. **`AnalyticsQueryRequestSchema`** extends the query, so it holds it too. + - The TypeScript types are unchanged (`number`). Only the parse narrows. + + Before, no refused value had one answer. Measured at `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14, `order { note: 'asc' }` over four groups: + + | window | native SQLite | native PostgreSQL | ObjectQL face | + |:--|:--|:--|:--| + | `limit: -1` | every row | 500 | all but the last row | + | `limit: 1.5` | 500 | two rows | one row | + | `offset: -1` | 500 | 500 | every row | + + Each one now answers `400 VALIDATION_FAILED`, with `details.fields[].field` naming `limit` or `offset` (`selection.limit` / `selection.offset` at the dataset door), on both drivers and both faces. + + **`@objectstack/service-analytics`** + + - **An `offset` with no `limit`** is a valid window: every row after the offset. The native-SQL strategy wrote `OFFSET n` with no `LIMIT` in front of it, and SQLite's grammar has no `OFFSET` without a `LIMIT`, so the query answered `500` (`near "OFFSET": syntax error`) on SQLite, while PostgreSQL and the ObjectQL face answered rows. The statement now carries the executing driver's no-limit spelling, read off the `sqlDialect` hook: `LIMIT -1 OFFSET n` on SQLite, `OFFSET n` alone on PostgreSQL (unchanged bytes), and `LIMIT 9223372036854775807 OFFSET n` when the host names no dialect. The MySQL arm is `LIMIT 18446744073709551615`, asserted as text only (no MySQL server was available to run it). + - The echoed `sql` and `POST /analytics/sql` show the statement that ran, byte for byte, on this face. + + ## FROM → TO + + | you wrote in an analytics query or dataset selection | write instead | + |:--|:--| + | `limit: -1` (meant: no limit) | omit `limit` | + | `limit: 1.5` | the integer page size you meant, for example `limit: 2` | + | `offset: -1` | omit `offset`, or `offset: 0` | + | `offset: 2.5` | the integer number of rows to skip, for example `offset: 2` | + + The one-line fix: write `limit` and `offset` as non-negative integers, or leave them out. + + ## Who is affected, measured + + At `origin/main` `ee75aae1a`: no example, package fixture, document or published skill writes a negative or fractional analytics `limit` or `offset`. The one stored producer that lowers into a dataset selection, a dashboard widget's `limit`, is already declared a positive integer (`z.number().int().positive()`). The sibling console repository and deployed metadata were not measured. The service does not parse a query passed to it in-process, so a host that builds an `AnalyticsQuery` in code parses it with `AnalyticsQuerySchema` before handing it over. +- d7d5b4f: fix(service-analytics): the ObjectQL strategy's echoed `sql` renders an offset with no limit in the dialect's own spelling, so SQLite runs the statement it prints + + Clause-②: no + + **Before**, the ObjectQL strategy wrote its own row window into the statement it echoes: `LIMIT n` when a limit was set, then `OFFSET n` when an offset was. An `offset` with no `limit` therefore echoed a bare `OFFSET`, which SQLite's grammar does not have. Measured through `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` on SQLite, for a composition served by the engine aggregate, with `order: { note: 'asc' }` and `offset: 1`: the rows were right (every group after the first), but the echoed `sql` and the `/analytics/sql` body both ended `ORDER BY "note" ASC OFFSET 1`, and SQLite refuses that statement with `near "OFFSET": syntax error`. + + **Now** the statement ends with the same window clause the native-SQL strategy runs, for the dialect of the driver that serves the object: `LIMIT -1 OFFSET 1` on SQLite, which runs and answers the same rows. One function renders the window for both strategies. + + **Unchanged.** The rows either strategy answers. A window with a `limit` keeps its bytes (`LIMIT 2 OFFSET 1`) on every dialect, and on PostgreSQL an offset with no limit still echoes `OFFSET 1` alone. A host that wires no `sqlDialect` hook gets the native strategy's dialect-neutral spelling, `LIMIT 9223372036854775807 OFFSET 1`. A date-bucketed dimension still echoes as `date_trunc(…)`, which SQLite does not run; this change touches only the window. +- 5d095a0: On SQLite, a `week` date bucket is grouped in SQL, and the analytics SQL echo never prints a bucket statement that SQLite refuses (#21595). + + Clause-②: no + + - **What was wrong.** `driver-sql` grouped `day`, `month`, `quarter` and `year` in SQL on SQLite, but not `week`. Its `supports.queryDateGranularity` said `week: false`, so the engine bucketed weeks in memory, and the ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` echoed the bucket as `date_trunc('week', col)`. SQLite has no `date_trunc`, so that echo could not run. A non-UTC `timezone` on SQLite gave the same echo for every granularity. + - **What it does now.** + - SQLite advertises all five granularities. `week` buckets as `YYYY-Www`, the ISO 8601 week that the PostgreSQL and MySQL arms answer. The expression does not use `strftime('%V')`, which needs SQLite 3.46: `@libsql/client` 0.18.0 bundles SQLite 3.45.1, where `%V` answers NULL. It runs on better-sqlite3, on libSQL (`driver-turso`) and on sql.js (`driver-sqlite-wasm`). A `Field.date` still buckets as its own calendar day. + - The echo prints that expression for a `week` bucket on SQLite, and the statement runs. + - With a non-UTC `timezone` on SQLite, `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. `POST /api/v1/analytics/query` still answers the rows, and its answer carries no `sql`. The engine buckets on that zone's calendar in memory, and SQLite has no time-zone database, so no SQLite statement produces those keys. + - **Where it shows.** `aggregate()` with a `week` group on SQLite, `SqlDriver.dateBucketSql()`, and the analytics SQL echo. A query sent with `timezone: 'UTC'`, or with no `timezone`, still echoes the driver's own expression. +- 1968d5e: With a non-UTC `timezone`, the analytics SQL echo of a date-bucketed dimension refuses on every dialect instead of printing `date_trunc` (#21630). + + Clause-②: no + + - **What was wrong.** With a non-UTC `timezone`, the engine buckets a date dimension in memory on that zone's calendar, on every driver. The ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` still echoed the bucket as `date_trunc('month', col)` (or the asked granularity) on PostgreSQL and MySQL, a statement the engine never ran. On PostgreSQL that statement groups on the database session's calendar: measured on PostgreSQL 16.14 with the server at `Asia/Shanghai`, it answered timestamp keys such as `2025-12-31T16:00:00.000Z` where the query answered `2026-01`, and with `timezone: 'America/New_York'` it grouped the rows differently from the query. MySQL has no `date_trunc` at all. SQLite already refused this echo. + - **What it does now.** For a date-bucketed dimension with a non-UTC `timezone`, on every dialect: + - `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. This is the answer SQLite already gave. + - `POST /api/v1/analytics/query` answers the same rows as before, and its answer carries no `sql`. + - **Unchanged.** A query sent with `timezone: 'UTC'`, or with no `timezone`, still echoes the expression the driver groups by: `to_char(…)` on PostgreSQL, `date_format(…)` on MySQL and `strftime(…)` on SQLite. +- 31e3e00: The analytics SQL echo prints a date bucket only in the expression the driver itself groups it by, and refuses everywhere else, including on the in-memory and MongoDB drivers (#21647). + + Clause-②: no + + - **What was wrong.** At a `timezone` of `UTC`, or with none, the ObjectQL face of `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` echoed a date-bucketed dimension as `date_trunc('month', col)` (or the asked granularity) wherever the driver renders no bucket expression of its own, and documented that as representative. On `driver-memory` the engine only fetches the rows and buckets them itself, answering keys such as `2026-01` and `2026-W02`, while both faces printed `date_trunc(...)`, a statement nothing ran. `driver-mongodb`, which groups the bucket in its own aggregation pipeline, took the same path. So did any host that wires no `dateBucketSql` hook. + - **What it does now.** Wherever no driver expression stands for the bucket, on every driver and dialect: + - `POST /api/v1/analytics/sql` refuses with `NOT_IMPLEMENTED` / 501, declared as a refusal so its message reaches the caller. Its message names the cause. A non-UTC `timezone` and SQLite already answered this way. + - `POST /api/v1/analytics/query` answers the same rows as before, and its answer carries no `sql`. + - **Unchanged.** On PostgreSQL, MySQL and SQLite at `UTC` or with no `timezone`, the echo still prints the expression the driver groups by: `to_char(...)`, `date_format(...)` and `strftime(...)`. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 68d9716a9e0..1373265dd30 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "17.6.0", + "version": "17.7.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 e45e48f56b3..8967a751769 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,333 @@ # @objectstack/service-automation +## 17.7.0 + +### Minor Changes + +- 909229e: A job pulls a mapping's connector source by declaration — `pull: { mapping }` — and every job runs as the `organization` it declares (#20281). + + Clause-②: yes (widening) + + - **`JobSchema.pull`** (`@objectstack/spec/system`). A third run form beside `body` and `handler`: `{ mapping: '' }`. On each run the platform pulls that mapping's `connectorSource` and writes the rows through the import runner. It carries no code. The key is refused beside `body` or `handler`, because one of the two run forms would never run. `body` with `handler` stays legal, and the body still wins. A job must now declare one of `body`, `handler` or `pull`. `pull` is closed: an unknown key inside it is refused. + - **`JobSchema.organization`**. The organization a job runs as. It applies to the body's `ctx.api`, to the handler's new `executionContext`, and to the pull's reads and writes. The value shape is the scheduled flow's: a non-empty `sys_organization.id`. A near-miss spelling (`organizationId`, `orgId`, `tenantId`, …) is refused at parse and pointed at the key. + - **`defineStack`, and so `os validate`**, refuses a job whose `pull` names a mapping the stack does not declare, or a mapping with no `connectorSource`. The refusal is the existing `STACK_CROSS_REFERENCE_INVALID` envelope. + - **`IAutomationService.pullConnectorSource`** (`@objectstack/spec/contracts`, with `ConnectorSourcePullRequest`, `ConnectorSourcePullResult` and `ConnectorSourcePullSummary`). The connector sync executor is now on the `automation` service. `@objectstack/service-automation`'s engine serves it from the executor `AutomationServicePlugin` attaches at init (`AutomationEngine.setConnectorPullSource`). A bare engine refuses with `SERVICE_UNAVAILABLE` (503). + - **The job binder** (`@objectstack/runtime`, `scheduleAppArtifactJobs`) schedules a `pull` job on every door: the boot, and `os package install` on install and rehydrate. Each run calls `pullConnectorSource` through the service registry. A refused pull fails the run, and `retryPolicy` applies. A pull whose rows the import runner refused records the run `degraded`, with the counts. A pull naming a mapping the artifact does not carry is not scheduled, and neither is one whose mapping has no `connectorSource`, nor one on a kernel whose `automation` service cannot pull. Each case is logged at `warn` with the reason. `collectJobsWithoutBody` does not name a `pull` job that binds, so `os package install` installs one. The result gains `pulls` and `missingOrganization`. + - **The organization, judged at bind** by the posture rule scheduled flows use (`resolveScheduledWorkPolicy`). Every run carries `{ isSystem: true, tenantId: }`, or `{ isSystem: true }` for a job that declares none. Under `single` the key is not required. Under `group` it is optional; an undeclared job is scheduled and named once at `warn`, because a tenant-scoped row it writes is refused. Under `isolated`, with package-authored scheduled work switched on, it is **required**. **Action on such a deployment:** declare `organization` on each packaged job, or the job is not scheduled; the error log names the job. Until now such a job was scheduled, and every tenant-scoped write it made was refused at the write. An unrecognized `OS_TENANCY_POSTURE` withholds every job (`scheduled-work-policy-unreadable`) instead of guessing whether a declaration is required. + - **Texts this makes true.** The `mapping.connectorSource` description, the `connector.syncConfig` tombstone prescription and the `connector-sync-keys-retired` upgrade entry said "nothing schedules a pull yet". They now name the `job` `pull` that drives it. + + Nothing that parsed before is refused now. Every new refusal falls on a key that did not exist before this change. +- 96a9719: feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790) + + Clause-②: yes (widening) + + A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it. + + **⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '' (): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification. + + What else changes: + + - **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`. + - **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential. + - **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials. + - **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing. + - **Refused now, loudly**: + - With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again. + - The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret. + + +- 748b240: feat(types,automation): a host's per-kernel scheduled-work OFF reports the host's own reason (#21110) + + Clause-②: yes (widening) + + `ScheduledWorkPolicy` (`@objectstack/types`) gains an optional + `hostDisabledReason`: the host's own sentence for why scheduled work is off on + this kernel, such as a plan that does not include scheduled flows. A new + export, `scheduledWorkDisabledReason(policy)`, gives the one answer for why + scheduled work is not armed under a policy. It returns the host's reason when + the policy carries one, and `SCHEDULED_WORK_DISABLED_REASON` otherwise. + + Every refusal site now reports that answer, read from the same policy reading + that refused: + + - the automation engine's bind log; + - the reason it records for `getTriggerBindingAudit()` and for the + `FlowRuntimeState.reason` that `GET /automation/_status` serves; + - the refusal of `ScheduleTrigger` and `TimeRelativeTrigger` when a host drives + them directly. + + Before this, a kernel that a host turned off through `scheduledWorkPolicy` + was reported with the deployment sentence. That sentence tells the reader to + set `OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true`, even on a process where the + variable is already set, and to a tenant who cannot set it. + + Nothing changes without the new field. A policy with no `hostDisabledReason`, + and the zero-argument deployment resolver `resolveScheduledWorkPolicy()`, which + never sets it, report `SCHEDULED_WORK_DISABLED_REASON` byte for byte. The field + is read only when `enabled` is `false`. + + To use it, a host that turns one kernel off for its own reason sets + `hostDisabledReason` on the `enabled: false` policy it already hands to that + kernel's `AutomationServicePlugin`, `ScheduleTriggerPlugin` and + `TimeRelativeTriggerPlugin`. Give the same policy to all three, as before, and + make the reason a whole sentence that names the cause and the remedy. It is + shown verbatim. +- 54fb60a: fix(service-automation)!: a flow the `kernel:ready` cold-boot bind refuses is no longer left registered and `active` from the boot pull + + Clause-②: no (narrowing) + + + + **BREAKING**: a flow that loaded `active` before can now be absent after boot. It ships as `minor` under the launch-window convention for accept-set narrowings. + + **What was kept before.** A boot registers a package's flows twice. The boot pull runs before a plugin that contributes a node type has registered its executor from its own `start()`, so it cannot check that node's config keys against the descriptor's `configSchema`, and it registers the flow and arms its trigger. The `kernel:ready` bind then re-registers every flow once the executor exists. When it refused one, for an undeclared config key for instance, it logged `[Automation] cold-boot flow bind: failed to register flow` and nothing else: the boot pull's registration stayed, `active` and bound to its trigger, so every run reached the node the refusal located. + + **What happens now.** A flow the `kernel:ready` bind refuses is withdrawn: it is not registered, its trigger is unbound, and the same warning names the flow and the refusal. Only that flow is withdrawn; the rest of the package loads, as it already did for a flow the boot pull refuses. A failed or empty read of the flow list still tears nothing down. + + **What an author sees now.** The flow is absent (`GET /automation/:name` answers `404`), and the boot warning carries the located refusal. The handling is to correct the config the warning locates; the flow then registers as before. + + **Unchanged.** What `registerFlow` refuses, at any door. A flow refused through the `/automation` write doors keeps the definition the engine already held, and a runtime reload that brings a refused body keeps the registered one. + +### Patch Changes + +- cc07862: Automation refusals, prescriptions, log lines and run-object field help, and the activity type help, no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these two packages show to flow authors, operators and administrators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/service-automation`: the refusal for a `fieldValues` write map says a runtime alias for it was rejected by design, so the node keeps one strict `fields` key; the refusal for a screen field's `visibleIf` says a predicate under any other key is never read, so the field always shows, and a `required` field meant to stay hidden then blocks the screen from ever being submitted; the undeclared-config-key refusal says the built-in node types were reconciled so that every key their executors read is declared; the unknown-function error in a flow value expression says such a name is refused rather than evaluated to null, which would write the field as undefined; the inert-connector warning says entries without a `provider` are catalog descriptors, while an entry that names a `provider` is a connector instance that provider's installed executor materializes; the `sys_automation_run` field help says the paused node's type decides who may continue a run (an approval pause only through its owning service), that rows written before run history recorded its trigger were not backfilled, and that a finished run's bounded step log keeps its per-node detail across a restart; three bridge debug lines say what each bridge provides. The bulk-intent guidance, the degraded-connector dispatch error and retry lines, the user-less `runAs` warning and refusal, the unclaimed-branch warning, the script-function and node-config refusals and the `sys_flow_dispatch` description drop their citations. + - `@objectstack/plugin-audit`: the `sys_activity` `type` help, whose English text all four shipped locale bundles carry, says the vocabulary is open by decision, not a gap awaiting enforcement. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- a4f0cb0: fix(service-automation): a flow's `get_record` node that reads the stored-metadata tables is served what the generic data door serves (#21519) + + Clause-②: no + + The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and a content hash computed over it. The generic data door serves such a row with the body as its type's read projection, with the stored credential material withheld, and the hash in keyed form. A flow's `get_record` node read the same rows and served them as stored, under either run identity (`runAs: 'system'` and `runAs: 'user'`). What it read went into the run's declared output, which the flow's caller is handed back, and into any record the flow wrote from it. + + **What changes.** When the node reads either table, its answer now takes the data door's form, for one row (`findOne`, no `limit`) and for a row list (`find`, `limit` above 1). The body is projected, and the content hash is keyed under the same key the data door uses. That key is the crypto provider's, read from the data engine when the node runs, or the process-scoped ephemeral key when no provider is registered. So the hash a flow is served equals the hash the data door serves for the same row. A record the flow writes from what it read can therefore carry only the projected body and the keyed hash. A `fields` projection that names the body column without the type column reads the type beside it and drops it again, as on the data door. + + **What does not change.** Every other object is read exactly as before. The node's other config keys (`filter`, `limit`, `outputVariable`) and the write nodes behave as before. The node consumes the data door's own functions from `@objectstack/metadata-protocol` (`storedMetadataBodyProjection`, `redactStoredMetadataRows`, `serveStoredMetadataHashColumnRows`, `ephemeralStoredHashDigest`) and keeps no copy of them. `@objectstack/service-automation` now depends on `@objectstack/metadata-protocol`. A composition that runs flows on `ObjectQLPlugin`, as `os dev` and `os serve` do, already loaded that package. +- 96b0e31: fix(service-automation): a flow's `get_record` node refuses a filter that evaluates the stored-metadata tables' body or content hash, as the generic data door does (#21623) + + Clause-②: no + + The two stored-metadata tables (the current metadata bodies and their version history) hold each body as stored, credential material included, and content-hash columns computed over it. A flow's `get_record` node now serves those rows projected and keyed, but it still ran its `filter` against the stored values as written, under either run identity (`runAs: 'system'` and `runAs: 'user'`). A filter over the body column or a content-hash column was evaluated row by row, so whether a row came back answered the filter: a predicate over the withheld values. The generic data door refuses those filters before its query runs. + + **What changes.** When the node reads either table, it judges its filter the way the data door judges the same filter, before the data engine is asked, on both branches (one row, and a row list when `limit` is above 1). The columns the filter reads are collected after interpolation, so a condition that a `{token}` supplies is judged too. A filter that reads the body column, or a content-hash column (the history table's parent hash and change note included), refuses the node with the data door's own message and error code, `INVALID_FIELD`. The refusal is a guard failure: the run fails, nothing downstream of the node runs, and a `fault` edge does not route it. A `try_catch` catch region reads the code on `{$error.code}`. To read a stored-metadata row from a flow, filter by `name`, `type`, `state` or another scalar column. + + **What does not change.** A filter over scalar columns is served as before: the body projected and the hash keyed. Every other object is filtered and read exactly as before, including columns that share these names. The write nodes are unchanged. The node consumes the data door's own functions from `@objectstack/metadata-protocol` (`collectStoredMetadataFilterFields`, `storedMetadataBodyPredicateRefusal`, `storedMetadataHashEvaluateRefusal`) and keeps no copy of them. +- f40bb32: fix(service-automation): a flow's `create_record`, `update_record` and `delete_record` nodes refuse a stored-metadata table as their target (#21624) + + Clause-②: no + + The two stored-metadata tables (the current metadata bodies and their version history) have one writer for app-authored work: the metadata protocol, where a change is validated and its provenance is recorded. A flow's write nodes wrote those tables directly, outside it. Under `runAs: 'system'` the write ran elevated, so the security middleware never judged it; under `runAs: 'user'` only a composition with the security plugin refused it, as a routable runtime failure with no code. A write node's `filter` was also evaluated against the stored rows, so whether the write acted answered a predicate over the stored body. + + **What changes.** A `create_record`, `update_record` or `delete_record` node whose `objectName` is either table is refused before it resolves its filter or its field values and before any engine write, under either run identity. The refusal names the metadata API as the way to change metadata and carries the standard `PERMISSION_DENIED` code, the code the data door answers a non-platform principal's write to these tables with. It is a guard failure: the run fails, nothing downstream of the node runs, and a `fault` edge does not route it. A `try_catch` catch region reads the code on `{$error.code}`. Metadata is changed through the metadata API (`PUT /api/v1/meta/:type/:name`), never through a flow's data nodes. + + **What does not change.** Every other object is created, updated and deleted exactly as before. `get_record` keeps serving these tables projected and keyed. +- 5ac2ba1: fix(service-automation): a flow write node's refusal of a stored-metadata table ends on the same prescription sentence as the save-time refusal (#21624) + + Clause-②: no + + A flow `create_record`, `update_record` or `delete_record` node aimed at a stored-metadata table is refused twice: at save by `FlowSchema`, and at run time by the node itself, for a definition the parse never judged. Both refusals tell the author where a metadata change goes instead, and until now they said it in two spellings of one sentence: the run-time refusal named the elevation as `runAs: 'system'`, the save-time one as `runAs`, a system context. + + **What changes.** The run-time refusal's message keeps its lead (the node type, what it would have done and the table, and that the write was not run) and now ends on `STORED_METADATA_BODY_PRESCRIPTION`, imported from `@objectstack/spec/kernel`: the one sentence the save-time refusal and the hook refusal also end on. Its elevation clause now reads "Elevation (`runAs`, a system context) does not change this." + + **What does not change.** Which writes are refused, the refusal's `PERMISSION_DENIED` code, its guard classification (a `fault` edge does not route it) and every other object's writes are exactly as before. +- 73b2246: A flow saved through the metadata API is armed on the running engine at once, as hooks and actions saved through the same door already are + + Clause-②: no + + `PUT /api/v1/meta/flow/:name` answered `200 "Saved flow … (env-wide, state=active)"` and `GET /api/v1/meta/flow/:name` served the row, but the automation engine registered nothing until the process restarted: `GET /api/v1/automation/:name` and `POST /api/v1/automation/:name/trigger` answered `404 Flow not found`, and a record-triggered flow never fired. The engine armed flows only at boot, at `kernel:ready` and on `metadata:reloaded`, and only the publish doors announce that event. + + The automation service now listens to the metadata protocol's post-write signal (`onMetadataMutation`), the one ObjectQL already re-binds authored hooks and actions on, and makes the engine follow the stored row of each flow it names: + + - an active save or a publish registers the flow, or re-registers it over the definition the engine held; + - a save whose `status` is `'obsolete'` or `'invalid'` keeps it registered and unbound, as a boot does; + - a delete unregisters it; + - a draft save changes nothing until it is published. + + The flow is re-read through the same execution view, precedence and env-wide scope the boot reads, so a save never arms a flow beyond the reach a restart would give it. The save answers first, and the registration follows it by one read of the stored metadata. + + A publish raises this signal and `metadata:reloaded` together, and the published flow is still registered once, not twice. `PUT /api/v1/automation/:name`, which registers the flow before it saves it, is not registered a second time either. + + A run already executing keeps the definition it started with. A suspended run resumes against the definition registered when it resumes, and answers `RUN_NOT_FOUND` once its flow is deleted. Both already held for a re-registration through a publish or `PUT /api/v1/automation/:name`. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [0a0debb] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [0e10be6] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [535d1d2] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [e9dec3d] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [ce53218] +- Updated dependencies [44defd4] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [74281a8] +- Updated dependencies [9a4182a] +- Updated dependencies [550f4cc] +- Updated dependencies [41b1333] +- Updated dependencies [5dbcee8] +- Updated dependencies [ec390ec] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [5d0e4e2] +- Updated dependencies [e367002] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [7b07749] +- Updated dependencies [eea82af] +- Updated dependencies [a2aadab] +- Updated dependencies [ced217c] +- Updated dependencies [ff16740] +- Updated dependencies [fe10172] +- Updated dependencies [7fd2c34] +- Updated dependencies [c43a8ae] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [cf60dbc] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [b7a13c7] +- Updated dependencies [045f764] +- Updated dependencies [18c2ddc] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [18fe681] +- Updated dependencies [3237b4a] +- Updated dependencies [2e78046] +- Updated dependencies [cab6396] +- Updated dependencies [87712ab] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [9cc2c79] +- Updated dependencies [d16b9fb] +- Updated dependencies [753e7a1] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [c9761cd] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/formula@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index 0dc7e5f701f..0f4f6b38570 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "17.6.0", + "version": "17.7.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 b9ebcfde0c3..cbe8f348734 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,121 @@ # @objectstack/service-cache +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/observability@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 851bf0ef814..eba700989e3 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "17.6.0", + "version": "17.7.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 012a9bb1161..98e248f13e3 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,113 @@ # @objectstack/service-cluster-redis +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/service-cluster@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 3ebf3a468e7..dfbe7126f7e 100644 --- a/packages/services/service-cluster-redis/package.json +++ b/packages/services/service-cluster-redis/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster-redis", - "version": "17.6.0", + "version": "17.7.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 5d1a2b2232e..c23179a26b3 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/service-cluster +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index d14b3822207..3a463b604b5 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "17.6.0", + "version": "17.7.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 35b4d0b013a..6a5b3285d63 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,314 @@ # @objectstack/service-external-datasource +## 17.7.0 + +### Minor Changes + +- 07e933b: fix(service-datasource)!: "Import as Object" saves the imported federated object through the metadata door's own save, so it is durable and reads from its remote table; federated validation stops reporting the platform's injected anchors as missing remote columns (#21788) + + **BREAKING** — the import route now answers `400` to some re-imports that used to answer `201`. + + Clause-②: no (narrowing) + + - **The import was neither durable nor mapped.** `POST /api/v1/datasources/:name/external/tables/:remote/import` held the generated object in the metadata service's memory only. No `sys_metadata` row was written, the object's storage was not synced, and the driver was never told the object's remote table. An object imported under a name that differs from its remote table answered `201` and then `500 DATABASE_ERROR` (`no such table: `) on its first read. Every import was gone after a restart (`404 OBJECT_NOT_FOUND`). + - **It now saves like `PUT /api/v1/meta/object/:name`.** The import calls `saveMetaItem` on the `protocol` service with the request that door sends for an `object`. The object becomes a `sys_metadata` row the next boot binds, it is written through to the engine registry, and it is mapped onto its `external.remoteName` table. An import under a different name and one under the remote table's own name both serve the remote rows, before and after a restart. The save door is looked up when an import runs. A deployment with no metadata save door still refuses the import with "requires a writable metadata store", before any remote introspection. + - **What narrows.** The metadata door's refusals now apply to the import, and the route relays each one as `400 EXTERNAL_IMPORT_ERROR` with the door's message. A re-import that would drop a field the stored object already has, or change its type, is refused as a destructive change. It used to answer `201` with an in-memory overwrite that a restart discarded. An import whose `name` collides with an object the metadata door will not overwrite is refused the same way. The route's request and response shapes are unchanged. + - **If a re-import or a name is refused:** import the table again under a new `name`. To change an object you already imported, save its new definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Re-submitting the import with `?force=true` does not help, because the import route reads no `force`. + - **Federated validation compares only what the remote owns.** A stored federated object is read back with the anchors the platform injects without storage (`organization_id`, `created_by`, `updated_by`, `owner_id`, `owning_business_unit_id`). The boot validation gate and `POST …/external/validate` reported each of them as a `missing_column` at error severity. So once a datasource with the default `external.validation.onMismatch: 'fail'` held such an object, it refused to boot. Validation now skips the columns `unprovisionedInjectedColumns` names. This closes the boot abort for objects saved through `PUT /api/v1/meta/object/:name`, not only for imports. A declared column the remote lacks is still a `missing_column` at error severity, and `fail` still aborts on it. + + +- bc7747c: fix(service-datasource)!: on `objectstack start`, external validation and the boot gate compare every federated object + + **BREAKING (narrowing)** — on `objectstack start` a deployment with real schema drift on a + federated object can now refuse to boot, as its `external.validation.onMismatch` setting + declares. + + The federation service (`ExternalDatasourceServicePlugin`) read the `metadata` service once, + in its `init()`, and kept the answer. `objectstack start` composes no metadata plugin: its + `metadata` service is the kernel's in-memory fallback, which the kernel registers after every + plugin's `init()`, just before the start phase. So on `start` the federation service kept "no + metadata service" for the life of the process, and every read behind it answered as if the + deployment declared nothing. It now asks for the `metadata` service each time it reads it, so + `start` sees what `objectstack dev` always saw. + + | on `objectstack start` | before | now | + | --- | --- | --- | + | the boot gate (ADR-0015 §5.2) | logged "all federated objects match their remote schema" with `objects: 0`, having compared nothing, so no `onMismatch` policy ever applied | compares every federated object and applies each datasource's `onMismatch` to every measured mismatch | + | `POST /api/v1/datasources/:name/external/validate` | `ok: true` with no rows | one row per federated object bound to the datasource, with its diffs | + | `POST /api/v1/datasources/:name/external/refresh-catalog` | answered the snapshot, never stored it | also stores it as the datasource's `external_catalog` record | + | `GET /api/v1/datasources/:name/external/tables` | ignored the datasource's `external.allowedSchemas` | leaves out a table whose schema is outside them | + | draft and import names | never resolved a package namespace: drafts were unprefixed, and an import's explicit `name` was never held to the ADR-0028 prefix rule | resolve the namespace of the datasource's package when the datasource carries package provenance; an import whose explicit `name` lacks that prefix is refused `400 EXTERNAL_IMPORT_ERROR` | + + **What to do if a `start` deployment now refuses to boot.** Under `onMismatch: 'fail'` (the + default) the boot stops with `ExternalSchemaMismatchError`, naming the object, the datasource + and each drifted column. Either fix the drift (align the object's fields, its + `external.columnMap` or `external.ignoreColumns`, or the remote table) or set + `external.validation.onMismatch: 'warn'` on that datasource, which boots and logs the drift + instead. To see the drift before deploying, call + `POST /api/v1/datasources/:name/external/validate` on `objectstack dev`, which already + compared. A remote that cannot be reached still never stops the boot. An import refused for + its name takes a `name` carrying the datasource package's namespace prefix. + + **Unchanged.** `objectstack dev`, and every composition that registers a metadata plugin before + the federation service, answers exactly as before: measured on the showcase, every federation + door and the boot gate gave the same answers before and after. What validation judges, what + each `onMismatch` value does, and the boot gate's skip for a datasource that sets + `external.validation.checkOnBoot: false` are unchanged. + + Clause-②: no (narrowing) + + +- 76fec88: Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off. + + Clause-②: yes (widening) + + - **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes. + - **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too. + - **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`. + - **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in. + - **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt. + - **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged. + - What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects. + - ⛔ No new export on any package entry, and no new elevation API. +- 753e7a1: fix(service-datasource,runtime,metadata-protocol)!: a stored datasource row no longer displaces a code-defined datasource at boot, and the metadata door refuses edits to the host's `default` (#21922, #21944) + + Clause-②: no (narrowing) + + A code-defined datasource (one the installed artifact declares in `*.datasource.ts`, or the host's own `default`) is read-only: `DatasourceSchema.origin` publishes it as "GitOps-owned, read-only in the UI", and the datasource-admin service states "code wins on collision". The boot restore broke both. It registered every stored `datasource` row in `sys_metadata` over whatever the runtime had registered from code, so after a restart a row left under a code-defined name was served by the admin door, editable there when it carried `origin: 'runtime'`, and handed to pool rehydration. A stored `default` row opened a second live pool named `default` on the row's own connection. The metadata door also still saved edits to `default`, the one code-defined datasource no package declares. + + The runtime now keeps one in-memory set of the datasource names it registers from code, on the kernel service `code-datasource-names`: `AppPlugin` adds the datasources the artifact declares and `DefaultDatasourcePlugin` adds `default`, both in `init()`, so the set is complete before any `start()` runs. The boot restore skips a stored row under a name in that set, and the metadata door's code-datasource check reads the same set. + + **BREAKING — what moves for consumers.** + + - After a restart over a stored row under a code-defined datasource's name, `GET /api/v1/datasources` serves the code definition (`origin: code`) instead of the row, and `PATCH /api/v1/datasources/:name` answers `400 DATASOURCE_ADMIN_ERROR` ("… is code-defined and cannot be edited at runtime.") where it answered 200 for a row that carried `origin: 'runtime'`. + - No live pool is opened from such a row at boot. + - `PUT /api/v1/meta/datasource/default` answered 200 and now answers `403 NOT_OVERRIDABLE`. `DELETE /api/v1/meta/datasource/default` with no stored row answered 200 and now answers the same `403`. The refusal's remedy names the host's database configuration (the database URL the server starts with), which is what defines `default`; every other code-defined datasource's refusal still names its `*.datasource.ts` source. + - The skipped row is kept, and the boot logs one warning naming it. + + **Remedy.** + + - To change a code-defined datasource, change its code definition and redeploy: its `*.datasource.ts` source, or the host's database configuration for `default`. + - A row the boot warning names is removable, and removing it is the repair: `DELETE /api/v1/meta/datasource/:name` answers 200 and deletes it. + + **Unchanged.** A runtime datasource with no code twin restores, saves and deletes through both doors as before. A host that composes neither `AppPlugin` nor `DefaultDatasourcePlugin` registers no set, and its stored rows restore as before. While a stored row exists under a code-defined name, `GET /api/v1/meta/datasource/:name` still serves that row (the metadata door reads its stored overlay first); after the `DELETE` above it serves the code definition, in the same boot. + + + +### Patch Changes + +- f9bcd08: Datasource and approval refusals, warnings, field help and generated-draft comments no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these two packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/service-datasource`: the credential-migration refusal says an unbindable key is either an alias spelling from before inline credentials were refused at publish, which no connection builder reads, or turso's `encryptionKey`, which has no secret slot of its own because the one slot carries the `authToken`; the remote-primary-key comment in a generated object draft says a driver's introspection can report only the first column of a composite key, so the list is a lower bound. + - `@objectstack/plugin-approvals`: the `queue` approver warning says the platform has no ownership queue to expand the type from, that the type is no longer offered for authoring, and to route the step to a team, department or position instead; the live-record warnings say approvers are being resolved against the trigger snapshot instead of the live record they are normally resolved from; the recall refusal's log line names the admin override; the `sys_approval_action` `via_override` help (in every shipped locale) says a platform or organization admin may act on any pending request, so that one nobody in its slate can decide never stays stuck; the cross-organization team, team-member and manager warnings, the expanded-to-nobody warning, the revise-window refusal, the `attachments` help and the `sys_approval_delegation` description drop their citations. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 57cc695: feat(spec): `CryptoContext` gains a required `scope` discriminant, and `LocalCryptoProvider` binds it into a delimiter-safe, versioned AAD (ADR-0128 D1–D3, #21326 stage 1) + + Clause-②: yes + + **BREAKING** for `ICryptoProvider` implementers and for every direct caller of + `encrypt`, `decrypt` or `rotateKey`: `CryptoContext.scope` is required, so a + context literal without it stops compiling (`TS2741`), and the compiler names the + missing member. `LocalCryptoProvider` also refuses such a context at runtime with + `CryptoContextScopeError`, for a caller the compiler never saw. Code that only + injects a provider is unaffected. + + `scope` is a member of the new closed set `CRYPTO_CONTEXT_SCOPES` (type + `CryptoContextScope`), one member per producer of `CryptoContext`: + `settings` (`SettingsService`), `object_secret_field` (the ObjectQL engine's + secret-field path) and `datasource_credential` (the datasource secret binder). + Each producer in this release passes its own member on every call. A new producer + adds its own member; it never borrows an existing one. + + What the contract now requires of every provider that binds AAD: + + - **Producer-discriminated (D1).** The AAD binds `(scope, namespace, key)`, so a + ciphertext sealed by one producer does not authenticate under another + producer's context, whatever the two `(namespace, key)` pairs are. + - **Delimiter-safe (D2).** Distinct triples produce distinct AAD bytes. An + unescaped join is not permitted. + - **Versioned.** A ciphertext records which AAD derivation sealed it, and is + opened only with that derivation. An unknown derivation fails closed. No second + derivation or scope is ever tried after a failure (D3). + + `LocalCryptoProvider` seals every new value under derivation version 2: a lead + byte that never occurs in UTF-8, a versioned label, then the scope, namespace and + key, each prefixed with its 4-byte length. The ciphertext carries a `v2:` marker. + A ciphertext with no marker is version 1, the bare base64 every earlier release + sealed, and it still opens with the older `(namespace, key)` binding. Existing + secrets therefore keep working with no action, and carry the older binding until + they are re-wrapped. Re-wrapping existing ciphertext at rest is stage 2 of + #21326. `rotateKey` already re-seals a version-1 handle under version 2. Any other + marker is refused with `UnknownCiphertextVersionError`. + + Operational note: a secret set or rotated by this release carries the `v2:` + marker, and an earlier release cannot open it. A rollback past this release needs + those values to be set again. + + `@objectstack/objectql` and `@objectstack/service-datasource` pass their own + scope on every seal and open. Their public surface is unchanged. + + +- e864db5: fix(service-datasource): a destructive re-import's refusal names the remedies that work from the import route, instead of a `?force=true` that route never reads (#21841) + + Clause-②: yes (widening) + + - **What was wrong.** "Import as Object" (`POST /api/v1/datasources/:name/external/tables/:remote/import`) saves through the metadata door's own `saveMetaItem`. A re-import that would drop or retype a field the stored object still carries is refused by that save's destructive-change gate, and the import route relays the refusal as `400 EXTERNAL_IMPORT_ERROR`. The refusal ended `re-submit with ?force=true to proceed.` The import route reads no `force`, so a caller who did exactly that got the identical refusal back. + - **What the refusal says now.** The import states its own write face, and the refusal ends: this import cannot be forced, because the external-table import route accepts no `force`. Import the table under a new `name`, or save the changed definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Both remedies are measured on the showcase: each one answers `201` or `200` where the re-import answered `400`. The same words appear in this package's earlier changeset for the import. + - **What widens.** `SaveMetaItemRequestSchema.writeFace` (`@objectstack/spec`) and `saveMetaItem`'s `writeFace` parameter (`@objectstack/metadata-protocol`) gain one member, `'external-import'`. The member is stated by the server. No door reads it from a request body, and the import's own options cannot carry it, or a `force`, into the save. Nothing accepted today is refused. + - **What does not change.** The refusal itself stays: a destructive re-import is still `400 EXTERNAL_IMPORT_ERROR`, and the stored definition does not move. The import route gains no `force`. Acknowledging a destructive change stays on the metadata door. The other faces' wording is unchanged. A `422 INVALID_METADATA` relayed by the import keeps its full findings in the message, because the import route's envelope carries no `issues`. +- 25eb7de: fix(service-datasource): `POST /api/v1/datasources/:name/external/validate` sees a federated object saved at runtime, with no restart (#21842) + + Clause-②: no + + - **What was wrong.** The federation service read its objects from the `metadata` service. That service holds a copy of the engine's object registry taken once at boot. `PUT /api/v1/meta/object/:name`, and the external-table import that saves through it, write `sys_metadata` and the engine registry, but never that copy. So after a federated object was saved at runtime, validate answered the code-defined objects only, and listed the saved one after a restart. An object re-saved at runtime was judged on its definition as it stood at boot. + - **What it reads now.** `ExternalDatasourceServicePlugin` reads objects (`listObjects` and `getObject`) from the engine's object registry on the `objectql` service, which is the registry the save writes through to. The registry is looked up when validation runs, not when the plugin starts. A saved or imported object is listed and judged on what was saved, the moment the save answers. + - **What does not move.** The comparison is unchanged: the same federation predicate, the same column and type checks, and datasource definitions read from the same place. On `objectstack dev` the boot validation gate sweeps the same objects with the same verdicts as before. No route's request or response shape changes, and no export is added. +- faf8dce: An import over a code-defined datasource is held to the namespace of the package that declares it (ADR-0028), and the draft door answers the prefixed name (#21889). + + Clause-②: no + + - **Before.** `POST /api/v1/datasources/:name/external/tables/:remote/import` with an explicit `name` that carried no namespace prefix answered `201` and saved an unprefixed federated object, and `POST …/external/tables/:remote/draft` answered the bare remote table name with a `TODO(namespace)` note. Measured on the showcase's `showcase_external` under `objectstack dev` and `objectstack start`. + - **`@objectstack/runtime`.** `AppPlugin` registers each code-defined datasource through `applyProtection` with the id and version of the package body that declares it, so the item carries `_packageId`, `_packageVersion` and `_provenance: 'package'`. On an ADR-0130 `packages[]` artifact each datasource takes its own body's id, never the artifact's top-level manifest id. A top-level datasource that no body declares keeps its registration under the artifact's own id, and a warning names it. + - **`@objectstack/service-datasource`.** The federation service reads the datasource's package record from the engine registry (`registry.getPackage` on the `objectql` service), the store the runtime publish gate reads for the same check. It used to ask the `metadata` service, which holds no package records in any composition, so no datasource resolved a namespace. The package id still comes only from the stamped `_packageId`. + - **What a caller sees now.** On a datasource whose package declares `manifest.namespace`, an unprefixed import `name` answers `400 EXTERNAL_IMPORT_ERROR` with ADR-0028's message, which names the prefixed name to use. An import with no `name` override saves the prefixed name the draft derives (for example `showcase_customers` instead of `customers`). `GET /api/v1/meta/datasource` lists the three provenance keys on a code-defined datasource; all three are declared on `DatasourceSchema`. The datasource admin list (`GET /api/v1/datasources`) is unchanged, and the admin door still refuses to edit or remove a code-defined datasource. + - **Unchanged.** A datasource that carries no `_packageId` (the host `default` is one) and a package that declares no namespace resolve no namespace, so their imports and drafts answer as before. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [f97660c] +- Updated dependencies [13a24ec] +- Updated dependencies [fd5a1cd] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [440cd32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [5d095a0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [da40a5f] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/driver-memory@17.7.0 + - @objectstack/driver-mongodb@17.7.0 + - @objectstack/driver-sql@17.7.0 + - @objectstack/driver-turso@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/driver-sqlite-wasm@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index f4abdc45e71..ff07752cb9b 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "17.6.0", + "version": "17.7.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 a00a478ed02..d4c57f2d715 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,125 @@ # @objectstack/service-i18n +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index 0b5632babc9..6e60680c01d 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "17.6.0", + "version": "17.7.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 dcd0cadf18c..078e408e492 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,127 @@ # @objectstack/service-job +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 0104eeced9d..bd280aea479 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "17.6.0", + "version": "17.7.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 e7dec202cd8..b26f06d15e6 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,150 @@ # @objectstack/service-knowledge +## 17.7.0 + +### Minor Changes + +- 568dc0b: Record-change payloads apply the same credential mask and internal-field omission as write responses. + + Clause-②: yes (widening) + + - **`data.record.created` / `data.record.updated` events.** The engine projects the event's `after` and `changes` bodies through `omitInternalFieldsFromWriteResponse` (`@objectstack/core`), the helper every external write response already uses: credential-class fields (`secret`, and `password` outside the exempt `managedBy` buckets) carry `SECRET_MASK` (or `null` when unset), and `internal: true` fields are omitted. The engine's own write result is unchanged, so a privileged in-process caller that reads the stored value back off `insert` / `update` still sees it. + - **Approval request snapshot.** The record snapshot an approval request stores (`payload_json`) applies the same rule when the request is opened. + - **Outbound webhook body.** The delivered body, and the delivery row that stores it, apply the same rule to `before`, `after` and `changes`. + - **Knowledge index documents.** `recordToDocument` takes the object definition as an optional fourth argument and skips credential-class and `internal` fields, under `'*'` and when a source names one explicitly. `KnowledgeService` passes the definition from the bound engine. + - **New public surface of `@objectstack/service-knowledge` (additive):** `recordToDocument` accepts the object definition as an optional fourth argument; existing three-argument calls behave as before. + - **Receivers see masked values.** Webhook receivers and realtime clients now get `SECRET_MASK` (or `null` when unset) for credential-class fields and no key for `internal` fields. + - **Existing rows are not rewritten.** Approval snapshots, webhook delivery rows and knowledge documents written before this change keep their stored bodies; reindexing a knowledge source refreshes its documents. + - The audit trail already masked these fields and is unchanged. No other accept set or public schema changes. + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index c5d3c589b08..c86802c013c 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Knowledge Service for ObjectStack — orchestrator implementing IKnowledgeService over pluggable IKnowledgeAdapter backends (RAGFlow, LlamaIndex, Dify, in-memory).", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index c24a2a8ae03..07a403f9d45 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,144 @@ # @objectstack/service-messaging +## 17.7.0 + +### Patch Changes + +- 76fec88: Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off. + + Clause-②: yes (widening) + + - **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes. + - **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too. + - **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`. + - **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in. + - **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt. + - **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged. + - What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects. + - ⛔ No new export on any package entry, and no new elevation API. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index fe72386cef2..78297685516 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "17.6.0", + "version": "17.7.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 11c5b816979..ad56f341a12 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,136 @@ # @objectstack/service-package +## 17.7.0 + +### Patch Changes + +- 0e10be6: fix: on MySQL, `sys_packages` is now created and written, so installed and edited packages survive a restart. When a `sys_packages` write fails, a package install or edit now answers the failure instead of success (#21243) + + Clause-②: no + + **`@objectstack/service-package`.** The `sys_packages` DDL and the publish upsert are spelled for the dialect the default driver names (`SqlDriver.dialectName`). SQLite and PostgreSQL keep the exact statements they always ran, and so does any driver that names no SQL dialect. MySQL gets the same `(id, version)` key and columns in its own spelling. Its index is created only after `information_schema` reports it absent, and its upsert is `INSERT … AS incoming ON DUPLICATE KEY UPDATE`, which needs MySQL 8.0.19 or later. Before this, the table was never created on MySQL. That DDL failed with `ER_INVALID_DEFAULT`, `ER_BLOB_KEY_WITHOUT_LENGTH` and `ER_PARSE_ERROR`. The DDL refusal was logged only at `debug`, as "may already exist". The `ON CONFLICT` upsert also failed with `ER_PARSE_ERROR`, so `POST /api/v1/packages/publish` answered `500 DATABASE_ERROR`. A refused DDL statement now fails the plugin's `start()` and is logged at `error`. + + **`@objectstack/metadata-protocol`.** `installPackage` and `updatePackage` no longer answer success when the `package` service's `sys_packages` write fails. The registry write is undone first. A fresh install leaves no package and releases the namespace it registered. A re-install puts the prior row back, and an edit puts the prior manifest back. Then the failure is thrown. A store fault answers `500`, with `DATABASE_ERROR` from a live SQL driver and `INTERNAL_ERROR` otherwise. A declared 4xx refusal is passed through unchanged. Before this, `POST /api/v1/packages` answered `201` and `PATCH /api/v1/packages/:id` answered `200` over a write that never landed, and the package was gone after the next restart. A host with no `package` service still installs in memory only and says so with a warning. That degraded path is unchanged. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 92496297b53..55a21150c43 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index 891d5985db0..b7ade7b2233 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,142 @@ # @objectstack/service-queue +## 17.7.0 + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index 8c8ea68eef7..ba925bba3ed 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "17.6.0", + "version": "17.7.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 611119d3e6d..3c2d77eeeb5 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,127 @@ # @objectstack/service-realtime +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 89c3b116507..1fcbeee339d 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "17.6.0", + "version": "17.7.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 c06367a8c1e..41a4657bfdd 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,300 @@ # @objectstack/service-settings +## 17.7.0 + +### Minor Changes + +- 222ecc2: feat(spec): `ICryptoProvider` gains a required `keyedDigest(plain): Promise` member, and `LocalCryptoProvider` implements it (#21263) + + Clause-②: yes + + **BREAKING** for `ICryptoProvider` implementers: the new member is required, so a + provider that does not declare it stops compiling (`TS2420` on a class, `TS2741` + on an object literal), and the compiler names the missing member. Code that only + calls a provider is unaffected. + + `keyedDigest` is a digest of `plain` under the provider's server-held key, for a + value that is handed to a caller but must not let that caller check a guess about + the input offline. The contract requires three things of every implementation: + + - **Keyed.** The output cannot be computed without the provider's key. A provider + that holds no key material rejects; it never returns an unkeyed value. + - **Stable per key.** Under one key, equal input gives equal output in every + process and on every node that holds the key. Replacing the key changes every + output. + - **Not a substitute for `digest`.** `digest` keeps its contract and the stability + the audit trail relies on. + + The output is `hmac-sha256:` followed by the 64 lowercase hex characters of an + HMAC-SHA-256: 76 characters from `[0-9a-z:-]`, which travel unchanged in an HTTP + header, a query string and JSON, and never collide with the `sha256:` spelling of + an unkeyed content hash. + + `LocalCryptoProvider` computes it from the 32-byte data key it already resolves + (`OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY`, the persisted key file, or the ephemeral + test-mode key), through a MAC key derived from that data key, so the AES-GCM key + is never used as a MAC key. There is no new secret or environment variable to + configure. An instance constructed with an explicit key that is not 32 bytes holds + no usable key material, and its `keyedDigest` rejects with + `KeyedDigestKeyUnavailableError`. + + +- 57cc695: feat(spec): `CryptoContext` gains a required `scope` discriminant, and `LocalCryptoProvider` binds it into a delimiter-safe, versioned AAD (ADR-0128 D1–D3, #21326 stage 1) + + Clause-②: yes + + **BREAKING** for `ICryptoProvider` implementers and for every direct caller of + `encrypt`, `decrypt` or `rotateKey`: `CryptoContext.scope` is required, so a + context literal without it stops compiling (`TS2741`), and the compiler names the + missing member. `LocalCryptoProvider` also refuses such a context at runtime with + `CryptoContextScopeError`, for a caller the compiler never saw. Code that only + injects a provider is unaffected. + + `scope` is a member of the new closed set `CRYPTO_CONTEXT_SCOPES` (type + `CryptoContextScope`), one member per producer of `CryptoContext`: + `settings` (`SettingsService`), `object_secret_field` (the ObjectQL engine's + secret-field path) and `datasource_credential` (the datasource secret binder). + Each producer in this release passes its own member on every call. A new producer + adds its own member; it never borrows an existing one. + + What the contract now requires of every provider that binds AAD: + + - **Producer-discriminated (D1).** The AAD binds `(scope, namespace, key)`, so a + ciphertext sealed by one producer does not authenticate under another + producer's context, whatever the two `(namespace, key)` pairs are. + - **Delimiter-safe (D2).** Distinct triples produce distinct AAD bytes. An + unescaped join is not permitted. + - **Versioned.** A ciphertext records which AAD derivation sealed it, and is + opened only with that derivation. An unknown derivation fails closed. No second + derivation or scope is ever tried after a failure (D3). + + `LocalCryptoProvider` seals every new value under derivation version 2: a lead + byte that never occurs in UTF-8, a versioned label, then the scope, namespace and + key, each prefixed with its 4-byte length. The ciphertext carries a `v2:` marker. + A ciphertext with no marker is version 1, the bare base64 every earlier release + sealed, and it still opens with the older `(namespace, key)` binding. Existing + secrets therefore keep working with no action, and carry the older binding until + they are re-wrapped. Re-wrapping existing ciphertext at rest is stage 2 of + #21326. `rotateKey` already re-seals a version-1 handle under version 2. Any other + marker is refused with `UnknownCiphertextVersionError`. + + Operational note: a secret set or rotated by this release carries the `v2:` + marker, and an earlier release cannot open it. A rollback past this release needs + those values to be set again. + + `@objectstack/objectql` and `@objectstack/service-datasource` pass their own + scope on every seal and open. Their public surface is unchanged. + + +- 0557c2f: feat(cli): `os secret rewrap` re-wraps version-1 `sys_secret` ciphertext under the current AAD derivation, each row under its holder's producer scope (ADR-0128 §4.2, #21326 stage 2) + + Clause-②: yes (widening) + + A ciphertext sealed before ADR-0128 D1–D3 carries the older binding over + `(namespace, key)` alone, and still opens in this release. `os secret rewrap` moves + the stored values to the current binding through `rotateKey`, the seam ADR-0128 §4 + names. It is an operator command: a dry run by default, `--apply` to write, and + nothing on any boot or upgrade path invokes it. It has no HTTP surface. + + - **The scope comes from the holder.** `sys_secret` records no producer, and a + version-1 ciphertext binds no scope, so each row is re-sealed under the scope of + the producer whose holder references it: `settings` for a `sys_setting.value_enc` + handle, `object_secret_field` for a `secret:` ref on a business row, + `datasource_credential` for a `sys_secret:` `credentialsRef`. The holders come from + the same cross-producer reference union `os secret orphans` reads. A row nothing + references, a row whose holders belong to different producers, and every row while + a holder family could not be read are left as they are and counted, never re-sealed + under a guessed scope. `--apply` refuses an incomplete union and names the family. + - **Resumable.** A row already sealed under the current derivation is skipped as + done, so a stopped run finishes the rest when re-run and a finished run writes + nothing. + - **Safe against a live deployment.** Each row is written by one conditional update, + keyed on its id and the ciphertext the run read. A row a producer changed in + between is not overwritten, and a re-run picks it up. A driver with no + `updateMany` is refused before any row is opened. + - **Fails closed.** A row that does not open, or whose re-seal does not open to the + same plaintext under the same scope, is not written. The run finishes the rest and + exits 1. The check happens before the write. + - **Output is classes and counts only.** It never prints a plaintext, a ciphertext + or a row id. + + The command resolves its data key from `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the + persisted key file, in the strict posture: it never mints a key, and it hands the + settings service it boots the same provider so that service does not mint one + either. With no key it refuses before opening any row. + + `@objectstack/service-settings` publishes `ciphertextDerivationStatus` (and its + `CiphertextDerivationStatus` type). It is `LocalCryptoProvider`'s own reading of + which derivation sealed a stored ciphertext, read off its marker without opening it: + `current`, `superseded` or `unknown`. The re-wrap classifies rows with it rather than + restating the marker grammar. +- 76fec88: Platform plumbing in these four packages now passes the explicit system opt-in (`{ isSystem: true }`) on its data-engine calls. Until now it reached the engine with no principal and no opt-in, and the security middleware let that through only because of its principal-less hand-off. + + Clause-②: yes (widening) + + - **Why `yes (widening)`:** two exported option types gain an optional `context` that an adapter must forward as-is. They are `SettingsEngine.find` / `.insert` (`@objectstack/service-settings`) and `SecretStoreEngineLike.delete` (`@objectstack/service-datasource`), so both packages take a `minor`. An implementation written against the old types still type-checks, and nothing accepted or refused at any door changes. + - **service-settings:** `SettingsService` reads and writes its own `sys_setting` rows under the opt-in: `loadRows`, plus the existence probe and insert in `upsertRow` (the update already used it). The `sys_setting_audit` writer does too. + - **service-datasource:** the `sys_metadata` helpers behind runtime datasources use the opt-in. They cover boot restore, cluster convergence, and persist and delete behind the admin doors. So do the `sys_secret` binder's `bind`, `unbind` and `resolve`. + - **plugin-webhooks:** the auto-enqueuer's subscription refresh and the redeliver guard's subscription lookup use the opt-in. + - **service-messaging:** two paths use the opt-in. One is the dispatcher's claim path: `claim`, `claimDigest` and the visibility-timeout reap on both outboxes. The other is the emit fan-out: the `sys_notification` row, the recipient's address and locale reads, the preference reads, the inbox row and the delivered receipt. + - **A user reference that names no user is still refused.** The engine skips its dangling-reference check for an `isSystem` write, so each producer that writes a user reference checks it first. The checked references are the `actor_id` of `sys_notification`, `sys_inbox_message` and `sys_setting_audit`, and the `user_id` of a user-scope `sys_setting` row. An unknown id is refused with the engine's own answer: `VALIDATION_FAILED`, one `reference_not_found` finding, and the same message. A write that names no user is unchanged. + - What each call reads and writes is otherwise unchanged. None of the gates the middleware runs before its hand-off applies to these objects. + - ⛔ No new export on any package entry, and no new elevation API. +- 0d8ea5e: fix(service-settings)!: the Localization settings no longer offer `date_format`, `time_format`, `number_format` or `first_day_of_week`: dates, times, numbers and the week start follow the locale (#21958) + + Clause-②: no (narrowing) + + + + **BREAKING**: the Localization settings namespace (`localizationSettingsManifest`) drops four specifiers that nothing ever read: `date_format`, `time_format`, `number_format` and `first_day_of_week`, together with the Formats group they made up and their copy in the `en`, `zh-CN`, `ja-JP` and `es-ES` settings bundles. Setup → Localization no longer shows them, and `GET /api/settings/localization` no longer serves them. The maintainer ruled that dates, times, numbers and the first day of the week follow the user's locale (language and region), as Salesforce derives them from a Locale, and that the four separate settings retire rather than being implemented. `timezone`, `locale`, `default_country`, `currency` and `fiscal_year_start` are unchanged. The manifest's `version` is now 2, as the `SettingsManifest.version` contract asks when keys are removed. + + **A value a workspace already stored for one of the four is kept.** Measured at the REST surface: + + - The `sys_setting` row stays exactly as it was. No read, save or reset rewrites or deletes it. + - `GET /api/settings/localization` does not resolve it: the key is absent from both `manifest.specifiers` and `values`. + - A `PUT /api/settings/localization` that names one of the four is refused with `400 UNKNOWN_KEY` (`details.key` names it), the answer every undeclared key gets. The refusal covers the whole batch, so a live key sent beside it does not land either. + - A stored row never blocks saving the live keys, and the built-in `reset` action leaves it in place. + - In process, `settings.get('localization', 'date_format')` (or any of the four) now rejects with `SETTINGS_UNKNOWN_KEY`. + - An `OS_LOCALIZATION_DATE_FORMAT`, `OS_LOCALIZATION_TIME_FORMAT`, `OS_LOCALIZATION_NUMBER_FORMAT` or `OS_LOCALIZATION_FIRST_DAY_OF_WEEK` variable is no longer read. It set a value that nothing read before either. + + **What to do after upgrading.** Nothing has to be rewritten: the four keys never changed how anything rendered. There is no replacement key. `locale`, the workspace's language and region, is what decides how dates, times and numbers are written. The console's calendar and timeline do not yet take their first day of the week from it; that is objectui work, tracked and shipped separately. + + It ships as `minor` under the launch-window convention for narrowings. + +### Patch Changes + +- ba57588: The settings audit trail records a secret-valued setting (an encrypted key) with the crypto provider's keyed digest, never an unkeyed one (#21792). + + Clause-②: no + + - **Both ledgers.** The `sys_audit_log` `config_change` row (`valueDigest`, spelled ``) and the `sys_setting_audit` row (`new_hash`) now carry `ICryptoProvider.keyedDigest(value)` for a secret-valued setting. Before, they carried the unkeyed `digest` (`sha256:…`). This covers the `sys_secret` path and the legacy inline-adapter path. + - **Change detection still works.** The keyed digest is stable for equal values under one key, so the trail still shows whether a secret changed and whether it went back to an earlier value. Rotating the data key changes every later fingerprint. Rows written before this release keep their old `sha256:` value. + - **No keyed digest, no fingerprint.** When no crypto provider is wired (a host that builds `SettingsService` with only a `CryptoAdapter`), or the provider refuses a keyed digest, the audit rows record the write with no value fingerprint: `valueDigest` is `` and `new_hash` is null. The service logs this once per key at `warn`. The settings write itself is never refused for it. The adapter's own `digest` is no longer used for secrets. + - **Non-secret settings are unchanged.** They keep the adapter's `digest` of the canonical JSON. + - **Contract text (`@objectstack/spec`).** The `ICryptoProvider` docs for `digest` and `keyedDigest` now state the rule: a secret's audit fingerprint comes from `keyedDigest`, never from `digest`, and with no keyed digest the trail records none. No type, export or schema changes. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index aa4f8f4eb88..daf7abcc816 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "17.6.0", + "version": "17.7.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 a3d4a345f29..ecd29b40f57 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,143 @@ # @objectstack/service-sms +## 17.7.0 + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [1c3a4d9] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [41a1135] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/plugin-auth@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index 35c4b46b346..76c360fd2d4 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "main": "dist/index.js", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index a997eea5c1b..a1fa78acdfd 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,185 @@ # @objectstack/service-storage +## 17.7.0 + +### Minor Changes + +- 3eb38ae: A user who can edit a record may delete another user's attachment on it, as the attachment gate declares (#21729). + + Clause-②: yes (widening) + + - **What was refused.** The attachment gate's delete rule is "the uploader OR a user who can edit the parent record". For every member holding `org_member`, the platform's row-level delete floor in `member_default` (`owner_only_deletes`: only the rows you created) answered first, so a parent editor's delete of someone else's attachment was refused with `PERMISSION_DENIED` before the gate ran. + - **`@objectstack/service-storage`** contributes a delete-only alternate match for `sys_attachment` (`sys_attachment_parent_editor_delete`, every row) when it installs the attachment gate, and only then. The gate decides: a parent editor's delete answers 200, and a caller who can read the attachment but neither uploaded it nor can edit the parent is refused with `ATTACHMENT_DELETE_DENIED`. A caller who cannot read the parent cannot see the attachment, and is still refused with `PERMISSION_DENIED` before the gate runs, so the parent is not named to them. + - **Without `@objectstack/service-storage`** nothing is contributed. A deployment that registers `sys_attachment` without the storage service keeps the floor, and only a row's creator may delete it. + - **The edit limb is unchanged.** Editing another user's attachment row is still refused by the floor for a member it binds, a parent editor included. + - **`@objectstack/plugin-security`** gains the seam: `contributeOwnershipFloorAlternates(plugin, alternates)` on the registered `security` service, an extension of `ISecurityService` that callers feature-detect. Each alternate names one object (never `'*'`), one floor limb (`update` or `delete`; `all` is refused) and a `using` predicate. It lands beside each enabled floor policy of that limb, in that policy's own `positions` domain, so it reaches only the principals the floor binds. A plugin's second call replaces its first, and an empty list withdraws it. A contribution that breaks these rules throws. + + Nothing that was admitted before is refused now. No principal outside the floor's domain, and no other object or operation, changes. + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- 417443e: `os migrate value-shapes` and `os migrate files-to-references` record the deployment-level ADR-0104 flag only from a run over every object, and every command in the `os migrate` data-migration family refuses an `--object` name the deployment does not declare (#21644). + + Clause-②: no + + - **A narrowed `--apply` records no deployment flag.** The flag attests the stored data of every object and turns strict enforcement on, but a run narrowed by `--object` reads only the named objects. Such a run still applies its fixes: `files-to-references` converts the named objects' values. It records no flag, whether it passes or fails, and leaves a flag that an earlier full-scope run recorded exactly as it was. Its output says why and names the run that records the flag: the same command without `--object`. The `--json` document carries `filter: { objects }`, which is `null` on a full-scope run, so a narrowed run is never mistaken for a full one. Any `--object` narrows, even a list that names every object. A full-scope `--apply` records the flag as before. + - **`runFilesToReferencesMigration`** (`@objectstack/service-storage`) skips the flag write when it is given `objects`. That includes `[]`, which walks nothing. Its `flag` result is `null` on a narrowed run. + - **The column step of `files-to-references` does not run on a narrowed run.** It retypes every single-value media column in the database on the authority of the gate, and a narrowed gate vouches only for the named objects. Before this change, a narrowed `--apply` or a misspelled one moved those columns and stamped `columns_moved_at`. + - **An unknown `--object` is an error.** This applies to `value-shapes`, `files-to-references`, `summary-nulls` and `duplicates`. A name the booted registry does not declare exits 1 with `OBJECT_NOT_FOUND`, and the error names that name and the declared objects. The check runs before anything is read or written. Until now, such a name was filtered out of the scan without a word, so a typo scanned nothing and read as a clean run. `duplicates` reports the refusal as `{ error: 'report_failed', detail, code }`. A declared object that the command has nothing to check on is still accepted. +- 33f9791: A file field's declared `accept` / `maxSize` refusal now answers `400 ERR_FILE_CONSTRAINT` with a sentence naming the field and the constraint, instead of `500 INTERNAL_ERROR` with the sentence withheld. + + Clause-②: no + + - **`FileConstraintError` declares `status = 400`**, as `FileFieldBulkWriteError` in the same module already did. The data API's declared-status passthrough now answers the refusal on create and on update, e.g. `400 {"error":"File exceeds the maximum size declared for 'doc' (5005 bytes > 10 bytes)","code":"ERR_FILE_CONSTRAINT","object":"…"}`. Before, the error declared a registered `code` but no status, so it fell through to the sanitised `500 INTERNAL_ERROR`, and the field and the reason reached only the server log. + - **It carries `field` and `constraint` (`'accept' | 'maxSize'`) as members**, for in-process callers. The constructor is now `new FileConstraintError(field, constraint, message)`. The new `FileConstraint` type is exported beside it. On the HTTP wire the message names both, and its wording is unchanged. + - The accept set is unchanged: the same files are refused, and a refused write still persists no row and claims no file. `ERR_FILE_CONSTRAINT` was already in the error-code ledger. +- 50b5e03: A write refusal on an attachment or a comment no longer names a parent record the caller cannot read (#21755). + + Clause-②: no + + - **What changed.** The attachment gate (`sys_attachment`, `@objectstack/service-storage`) and the comment gate (`sys_comment`, `@objectstack/plugin-audit`) refuse an update or a delete by a caller who neither wrote the row nor can edit its parent record. That refusal names the parent record. A caller who cannot read the parent now gets the platform's not-visible refusal instead. This is the answer the row-level write check gives the principals it covers: `PERMISSION_DENIED` (403), with the same localized `record_access_denied` sentence. It names neither the parent nor the row's link to it, in the message or in the envelope. + - **What did not change.** A caller who can read the parent but may not edit it keeps the named refusal: `ATTACHMENT_DELETE_DENIED` for an attachment delete, and `RECORD_NOT_ACCESSIBLE` for an attachment update and for a comment update or delete. Who may update or delete is unchanged. + - **A comment whose thread names no record** is read by nobody, so a non-author's write on it now gets the not-visible refusal too, and the thread value is not echoed back. + - **Localization.** `installAttachmentAccessHooks` and `installCommentAccessHooks` accept an optional fourth argument: a lazily resolved i18n lookup. With it, the sentence honours a deployment's `errors.record_access_denied` override, as the row-level write check's sentence does. Without it, the built-in catalog still renders the caller's locale. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/observability@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index 56e0265dc22..66dfc997f46 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index c5690980783..ef61443c743 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,2323 @@ # @objectstack/spec +## 17.7.0 + +### Minor Changes + +- ecb6ca0: A flow screen field's help text is translatable: the `flows` translation face carries `inlineHelpText` beside `label` and `placeholder` (#17306). + + Clause-②: yes (widening) + + - **`TranslationDataSchema`.** `flows..screens..fields.` accepts `inlineHelpText`, the key the screen field itself declares (`ScreenFieldConfig.inlineHelpText`, the object field's spelling). The console's screen dialog draws that text under the control, so a translated help line now renders in the active locale. + - **`FLOW_SCREEN_FIELD_COPY_KEYS`** (`@objectstack/spec/system`) is `['label', 'placeholder', 'inlineHelpText']`. Its readers follow it without an edit: `translateFlow` overlays the key, `os i18n extract` scaffolds it, and objectui's `FlowRunner` overlays it on the field it draws. `FlowScreenFieldLike` gains the optional `inlineHelpText` member. + - **Refusals.** `help`, `helpText`, `hint`, `tooltip` and `description` on a screen field translation are still refused, and the message now names the rename to `inlineHelpText`. They used to be told that the face had no help key. `options` is still refused with its guidance. + + Nothing that parsed before is refused now. A bundle that never wrote a help line is unchanged. +- 22c2d6f: feat(spec)!: an agent's `memory` contract states exactly what the runtime honours — `maxEntries` and `reflectionInterval` are required once long-term memory is enabled, `longTerm.store` is retired, and the block is `live`, enforced by the cloud AI runtime (#20274) + + **BREAKING** — `agent.memory` narrows to what the cloud AI runtime, the one runtime + that executes agents, actually does with it. That runtime recalls the newest + `maxEntries` distilled notes for the user before the first round, writes one note + every `reflectionInterval` delivered interactions, evicts notes beyond `maxEntries`, + and keeps them in its own database store. Before an agent's first turn it refused + exactly the declarations this spec still accepted, so authoring now refuses them, + by name, with a prescription (ADR-0049 enforce-or-remove): + + - **`longTerm.maxEntries` and `reflectionInterval` are required when + `longTerm.enabled` is true.** No default is declared for either: none has a + measured basis, and the runtime adds none. + - **`reflectionInterval` is refused without an enabled `longTerm`** — a reflection + writes a long-term note, so with none enabled it would do nothing. + - **`longTerm.store` is retired as a whole key.** The memory store is platform + infrastructure, not agent metadata: the runtime keeps the notes in its own + database store, and refused `vector` (the key's default, so what an omitted + `store` parsed to) and `redis`. Its old spellings `backend`, `storage` and + `provider` under `longTerm` are answered with the same prescription instead of + being steered onto `store`. + + `longTerm.enabled` is unchanged. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `memory.longTerm.store` — any value, `database` included | delete the key; where the notes are kept is the platform's choice. | + | `longTerm: { enabled: true, … }` without `maxEntries` | add `maxEntries`: how many distilled notes are kept for each user (an integer of at least 1). | + | `longTerm: { enabled: true, … }` without `memory.reflectionInterval` | add `reflectionInterval`: how many delivered interactions pass between the reflections that write a note (an integer of at least 1). | + | `memory.reflectionInterval` without `longTerm.enabled: true` | enable long-term memory with both numbers, or delete `reflectionInterval`. | + + **The one-line fix: declare `maxEntries` and `reflectionInterval` when `longTerm.enabled`; delete `store`.** + `os migrate meta --from 17` lists the mechanical edits for existing sources (the + `store` deletion); the two numbers are the author's to choose. + + Each refusal is a parse error at the key's own path, naming the key and the fix, and + `store` also fails `tsc` (its input type is `never`). + + ### The retirement kit + + - **Tombstone.** `longTerm.store` is a `retiredKey()` carrying the prescription; the + three old alias spellings moved from `aliases` to `guidance`, because an alias may + not steer an author onto a tombstone. + - **The contract check** is a refinement on `memory` (`reflectionInterval` is + `longTerm`'s sibling), one `custom` issue per missing or misplaced key. A JSON + Schema cannot state a value-conditioned requirement in the closed projection list, + so the published `ai/Agent` schema (and the four installed-package schemas that + embed agents) names the site in `x-dropped-refinements`, recorded in + `dropped-refinements.baseline.json`. + - **D2 conversion `agent-memory-long-term-store-removed`** (step 18, retired from the + load path): it deletes `store` from `memory.longTerm`, whatever it holds — the + delete is lossless, because no value of it ever chose a backend. Stored + `sys_metadata` agent rows and built artifacts replay it; one notice per agent. It + supplies neither number. + - **D3 entry `agent-memory-store-retired-and-limits-required`** carries the judgement + the conversion cannot make: the two numbers an enabled `longTerm` now requires. + - **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:memory.longTerm.store`. + - **No deprecation window**, per the project's startup-stage posture. + + ### Describes and the liveness ledger + + - `agent.memory` drops `[EXPERIMENTAL — not enforced]`: it states that the cloud AI + runtime enforces it and that the open framework edition does not run agents. + `longTerm`, `enabled`, `maxEntries` and `reflectionInterval` each state what the + runtime does with them. + - The ledger row moves `experimental` → `live`, citing the cloud reader + `agent-runtime.ts#compileAgentMemory` (via `AgentRuntime.resolveTurnGuardrails`), + the enforcement in `ai-service.ts` and the store `agent-memory.ts#AgentMemoryStore`, + as attested by the cloud seat's reading at cloud `ef5a4344`, `verifiedAt` + 2026-10-02. `os lint` / `os validate` no longer warn + `liveness-experimental-property` on an agent that sets `memory`. + - ⚠️ **The window, stated.** At `ef5a4344` the cloud reader still reads `store`: it + honours `database` only and refuses `vector` and `redis`. Cloud drops `store` in + that one reader once this release reaches its pin, and no earlier. + + ### The agent form's help texts + + - The `memory` row's help text on the agent metadata form named short-term memory, + a key the schema refuses. It now states what memory does and that `maxEntries` + and `reflectionInterval` are required once long-term memory is enabled. + - The neighbouring `planning` row named a strategy and a replan switch the schema + does not declare; it now states the one key it has, the iteration cap. + - The `platform-objects` metadata-form catalogs follow: the English leaves are + regenerated, and the `zh-CN`, `ja-JP` and `es-ES` leaves are authored, not copied. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published, and tenant-authored agents were not measured. This repo authors no + `longTerm` outside `packages/spec`, and no cloud built-in agent declares one. + + Clause-②: yes (narrowing) + + +- 909229e: A job pulls a mapping's connector source by declaration — `pull: { mapping }` — and every job runs as the `organization` it declares (#20281). + + Clause-②: yes (widening) + + - **`JobSchema.pull`** (`@objectstack/spec/system`). A third run form beside `body` and `handler`: `{ mapping: '' }`. On each run the platform pulls that mapping's `connectorSource` and writes the rows through the import runner. It carries no code. The key is refused beside `body` or `handler`, because one of the two run forms would never run. `body` with `handler` stays legal, and the body still wins. A job must now declare one of `body`, `handler` or `pull`. `pull` is closed: an unknown key inside it is refused. + - **`JobSchema.organization`**. The organization a job runs as. It applies to the body's `ctx.api`, to the handler's new `executionContext`, and to the pull's reads and writes. The value shape is the scheduled flow's: a non-empty `sys_organization.id`. A near-miss spelling (`organizationId`, `orgId`, `tenantId`, …) is refused at parse and pointed at the key. + - **`defineStack`, and so `os validate`**, refuses a job whose `pull` names a mapping the stack does not declare, or a mapping with no `connectorSource`. The refusal is the existing `STACK_CROSS_REFERENCE_INVALID` envelope. + - **`IAutomationService.pullConnectorSource`** (`@objectstack/spec/contracts`, with `ConnectorSourcePullRequest`, `ConnectorSourcePullResult` and `ConnectorSourcePullSummary`). The connector sync executor is now on the `automation` service. `@objectstack/service-automation`'s engine serves it from the executor `AutomationServicePlugin` attaches at init (`AutomationEngine.setConnectorPullSource`). A bare engine refuses with `SERVICE_UNAVAILABLE` (503). + - **The job binder** (`@objectstack/runtime`, `scheduleAppArtifactJobs`) schedules a `pull` job on every door: the boot, and `os package install` on install and rehydrate. Each run calls `pullConnectorSource` through the service registry. A refused pull fails the run, and `retryPolicy` applies. A pull whose rows the import runner refused records the run `degraded`, with the counts. A pull naming a mapping the artifact does not carry is not scheduled, and neither is one whose mapping has no `connectorSource`, nor one on a kernel whose `automation` service cannot pull. Each case is logged at `warn` with the reason. `collectJobsWithoutBody` does not name a `pull` job that binds, so `os package install` installs one. The result gains `pulls` and `missingOrganization`. + - **The organization, judged at bind** by the posture rule scheduled flows use (`resolveScheduledWorkPolicy`). Every run carries `{ isSystem: true, tenantId: }`, or `{ isSystem: true }` for a job that declares none. Under `single` the key is not required. Under `group` it is optional; an undeclared job is scheduled and named once at `warn`, because a tenant-scoped row it writes is refused. Under `isolated`, with package-authored scheduled work switched on, it is **required**. **Action on such a deployment:** declare `organization` on each packaged job, or the job is not scheduled; the error log names the job. Until now such a job was scheduled, and every tenant-scoped write it made was refused at the write. An unrecognized `OS_TENANCY_POSTURE` withholds every job (`scheduled-work-policy-unreadable`) instead of guessing whether a declaration is required. + - **Texts this makes true.** The `mapping.connectorSource` description, the `connector.syncConfig` tombstone prescription and the `connector-sync-keys-retired` upgrade entry said "nothing schedules a pull yet". They now name the `job` `pull` that drives it. + + Nothing that parsed before is refused now. Every new refusal falls on a key that did not exist before this change. +- 96a9719: feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790) + + Clause-②: yes (widening) + + A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it. + + **⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '' (): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification. + + What else changes: + + - **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`. + - **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential. + - **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials. + - **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing. + - **Refused now, loudly**: + - With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again. + - The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret. + + +- c52c49d: fix(spec)!: `FieldSchema` refuses a `select` / `radio` field with neither `options` nor `picklist` (#20827) + + Clause-②: yes + + + + **BREAKING** accept-set narrowing on `FieldSchema`, shipped as `minor` under the + repo's launch-window convention for breaking changes — the grade the `reference` + precedent shipped with (a `lookup` / `master_detail` without `reference`, refused + at parse as a `minor` with the **BREAKING** header). + + **What was accepted before.** A `select` or `radio` field with no `options` key, + with `options: []`, and with no `picklist` parsed cleanly. It is a choice with + nothing to choose: the form control offers nothing, and server-side value + validation is off (the record validator checks membership only against a + non-empty allowed list), so any value writes through the API. The author-time + completeness gate (ADR-0078, `field/choice-without-options`, used by `os build`, + `os validate` and `os lint`) already graded it an error, and registration warns on + it; a runtime-API or Studio save was the one door that let it through. + + **What is refused now.** At parse, on the `options` path, with a `custom` issue + that names the field type and both remedies: a `select` / `radio` whose `options` + is absent or empty and whose `picklist` is absent. The predicate is the + completeness gate's own, so the two cannot disagree. + + **The fix.** Declare `options: [{ label, value }]` with at least one entry, or + `picklist: 'industry'` (the name of any shared list) to offer a shared list — + never both (that pair stays refused as before). If any value is meant to be allowed, use a `text` field instead. + + **Unchanged.** `multiselect` and `tags` keep parsing without options (free-form + by design), and `checkboxes` keeps parsing with a completeness warning. A + `select` / `radio` with at least one option, or with a `picklist`, parses as + before. The ADR-0078 author-time rule and the registration warning are + unchanged — this door is one more gate, not a replacement. `Field.select()` + called with an empty list emits `options: []`, which is now refused at parse. + + **Stored rows.** No conversion can supply the missing options, so a row saved + before this release is not rewritten. It is still served — with + `_diagnostics.valid: false` naming `fields.FIELD.options` — and still + registered at boot (counted invalid); a later save of its object is refused + until an option or a `picklist` is added. To find such rows, read + `GET /api/v1/meta/diagnostics`, or the boot log's `field/choice-without-options` + lines. The `os migrate meta --stored` preview does not validate bodies, so it + does not find them: it counts such a row canonical, or — when the row also + carries an older spelling to lower — pending, and the apply then reports that + row failed and leaves its bytes as they were. +- 99589f9: feat(spec)!: retire the cube metric types `number`, `string` and `boolean` — a measure's `sql` is a column reference, so the custom-SQL-expression types had nothing left to compute (#21000) + + **BREAKING** — three members leave `AggregationMetricType`, so a cube measure's + `measures..type` no longer accepts `number`, `string` or `boolean`. ADR-0049 + enforce-or-remove. They declared "a custom SQL expression returning a number / + string / boolean": the measure's `sql` was the whole computation. A cube member's + `sql` is a column reference since `cube-member-sql-expression-retired` (#20943), so + the three were left naming nothing: measured before this change, the raw-SQL + analytics path returned the referenced column UNAGGREGATED (a bare column in a + grouped statement — by SQL's own rules an error on PostgreSQL and an arbitrary row's + value on SQLite), and the ObjectQL path refused the measure. The six aggregates — `count`, `sum`, + `avg`, `min`, `max`, `count_distinct` — are unchanged and are now the whole + vocabulary. + + ### FROM → TO + + | removed | what to write instead | + | --- | --- | + | `measures..type: 'number'`, `'string'` or `'boolean'` | the aggregate the measure means: `sum`, `avg`, `min` or `max` over the column; `count` (over `'*'` for a row count, or over a column for its non-null values); or `count_distinct`. | + | a measure whose old expression computed a value per row | keep that value as a field of the object (a stored or formula field) and aggregate the field. | + | a measure whose old expression combined measures (a ratio, a difference) | `derived: { op, of: [...] }` on an ADR-0021 dataset over the same object. | + + **The one-line fix: give the measure an aggregate type.** There is no mechanical + rewrite — the column alone does not say whether `amount` meant its sum, its average + or its largest value — so `os migrate meta` lists nothing for this change. + + Each retired member is refused at parse with a prescription naming the six + aggregates, at the measure's `type`, and in `tsc` (the members are gone from the + `AggregationMetricType` type). A value the enum never declared keeps zod's own + message. + + ### The retirement kit + + - **Value-level retirement.** `AggregationMetricType` is declared through + `enumWithRetiredValues` (`shared/retired-key.ts`), with the prescriptions + module-private. No authorable KEY and no def changed, so nothing lands in + `RETIRED_KEYS_BY_MAJOR`, and the four surface ratchets (`api-surface`, + `authorable-surface`, `json-schema.manifest`, `api-surface-signatures`) are + byte-identical. + - **No D2 conversion, by design.** A stored or built cube that still carries one of + the three is REFUSED, never rewritten or dropped: the boot door + (`ObjectStackDefinitionSchema`, which a built artifact is parsed through), the + `analytics_cube` write door and `defineStack` refuse it with the prescription, and + the rehydration seam replays no conversion over it. + - **D3 entry `cube-metric-expression-types-retired`**, with its step-18 rationale + fragment, carries the judgement the upgrader owes: which aggregate each measure + meant. + - **Liveness.** The `analytics_cube` row `measures.type` stays `live`, re-verified + 2026-10-02, with the narrowing recorded. + - **Docs.** The `data/analytics` reference page is regenerated. + - **No deprecation window**, per the project's startup-stage posture. + + ### Reach, measured + + - This repository authors no cube measure of the three types outside tests: + `examples/**`, `packages/**` (the platform objects included) and the skills and + docs carry none. The showcase cube's `type: 'string'` entries are dimensions, + whose `DimensionType` is a separate enum and is unchanged. + - objectui at its pinned commit carries no `AggregationMetricType` mirror and no + cube measure of the three types. + - Out-of-repo authored cubes: NOT MEASURED. + + Clause-②: no (narrowing) + + +- 36ad321: feat(spec)!: `element:text` `variant` refuses `heading` / `subheading` by name — the vocabulary is the nine `ui:text` publishes, and `os migrate meta` rewrites them to `h2` / `h3` (#21015) + + **BREAKING** — `heading` and `subheading` leave `ElementTextPropsSchema.variant` (an + `element:text` page component's `properties.variant`). This is the second release of + the ruled two-release convergence on the nine values `ui:text` publishes — `h1`-`h6`, + `body`, `caption`, `overline`. 17.5.0 added the nine and refused nothing; 17.6.0 was + the full release in which both vocabularies parsed; this release refuses the two old + spellings. A heading is a document level, not a text style: `heading` and + `subheading` named a style and left the renderer to pick the level. + + ### FROM → TO + + | removed | what to write instead | + | --- | --- | + | `variant: 'heading'` | `variant: 'h2'` — the heading element `heading` always rendered — or the level the page outline means. | + | `variant: 'subheading'` | `variant: 'h3'` — the heading element `subheading` always rendered — or the level the page outline means. | + + **The one-line fix: `heading` → `h2`, `subheading` → `h3`.** + `os migrate meta --from 17` lists the mechanical edits for existing sources. + + The rewrite keeps the heading ELEMENT (so the document outline is unchanged) but not + the size: `heading` drew in the `h3` style and `subheading` in a medium-weight small + heading style, and `h2` / `h3` draw their own, larger styles. Where the old look + mattered more than the level, pick the level whose style you want. + + Each retired spelling is refused at parse with a prescription naming the level to + write, and in `tsc` (the two members are gone from the input type). Any other unknown + value keeps zod's own message. An `element:text` with no `variant` still parses to + `body`. + + ### The retirement kit + + - **Value-level retirement.** The enum is declared through `enumWithRetiredValues` + (`shared/retired-key.ts`), with the two prescriptions module-private. No authorable + KEY and no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four + surface ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`, + `api-surface-signatures`) are byte-identical; the generated component reference + page drops the two values. + - **D2 conversion `element-text-variant-heading-levels`** (step 18, retired from the + load path): `heading` → `h2` and `subheading` → `h3` on every `element:text` page + component — regions, named slots and container nesting. Stored `sys_metadata` page + rows replay it at rehydration; one notice per rewritten block. + - **D3 entry `element-text-variant-heading-subheading-retired`** carries the judgement + the conversion cannot make: whether the rewritten level is the one the page means. + - **No further deprecation window**: 17.6.0 was the window the ruling asked for. + + ### Producers moved in this repository + + - `@objectstack/platform-objects`: the four section headings on the `sys_user` record + page's Security tab (`Password & Sign-in`, `Two-Factor Authentication`, `Email + Verification`, `Danger Zone`) move from `subheading` to `h3`. They render the same + h3 element, in the `h3` style. + - `examples/app-showcase`: the `page-variables` detail heading moves to `h3`. + + ⚠️ **The out-of-repo author population is NOT MEASURED.** `@objectstack/spec` is + published, and tenant-authored pages were not measured. In this repository the five + writers above were the only ones outside `packages/spec`. objectui at `main` authors + neither value; its `element:text` renderer, registry `inputs` enum, html tier and the + published `sdui.manifest.json` still list the two, and drop them once this release is + installable there (the objectui follow-up). + + Clause-②: no (narrowing) + + +- dcc5ef4: `deriveInlineRowFormFields` and `isInlineRowFormOffered` (`@objectstack/spec/data`) state which fields an inline master-detail grid's per-row expand form draws and when that form is offered, and `field-no-consumers` stops calling four more kinds of in-use child field "inert" (#21091). + + Clause-②: yes (widening) + + - **`@objectstack/spec`.** Two new exports from `@objectstack/spec/data`, beside `deriveInlineGridColumns`: + - `deriveInlineRowFormFields(def, { relationshipField?, exclude? })` returns the child field names of the per-row expand form, in the child's field order. It skips the same system, audit, tenancy, ownership and sort-position names as the grid, the relationship field, `exclude`, `system` and `hidden` fields, and the computed types (`formula`, `summary`, `rollup`, `autonumber`, `auto_number`). Unlike the grid it keeps `readonly` fields and the rich types a cell cannot edit (`richtext`, `json`, `markdown`, …), so the derived grid's columns are always a subset of its fields. + - `isInlineRowFormOffered({ inlineMode?, formFields?, columns? })` is `true` when the form factor is `form`, or when the form has more fields than the grid has columns. + - Both are the renderer's current rule, reproduced exactly. No schema accepts anything new or refuses anything new. + - **`@objectstack/lint`.** `os validate` no longer warns that these fields are inert: + - a `lookup` field that sets `inlineEdit`: it is the inline grid's join key, read whatever columns the grid draws, as a `master_detail` field already was; + - a field a derived inline grid's per-row expand form draws, through `deriveInlineRowFormFields`, such as a `readonly`, `richtext` or `json` child field; + - a field named in an `object-master-detail-form` detail entry's `formFields`, now read against the entry's `childObject` instead of the block's object. When the form is never offered for the list, the list is reported as a carrier. That is judged on an entry that names both its `relationshipField` and its `columns` under its declared `inlineMode` or none. On any other entry it is judged under a declared `inlineMode` where the grid can be counted: authored `columns`, or the derived grid of a named `relationshipField`. Otherwise the list is credited as drawn; + - a field named in a `record:line_items` block's `columns`, `relationshipField`, `amountField`, `sort` or `filter`, now read against the block's `childObject`. + + A parent field that shares a name with one of those child fields was credited in the child's place, and is now reported if nothing else reads it. A child field nothing draws or names, such as a `hidden` one, is still reported. +- 5a9292e: feat(spec)!: an `object-grid` page block's `exportOptions` is the list view's export options object, and a bare format array is refused (#21229) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`ComponentPropsMap['object-grid'].exportOptions`** was `z.unknown()`, so any value passed. The console's `ObjectGrid` reads `exportOptions.formats`, `.maxRecords`, `.includeHeaders`, `.fileNamePrefix` and `.streaming`, and lifts nothing: a bare format array — legal on a list view, which lifts it to `{ formats }` at parse — showed the export menu with its csv/json default and dropped the author's list without a report. The row now takes the list view's own five-member export options object, by identity and not the list view's union, so the legacy spelling does not spread to the grid: + - a bare array is refused with the object form named (`{ formats: ['csv', 'xlsx'] }`); + - a format outside `csv` / `xlsx` / `json` is refused at its index, and `pdf` keeps its retirement text; + - a key the object does not declare is named, with the rename a near-miss gets (`maxRecord` → `maxRecords`); + - `null` and other non-object values are refused. + - **`ObjectGridProps['exportOptions']`** (and `ObjectGridPropsParsed`) is the object type `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }` instead of `unknown`. + - The list view's `exportOptions` accepts and lifts exactly what it did. One message changed there, nested only: when a bare array also fails the array arm (a format outside the enum), the object arm's branch of the union now names the object form instead of zod's `expected object, received array`. + + ## FROM → TO + + | you wrote on an `object-grid` | write instead | + |:--|:--| + | `exportOptions: ['csv', 'xlsx']` | `exportOptions: { formats: ['csv', 'xlsx'] }` — the grid now offers exactly those formats; write `{}` to keep the csv/json default it has been offering | + | `exportOptions: { formats: ['csv', 'pdf'] }` | `exportOptions: { formats: ['csv'] }` | + | `exportOptions: { formats: ['csv'], maxRecord: 100 }` | `exportOptions: { formats: ['csv'], maxRecords: 100 }` | + | `exportOptions: null` | omit `exportOptions` | + + The one-line fix: write `exportOptions` on an `object-grid` as the object `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, with `formats` drawn from `csv`, `xlsx` and `json`. + + ## Who is affected, measured + + On `origin/main` `f148852752`: zero `object-grid` blocks authoring `exportOptions` in the examples, the package fixtures, the documentation and the published skills, against ten authored `object-grid` blocks through the same census (nine in TypeScript, one in a YAML documentation example) and four list-view `exportOptions` authorings as the key's control. No conversion is registered: nothing on the metadata load path refuses the shape, and a bare array has no rewrite that both keeps what the grid shows today and honours the author's list. Deployed metadata was not measured. +- 99e1912: The metric sub-caption is retired at both ends. A dashboard widget keeps one authored description, `widget.description`, which renders as the card-header subtitle and is translated by the widget's `description` translation key. The widget translation key `subCaption` is refused, and the server no longer writes a widget's `options.description`. + + Clause-②: no (narrowing) + + + + **What is retired.** `dashboards.DASHBOARD.widgets.WIDGET.subCaption` in a translation bundle (`defineTranslationBundle`, `stack.translations`, the platform bundle) and in a registered `translation` item. It overlaid a caption under a metric's value onto the widget's `options.description`. The dashboard schema never declared `options.description`, and no authored widget wrote it, so `translateDashboard`'s overlay was the key's only writer. That overlay is removed: `translateDashboard` now translates a widget's `title` and `description` and carries `options` through untouched. + + **BREAKING** — an accept-set narrowing, shipped as `minor` under the launch-window convention. + + ### FROM → TO + + | wrote | write instead | + | --- | --- | + | `dashboards.DASHBOARD.widgets.WIDGET.subCaption: 'TEXT'` | delete the entry. If the copy belongs on the card, put it in the widget's `description` and translate it under `dashboards.DASHBOARD.widgets.WIDGET.description`. | + | `dashboards.DASHBOARD.widgets.WIDGET.subtitle: 'TEXT'` | `subtitle` was only ever a rename suggestion for `subCaption`. Card-header copy goes under `description`; a caption under the value has nowhere to render, so delete it. | + + **The one-line fix: delete every `subCaption:` entry under `dashboards.*.widgets.*` in your translation bundles.** `os migrate meta --from 17` lists the mechanical edits for existing sources; stored `translation` items are converted when they are read. + + **What an author now sees.** Writing `subCaption` fails `tsc` (its input type is the retired-key mark) and fails the parse with a prescription naming the widget's `description`. Writing `subtitle` on a widget translation fails the parse with both readings named, instead of a rename suggestion onto a key that is refused next. `os validate`, `os build` and `os lint` now raise the `unconsumed-widget-option` warning on an authored widget `options.description`, like any other options key the dataset-bound render path does not read. It is a warning, so none of the three fails on it. + + **Measured producers: none.** Zero `subCaption` entries and zero authored widget `options.description` in the four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and in the bundles `@objectstack/platform-objects` ships, so no shipped exit code changes. + + ### The retirement kit + + - **Tombstone.** `subCaption` is a `retiredKey()` tombstone on the widget translation node, so the refusal carries the prescription on all three faces the node is spread into (per-app bundle entry, platform bundle entry, `translation` item). The node sits under two records (`dashboards`, `widgets`), below the authorable-surface walk, so it has no `RETIRED_KEYS_BY_MAJOR` row, the same as the `submitLabel` component-copy key before it. + - **The former alias.** The `subtitle` → `subCaption` rename suggestion moves to the node's `guidance` table. An alias whose target is a tombstone is the shape the alias-integrity audit refuses, and repointing it at `description` would silently change what the word is taken to mean. + - **Conversion.** `translation-widget-sub-caption-removed` (protocol 18) strips the key from bundle entries and bare translation items as a lossless delete. It is retired from the load path, so authors are refused at parse while stored rows and `os migrate meta` replay it. Its D3 record is the semantic entry `translation-widget-sub-caption-retired`. + - **`@objectstack/sdui-parser`.** `CONSUMED_WIDGET_OPTION_KEYS` drops `description`, its one undeclared member, which existed only because the overlay wrote it. `check:widget-option-census`'s `NON_DECLARED_MEMBERS` ledger is now empty, so the census asserts that nothing writes an undeclared key into `options`. +- 7ebb543: feat(spec,plugin-audit): the compliance ledger's audit capability, `view_all_audit_log`, exempts its holder from the ledger's parent-record read gate; platform administrators hold it by default (#21260) + + Clause-②: yes (widening) + + - **The capability.** `PLATFORM_CAPABILITIES` (`@objectstack/spec/security`) gains `view_all_audit_log` ("View All Audit Log", `scope: 'org'`). It is seeded into `sys_capability` like every other curated capability, and a permission set grants it through `systemPermissions`. It is a platform capability, so an app that declares a capability of the same name cannot bind a set carrying it to the `everyone` or `guest` anchor. + - **Who holds it.** `ADMIN_FULL_ACCESS_CAPABILITIES` (`@objectstack/spec`) now lists it, so platform administrators hold it by default: through the `admin_full_access` grant, and through the envelope a configured platform owner resolves to. No other shipped permission set carries it. Any other position holds it only through a permission set that grants it. + - **What it does.** A read of `sys_audit_log` keeps only the rows whose parent record the caller can read. The holder skips that gate and is served every ledger row its grant on `sys_audit_log` reaches: rows about deleted records, sign-out rows, sign-in rows whose session has ended, and rows about records it cannot open. A broad read is served whole. The gate's 2,000-row pre-scan does not run for a holder, so the read is not cut off at that bound. + - **What still applies to the holder.** The holder still needs object-level read on `sys_audit_log`. The field-level redaction still narrows every before/after snapshot it is served. Under a walled tenancy posture, the tenant wall still keeps the holder to its own organization's rows, which is why the capability is declared `org`. + - **What it does not touch.** The activity stream (`sys_activity`) keeps its own parent-record gate for every caller, holders included. A non-holder's ledger reads are unchanged. + + **Migration.** None: no metadata, code or configuration change is needed. Platform administrators get the deletion and sign-out trail back with no action. To give an auditor the trail, grant `view_all_audit_log` through `systemPermissions` in a permission set that also grants read on `sys_audit_log`. +- 222ecc2: feat(spec): `ICryptoProvider` gains a required `keyedDigest(plain): Promise` member, and `LocalCryptoProvider` implements it (#21263) + + Clause-②: yes + + **BREAKING** for `ICryptoProvider` implementers: the new member is required, so a + provider that does not declare it stops compiling (`TS2420` on a class, `TS2741` + on an object literal), and the compiler names the missing member. Code that only + calls a provider is unaffected. + + `keyedDigest` is a digest of `plain` under the provider's server-held key, for a + value that is handed to a caller but must not let that caller check a guess about + the input offline. The contract requires three things of every implementation: + + - **Keyed.** The output cannot be computed without the provider's key. A provider + that holds no key material rejects; it never returns an unkeyed value. + - **Stable per key.** Under one key, equal input gives equal output in every + process and on every node that holds the key. Replacing the key changes every + output. + - **Not a substitute for `digest`.** `digest` keeps its contract and the stability + the audit trail relies on. + + The output is `hmac-sha256:` followed by the 64 lowercase hex characters of an + HMAC-SHA-256: 76 characters from `[0-9a-z:-]`, which travel unchanged in an HTTP + header, a query string and JSON, and never collide with the `sha256:` spelling of + an unkeyed content hash. + + `LocalCryptoProvider` computes it from the 32-byte data key it already resolves + (`OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY`, the persisted key file, or the ephemeral + test-mode key), through a MAC key derived from that data key, so the AES-GCM key + is never used as a MAC key. There is no new secret or environment variable to + configure. An instance constructed with an explicit key that is not 32 bytes holds + no usable key material, and its `keyedDigest` rejects with + `KeyedDigestKeyUnavailableError`. + + +- 3937ad2: feat(spec)!: an agent's `structuredOutput` is JSON-only — the `regex` / `grammar` / `xml` formats and the `coerce_types` step are retired, and the block is `live`, enforced by the cloud AI runtime (#21277) + + **BREAKING** — four members leave the agent's structured-output vocabulary: + `regex`, `grammar` and `xml` from `StructuredOutputFormat` (so from + `agent.structuredOutput.format` and `agent.structuredOutput.fallbackFormat`), and + `coerce_types` from `TransformPipelineStep` (so from + `agent.structuredOutput.transformPipeline`). ADR-0049 enforce-or-remove, ruled + retire. The cloud AI runtime, the one runtime that executes agents, enforces + `structuredOutput` on every final answer and refused an agent declaring any of the + four before its first turn: the spec never had a key to carry the pattern or + grammar a `regex` / `grammar` answer would be checked against, an answer is checked + only as JSON, and no coercion engine exists. So no authored value of the four ever + did what it named, and authoring now refuses them by name instead of the first + live turn refusing the agent. `json_object`, `json_schema`, `trim`, `parse_json` + and `validate` are unchanged. + + ### FROM → TO + + | removed | what to write instead | + | --- | --- | + | `structuredOutput.format: 'regex'`, `'grammar'` or `'xml'` | `format: 'json_schema'` with a JSON Schema in `schema` when the answer must have a shape, or `format: 'json_object'`; or delete the `structuredOutput` block if the agent needs no output contract. | + | `structuredOutput.fallbackFormat: 'regex'`, `'grammar'` or `'xml'` | `'json_object'` or `'json_schema'`, or delete the key. | + | `'coerce_types'` in `structuredOutput.transformPipeline` | delete the step, and declare the exact types in `schema` so the answer is validated as the model wrote it. | + + **The one-line fix: use `json_schema` with a JSON Schema; drop `coerce_types`.** + `os migrate meta --from 17` lists the mechanical edits for existing sources. + + Each retired member is refused at parse with a prescription naming the JSON + formats, and in `tsc` (the members are gone from the `StructuredOutputFormat` / + `TransformPipelineStep` types). Any other unknown value keeps zod's own message. + + ### The retirement kit + + - **Value-level retirement.** Both enums are declared through + `enumWithRetiredValues` (`shared/retired-key.ts`), the house mechanism for a + narrowed vocabulary, with the prescriptions module-private. No authorable KEY and + no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface + ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`, + `api-surface-signatures`) are byte-identical. + - **D2 conversion `agent-structured-output-refused-members-removed`** (step 18, + retired from the load path): it deletes a `structuredOutput` block whose `format` + was retired (the format is required, and no rewrite can say which JSON contract + was meant), deletes a retired `fallbackFormat`, and drops `coerce_types` from the + pipeline, keeping the other steps in order. Stored `sys_metadata` agent rows replay + it at rehydration; one notice per edit. + - **D3 entry `agent-structured-output-refused-members-retired`** carries the + judgement the conversion cannot make: whether an agent whose block was deleted + should now carry a `json_schema` contract. + - **No deprecation window**, per the project's startup-stage posture. + + ### Describes and the liveness ledger + + - `agent.structuredOutput` drops `[EXPERIMENTAL — not enforced]`: it states that the + cloud AI runtime enforces it on every final answer and that the open framework + edition does not run agents. Its ledger row moves `experimental` → `live`, citing + the cloud readers (`agent-runtime.ts#compileStructuredOutput`, + `ai-service.ts#AIService.settleFinalAnswer`) as attested by the cloud seat's + reading at cloud `cb62c3ea`, `verifiedAt` 2026-10-02. `os lint` / `os validate` no + longer warn `liveness-experimental-property` on an agent that sets it. + - `fallbackFormat`'s describe states what the runtime does with it: once the primary + format's retries are spent, the last answer is checked against the fallback. + - `guardrails.blockedTopics`'s describe states the enforced match: an exact, + case-sensitive match on the tool name, on `action_` plus the action type, or on + the tool category. + - The generated agent reference page follows. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published, and tenant-authored agents were not measured. This repo authors no + `structuredOutput` outside `packages/spec`, and the cloud seat's reading found no + producer in cloud. + + Clause-②: no (narrowing) + + +- 23365ea: feat(spec)!: `action.ai.outputSchema` and `agent.structuredOutput.schema` refuse an untyped subschema that carries a type-scoped keyword, at its path, as the AI runtime does (#21289) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on two published authoring slots, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. Every schema it refuses was already refused by the AI runtime before the action or agent ran, so nothing that worked stops working; what moves is where the refusal is reported — at authoring, at the subschema's path, instead of at the first invocation. + + **`@objectstack/spec`** + + - **`action.ai.outputSchema`** (stack actions and object-nested actions) and **`agent.structuredOutput.schema`** were open records. The cloud AI runtime compiles both through one guard whose schema reader does not check a type-scoped keyword on a subschema with no `type`, and refuses the whole schema. Both slots are now declared by one factory that mirrors that guard exactly: + - **refused:** an object node whose `type` is absent and which carries any of the 22 type-scoped keywords (`properties`, `required`, `additionalProperties`, `patternProperties`, `propertyNames`, `minProperties`, `maxProperties`, `items`, `prefixItems`, `contains`, `minItems`, `maxItems`, `uniqueItems`, `minLength`, `maxLength`, `pattern`, `format`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`), with any value; + - **where:** the schema root, every value of `properties`, `patternProperties`, `$defs`, `definitions` and `dependentSchemas`, and the subschema (or each array entry) of `items`, `additionalProperties`, `contains`, `propertyNames`, `not`, `if`, `then`, `else`, `unevaluatedProperties`, `unevaluatedItems`, `anyOf`, `oneOf`, `allOf` and `prefixItems` — under typed parents too; `$ref` is not followed; + - **accepted:** boolean subschemas, `{}`, a node with any `type` value, and an untyped node carrying only keywords outside the list (`enum`, `const`, `$ref`, `anyOf`, `title`, `description`, `default`, …); + - each offending subschema is its own issue, located at the slot path plus the subschema path (`ai.outputSchema.properties.customer`), and the message names the keyword and the `type` to declare. + - The TypeScript types of both slots are unchanged (`Record`). The published JSON Schema does not state the rule: it is a refinement, which the JSON Schema projection does not carry, so a JSON Schema validator still accepts such a schema in either slot. The affected published schemas name the slot in their `x-dropped-refinements` list. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `outputSchema: { properties: { id: { type: 'string' } }, required: ['id'] }` | `outputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }` | + | `schema: { type: 'object', properties: { tags: { items: { type: 'string' } } } }` | `schema: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' } } } }` | + | `{ properties: { code: { pattern: '^[A-Z]+$' } } }` anywhere in either slot | `{ type: 'object', properties: { code: { type: 'string', pattern: '^[A-Z]+$' } } }` | + + The one-line fix: declare its `type` on every subschema that carries a type-scoped keyword — `"object"`, `"array"`, `"string"`, or `"number"` / `"integer"`, as the refusal names. + + ## Who is affected, measured + + On `origin/main` `135daaa06b`: the package fixtures author either slot three times (one action `ai.outputSchema`, two `structuredOutput.schema`), every subschema typed; the examples, the documentation and the published skills author neither slot. No fixture needed a change. Deployed metadata was not measured. A stored action or agent carrying such a schema still loads; its next save is refused until the `type` is declared. +- 32d5769: feat(spec)!: a `pie` / `donut` / `funnel` / `treemap` / `sankey` dashboard widget takes ONE measure with a dimension too — two or more are refused at `values`, and the check export is renamed `checkDashboardWidgetChartMeasureArity` (#21293; extends #20958) + + Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface swaps one export for another: `checkDashboardWidgetDimensionlessMeasureArity` is removed and `checkDashboardWidgetChartMeasureArity` is added in its place, the same check with a second arm. + + + + **BREAKING** accept-set narrowing at `dashboard.widgets[].values`, plus one renamed + export, shipped as `minor` under this repo's launch-window convention for breaking + changes (`check-changeset-no-major` refuses `major` while the window is open, so + breaking-ness is carried by this banner and by the ADR-0087 disposition above, + never by the bump level). The prescription is registered under protocol major 18 + as `dashboard-widget-single-series-multi-measure-refused`. + + **What was wrong.** The previous release refused two or more measures on a + dimensionless `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / + `sankey`, and stepped aside for any widget that declared a dimension. Five of those + types draw ONE series whatever the dimension: objectui's chart renderer binds the + first series on its `pie` / `donut`, `funnel`, `treemap` and `sankey` arms and reads + no other, so `{ type: 'pie', dimensions: ['stage'], values: ['revenue', 'cost'] }` + drew one slice per stage for `revenue` and no trace of `cost`. Measured on this tree + before the change: that body parsed through `DashboardWidgetSchema` on all five + types (and on `scatter` / `radar` / `bar` / `table`), while `bogusProp` on the same + widget was refused by name, the lit control. After it, the five are refused at + `widgets[N].values`; `scatter` and `radar` with a dimension are outside the ruling + and parse as before. + + ### Write instead + + | wrote | write instead | + |---|---| + | `{ id: 'mix', type: 'pie', dataset: 'sales', dimensions: ['stage'], values: ['revenue', 'cost'] }` | `{ id: 'mix', type: 'table', dataset: 'sales', dimensions: ['stage'], values: ['revenue', 'cost'] }` — a column per measure | + | the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure in each stage | + | the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['revenue'] }` **and** `{ id: 'mix_cost', type: 'pie', …, values: ['cost'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) | + | `import { checkDashboardWidgetDimensionlessMeasureArity } from '@objectstack/spec/ui'` | `import { checkDashboardWidgetChartMeasureArity } from '@objectstack/spec/ui'` — same `(widget, ctx)` signature; chain it where the old name was chained | + + No conversion does this for you: whether a two-measure pie by stage meant a table, a + grouped bar chart or two pies is an authoring choice. The refusal is ONE `custom` + issue at `widgets[N].values` naming the widget's `id`, the number of measures and + the authored `type`, and saying that type draws one series whatever its + `dimensions`. + + **Why the export is renamed.** The dimensionless rule's check now has a second arm + that judges widgets WITH a dimension, so its old name described a boundary that no + longer exists. It refuses everything the old name refused, word for word on a + dimensionless widget. No first-party consumer chained the old name: objectui's + `DashboardWidgetSchema` mirror chains `checkDashboardWidgetStageOrder` and + `checkDashboardWidgetMetricMeasureArity` only, measured at the pinned objectui + commit and on objectui's `main`. + + **Nothing else moves.** One measure parses on every type; `scatter` and `radar` + keep accepting several measures with a dimension; every type in + `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` keeps accepting any number of measures with + or without a dimension; a dimensionless widget of the five keeps the dimensionless + refusal, word for word and still ONE issue; the metric family's refusal is + unchanged; an empty `values` keeps its `too_small`; a `type` outside + `ChartTypeSchema` reports the type refusal alone. Census at the branch point + (`4b20c8474`), every tracked `.ts` / `.tsx` / `.js` / `.mjs` / `.cjs` / `.json` / + `.md` / `.mdx` / `.yml`: 496 literals carry `values: [...]`, 33 of them on one of + the seven types, and the only dimensioned multi-measure one on the five is a spec + test fixture that pinned the old acceptance (moved to the refusal in this change). + The same scan over objectui at its pinned commit (`89cad75d5`) finds no authored + widget of that shape — its one hit is the prose example in a changeset. +- 6e33b67: feat(spec)!: retire `agent.lifecycle`, the agent conversation state machine, and with it the XState `StateMachineSchema` family — a conversation phase is a skill with `triggerConditions`, orchestration is Flow, record transitions are the `state_machine` validation rule (#21320) + + **BREAKING** — `agent.lifecycle` was parsed and never read. No runtime, in this + repository or in the cloud AI runtime that executes agents, moved an agent through a + declared state or refused an undeclared transition, so an authored machine changed + nothing an agent did (ADR-0049 enforce-or-remove). Enforcing it would have meant a + statechart interpreter beside Flow, the two-engine shape ADR-0020 rejected. Authoring + now refuses the key by name, with a prescription, and TypeScript rejects it. + + Its value schema had no other authorable door: ADR-0020 had already retired the XState + shape as a record-lifecycle declaration and kept the file only for this key. So the + family leaves the package with it. + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `agent.lifecycle` — any value | delete the key. | + | a conversation phase in the machine (its own instructions and tools) | a skill with its own `instructions` and `tools`, selected by its `triggerConditions`, listed in the agent's `skills`. | + | a multi-step process in the machine | a Flow. | + | a record's status transitions in the machine | a `state_machine` validation rule in the object's `validations`: `{ type: 'state_machine', field, transitions: { from: [to, …] } }`. | + | `StateMachineSchema`, `StateNodeSchema`, `TransitionSchema`, `ActionRefSchema`, `GuardRefSchema` and the types `StateMachineConfig`, `StateNode`, `StateNodeConfig`, `Transition`, `ActionRef`, `GuardRef` from `@objectstack/spec/automation` | no replacement: declare the shape your code needs itself, or drop it. For record transitions, `StateMachineValidationSchema` in `@objectstack/spec/data` is the enforced shape. | + | `StateNodeConfig` from `@objectstack/spec` or `@objectstack/spec/ai` | removed with the family; nothing in those entries mentions it any more. | + + **The one-line fix: delete `lifecycle`; put phase-scoped instructions and tools in + skills with `triggerConditions`, and orchestration in Flow.** `os migrate meta --from 17` + lists the mechanical edits for existing sources (the `lifecycle` deletion). Where each + deleted machine's intent goes is the author's judgement. + + The refusal is a parse error at `lifecycle` naming the key and the fix, and the key + fails `tsc` (its input type is `never`). + + ### The retirement kit + + - **Tombstone.** `lifecycle` is a `retiredKey()` on `AgentSchema` carrying the + prescription; the agent metadata form no longer offers it. + - **D2 conversion `agent-lifecycle-removed`** (step 18, retired from the load path): + it deletes `lifecycle` from every agent, whatever it holds. The delete is lossless, + because no value of it ever changed what an agent did. Stored `sys_metadata` agent + rows and built artifacts replay it; one notice per agent. An object's ADR-0057 + `lifecycle` block shares the name and is not touched. + - **D3 entry `agent-lifecycle-retired`** carries the judgement the conversion cannot + make: which of the three destinations each deleted machine meant. + - **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:lifecycle`, and + **`RETIRED_DEFS_BY_MAJOR[18]`** registers the five published defs + `automation/StateMachine`, `automation/StateNode`, `automation/Transition`, + `automation/ActionRef` and `automation/GuardRef`. Their reference page + (`references/automation/state-machine`) is gone. + - **No deprecation window**, per the project's startup-stage posture. + + ### The liveness ledger + + The `agent.lifecycle` row moves `experimental` → `dead` with a REMOVED note + (`verifiedAt` 2026-10-02); the tombstone keeps it in the walked shape. No `agent` row is + `experimental` any more. `os validate` and every other parsing door refuse the key at + parse, before any advisory runs. `os lint` reads the unparsed stack, so it now grades the + key `liveness-dead-property` where it used to say `liveness-experimental-property`. + + ### `@objectstack/platform-objects` + + The agent metadata-form catalogs drop the `lifecycle` row's label and help text in all + four locales. + + ⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is + published: tenant-authored agents, and code outside this repository importing the + family's exports, were not measured. This repository authors no `agent.lifecycle` + outside `packages/spec` and imports none of the family outside it; the pinned objectui + checkout imports none of the family and reads no `agent.lifecycle`. + + Clause-②: yes (narrowing) + + +- 57cc695: feat(spec): `CryptoContext` gains a required `scope` discriminant, and `LocalCryptoProvider` binds it into a delimiter-safe, versioned AAD (ADR-0128 D1–D3, #21326 stage 1) + + Clause-②: yes + + **BREAKING** for `ICryptoProvider` implementers and for every direct caller of + `encrypt`, `decrypt` or `rotateKey`: `CryptoContext.scope` is required, so a + context literal without it stops compiling (`TS2741`), and the compiler names the + missing member. `LocalCryptoProvider` also refuses such a context at runtime with + `CryptoContextScopeError`, for a caller the compiler never saw. Code that only + injects a provider is unaffected. + + `scope` is a member of the new closed set `CRYPTO_CONTEXT_SCOPES` (type + `CryptoContextScope`), one member per producer of `CryptoContext`: + `settings` (`SettingsService`), `object_secret_field` (the ObjectQL engine's + secret-field path) and `datasource_credential` (the datasource secret binder). + Each producer in this release passes its own member on every call. A new producer + adds its own member; it never borrows an existing one. + + What the contract now requires of every provider that binds AAD: + + - **Producer-discriminated (D1).** The AAD binds `(scope, namespace, key)`, so a + ciphertext sealed by one producer does not authenticate under another + producer's context, whatever the two `(namespace, key)` pairs are. + - **Delimiter-safe (D2).** Distinct triples produce distinct AAD bytes. An + unescaped join is not permitted. + - **Versioned.** A ciphertext records which AAD derivation sealed it, and is + opened only with that derivation. An unknown derivation fails closed. No second + derivation or scope is ever tried after a failure (D3). + + `LocalCryptoProvider` seals every new value under derivation version 2: a lead + byte that never occurs in UTF-8, a versioned label, then the scope, namespace and + key, each prefixed with its 4-byte length. The ciphertext carries a `v2:` marker. + A ciphertext with no marker is version 1, the bare base64 every earlier release + sealed, and it still opens with the older `(namespace, key)` binding. Existing + secrets therefore keep working with no action, and carry the older binding until + they are re-wrapped. Re-wrapping existing ciphertext at rest is stage 2 of + #21326. `rotateKey` already re-seals a version-1 handle under version 2. Any other + marker is refused with `UnknownCiphertextVersionError`. + + Operational note: a secret set or rotated by this release carries the `v2:` + marker, and an earlier release cannot open it. A rollback past this release needs + those values to be set again. + + `@objectstack/objectql` and `@objectstack/service-datasource` pass their own + scope on every seal and open. Their public surface is unchanged. + + +- 9f13c94: feat(spec): the boolean-comparand declared-type contract in `@objectstack/spec/data` — the comparands a declared boolean field accepts in a filter, the boolean each narrows to, and the refusal words + + Clause-②: yes + + **What it declares.** `filter-boolean-comparand-declared-type.ts`, the boolean twin of `filter-number-comparand-declared-type.ts`: + + - `BOOLEAN_COMPARAND_SPELLINGS`: the accepted non-boolean spellings, `1` / `0`, `"1"` / `"0"` and `"true"` / `"false"`, each with the boolean it narrows to. This is the set the record validator admits when a boolean field is written. `readBooleanComparand` reads a comparand by it, and names why a string is not one (`NON_BOOLEAN_STRING_FORMS`: `empty`, `padded`, `letter-case`, `placeholder`, `not-a-boolean`). + - `BOOLEAN_COMPARAND_DOOR_JUDGED_TYPES` (`BOOLEAN_VALUE_TYPES` itself), and the judged positions, which are the number door's lists by identity. + - `booleanComparandFieldVerdict` and `booleanComparandDoorVerdict`, the pure verdict: `narrows`, `door-refusal` (`INVALID_FILTER` / 400), `passes` or `deferred`. + - `booleanComparandRefusalMessage`: the refusal words, inside the 500-character client bound. + - `BOOLEAN_COMPARAND_READING_CASES`, `BOOLEAN_COMPARAND_DOOR_FIXTURE` and the derived `BOOLEAN_COMPARAND_DOOR_CASES`, for a door's suite to drive. + + **What the verdict answers `door-refusal` for.** A string other than the four accepted ones, compared with a declared boolean field, at the value positions of a filter (the implicit comparand, `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte`, and each member of `$in` / `$nin` / `$between`). + + **What moves for consumers.** Nothing in this package refuses or narrows a filter, and every existing export is unchanged. The door that applies the verdict ships in the same release in `@objectstack/objectql`, whose changeset states what changes for a caller. +- 6d67ad5: fix(spec)!: an analytics query's `limit` and `offset` are non-negative integers, and the native face runs an `offset` with no `limit` on SQLite + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing of a published request schema, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads it: the `/analytics` doors, which parse every body with `AnalyticsQueryRequestSchema` (`POST /analytics/query`, `POST /analytics/sql`) or `DatasetSelectionSchema` (`POST /analytics/dataset/query`), and answer `400 VALIDATION_FAILED` before any engine runs. + + **`@objectstack/spec`** + + - **`AnalyticsQuerySchema.limit` and `.offset`** were a bare `z.number()`. They are `z.number().int().nonnegative()` now. A negative number, a fraction, and an integer above `Number.MAX_SAFE_INTEGER` are refused at the member. `limit: 0` stays legal and answers no rows. + - **`DatasetSelectionSchema`** reads the same two declarations off `AnalyticsQuerySchema.shape`, so the dataset door holds the same accept set with no second copy. **`AnalyticsQueryRequestSchema`** extends the query, so it holds it too. + - The TypeScript types are unchanged (`number`). Only the parse narrows. + + Before, no refused value had one answer. Measured at `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14, `order { note: 'asc' }` over four groups: + + | window | native SQLite | native PostgreSQL | ObjectQL face | + |:--|:--|:--|:--| + | `limit: -1` | every row | 500 | all but the last row | + | `limit: 1.5` | 500 | two rows | one row | + | `offset: -1` | 500 | 500 | every row | + + Each one now answers `400 VALIDATION_FAILED`, with `details.fields[].field` naming `limit` or `offset` (`selection.limit` / `selection.offset` at the dataset door), on both drivers and both faces. + + **`@objectstack/service-analytics`** + + - **An `offset` with no `limit`** is a valid window: every row after the offset. The native-SQL strategy wrote `OFFSET n` with no `LIMIT` in front of it, and SQLite's grammar has no `OFFSET` without a `LIMIT`, so the query answered `500` (`near "OFFSET": syntax error`) on SQLite, while PostgreSQL and the ObjectQL face answered rows. The statement now carries the executing driver's no-limit spelling, read off the `sqlDialect` hook: `LIMIT -1 OFFSET n` on SQLite, `OFFSET n` alone on PostgreSQL (unchanged bytes), and `LIMIT 9223372036854775807 OFFSET n` when the host names no dialect. The MySQL arm is `LIMIT 18446744073709551615`, asserted as text only (no MySQL server was available to run it). + - The echoed `sql` and `POST /analytics/sql` show the statement that ran, byte for byte, on this face. + + ## FROM → TO + + | you wrote in an analytics query or dataset selection | write instead | + |:--|:--| + | `limit: -1` (meant: no limit) | omit `limit` | + | `limit: 1.5` | the integer page size you meant, for example `limit: 2` | + | `offset: -1` | omit `offset`, or `offset: 0` | + | `offset: 2.5` | the integer number of rows to skip, for example `offset: 2` | + + The one-line fix: write `limit` and `offset` as non-negative integers, or leave them out. + + ## Who is affected, measured + + At `origin/main` `ee75aae1a`: no example, package fixture, document or published skill writes a negative or fractional analytics `limit` or `offset`. The one stored producer that lowers into a dataset selection, a dashboard widget's `limit`, is already declared a positive integer (`z.number().int().positive()`). The sibling console repository and deployed metadata were not measured. The service does not parse a query passed to it in-process, so a host that builds an `AnalyticsQuery` in code parses it with `AnalyticsQuerySchema` before handing it over. +- ca0dfb6: The agent metadata form now offers `structuredOutput`, the output contract the cloud AI runtime enforces on every final answer. It is a `composite` row in the AI Configuration section, spelled like the `memory` and `guardrails` rows: Studio derives its seven sub-rows from the served JSON Schema. + + Clause-②: no + + - Before this, the block had no row on the agent form, so the only way to author it in Studio was the Source tab. The form's reconciliation test excused that with a ledger row saying the key was declared but not enforced. The key has been enforced since the structured-output enforcement landed (liveness `live`), and that row is gone. + - What Studio renders, read in the console's metadata form renderer: `format` and `fallbackFormat` are selects over `json_object` / `json_schema`. `strict` and `retryOnValidationFailure` are switches, and `maxRetries` is a number. `transformPipeline` is a multi-select over `trim` / `parse_json` / `validate`. `schema`, the free-form JSON Schema record, is a JSON text editor: the stored value is shown as JSON and saved back as parsed. That is the same editor the action form already gives `ai.outputSchema`, which is the other slot this JSON Schema rule governs. + - Two editing limits of those controls. A multi-select toggle stores the steps in the order the enum declares them (`trim`, `parse_json`, `validate`). And the schema editor keeps the last valid JSON while the text does not parse. A value nobody edits is saved back unchanged. + - No schema, parse or export change. The accept set of `AgentSchema` is unchanged, and so is the refusal of an untyped JSON subschema at `structuredOutput.schema`. What moves is the form payload `getMetaTypes()` serves, and the two new leaves of the `platform-objects` metadata-form catalogs (the row's label and help text). Those are authored in `zh-CN`, `ja-JP` and `es-ES`, not left as copies of the English source. +- 45efcfa: fix(spec)!: the boolean-comparand verdict refuses a number other than `1` / `0`, a `Date` and an array compared against a boolean field, the same as a string that is not a boolean + + Clause-②: yes (narrowing) + + + + **BREAKING**: this narrows what a filter may compare a boolean field with. `booleanComparandDoorVerdict`, the published verdict the engine's boolean-comparand arm consumes, judged strings only; it now also answers `door-refusal` (`INVALID_FILTER` / 400) for a number other than `1` / `0`, a `Date` and an array, so the engine refuses them before any read, on every driver. It ships as `minor` under the launch-window convention for accept-set narrowings. The accepted set is unchanged: `true`, `false`, `1`, `0`, `"true"`, `"false"`, `"1"` and `"0"`, and `null` is still the null test. + + What moves in `@objectstack/spec/data`: + + - `booleanComparandDoorVerdict(field, comparand)` answers `door-refusal` with a new `form` for each non-string: `number`, `date` or `array`. `readBooleanComparand` reads a `bigint` as the number it names, so `1n` / `0n` narrow like `1` / `0` and any other `bigint` is refused as a number. That is the number the comparand-type door rewrites a `bigint` to, so the answer no longer depends on which door met it first. + - Three additive exports: `NON_BOOLEAN_VALUE_FORMS` (`number`, `date`, `array`) and the types `NonBooleanValueForm` and `NonBooleanComparandForm`. The refusal's `form` (on `BooleanComparandDoorVerdict`, `BooleanComparandRefusalSite` and `BooleanComparandDoorRefusalCase`) widens from `NonBooleanStringForm` to `NonBooleanComparandForm`, and the refusal site's `value` now carries a non-string. A consumer that switches over `form` exhaustively gains three cases. + - `booleanComparandRefusalMessage` gains one clause per non-string form, and renders a `Date` as `Date(ISO)` and a non-finite number by name instead of as JSON. + - `BOOLEAN_COMPARAND_DOOR_CASES` gains a `value` group: `-1` at `$ne` on every judged field, and `2`, a `Date` and an array at every judged position of `f_boolean` (no array at the equality slots, where the comparand-shape door refuses one first), plus the passing rows beside them. The `2` / `-1` reading rows, and a new `0.5` row, now derive refusals. + + FROM a number other than `1` / `0` (`2`, `-1`, `0.5`), a `Date`, or an array where one value belongs (a scalar operator's comparand, or a member of `$in` / `$nin` / `$between`), compared against a `boolean` or `toggle` field (or a groupBy / `min` / `max` column of one in `having`) → TO `INVALID_FILTER` / 400, naming the field, its declared type, the comparand, its position and what is wrong with it. The fix is one line: send `true` or `false`, or `1` / `0`; to match either value use `$in`, each member a boolean. + + **Unchanged.** Every string the verdict accepted or refused is answered as before, in the same words. A boolean, `null` and the flag operators (`$null`, `$exists`, `$empty`) pass. A value outside the accepted comparand types (`undefined`, a plain object, a `Map`) keeps the comparand-type door's own refusal and words, and a `{ $field }` reference is not judged. A comparand against a field that is not boolean is not this verdict's subject. +- b793010: feat(spec)!: the analytics row wildcard `'*'` is admitted only where a `count` consumes it — a cube or dataset measure over `'*'` under any other aggregate, and a cube dimension over `'*'`, are refused at parse (#21409) + + Clause-②: no (narrowing) + + **BREAKING** — shipped as `minor` under the launch-window convention + (`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by + this banner, the `(narrowing)` arm above and the ADR-0087 disposition below, + never by the level). + + `'*'` is the row wildcard: what a `count` aggregates (`COUNT(*)`), reading no + field value. It is now admitted in exactly one place, a measure that counts: + + - `MetricSchema.sql` — a cube measure's `sql` — admits `'*'` under + `type: 'count'` only; under any other `type` it is refused at `sql` + (code `custom`). + - `DatasetMeasureSchema.field` — an ADR-0021 dataset measure's `field` — admits + `'*'` under `aggregate: 'count'` only; under any other aggregate, or on a + measure with no aggregate (a `derived` one), it is refused at `field` + (code `custom`). A count may still omit `field`. + - `DimensionSchema.sql` — a cube dimension's `sql` — never admits `'*'` + (code `invalid_format`): it takes the column path without the wildcard arm, + the pattern a dataset dimension's `field` already takes. + + Each refusal names the slot and the aggregate the author wrote, and prescribes + the two ways out: a `count`, or a column. A column or a relationship path parses + byte-identically to before on every slot, and so does a `count` over `'*'`. + + Why: no aggregate but `count` has a column to read over `'*'`, and a dimension + has no aggregate at all, yet the contract admitted the wildcard on any measure + and on a cube dimension, and the analytics strategies sent it to the database as + written. Measured at `POST /api/v1/analytics/dataset/query` over a real SQLite + driver, on the native-SQL and the ObjectQL strategy alike: a dataset measure + aggregating `'*'` under `sum`, `avg`, `min`, `max` or `count_distinct` answered + `500 DATABASE_ERROR`. A dataset measure compiles to the cube measure it names + verbatim, so the same reading covers an authored cube measure. Such a member + never produced an answer, so no working document changes meaning: the failure + moves from the query to the authoring parse. The two measure slots ask ONE + shared predicate; the rule is cross-field (the slot and its aggregate), so it is + a refinement, which the published JSON Schema cannot carry — both sites are + declared in `dropped-refinements.baseline.json`. The dimension half is a + `pattern`, so `json-schema/**` states it. + + ## FROM → TO + + ``` + FROM { name: 'deal_metrics', label: 'Deal Metrics', object: 'deal', + dimensions: [{ name: 'stage', field: 'stage' }], + measures: [{ name: 'deals', aggregate: 'sum', field: '*' }] } + -> DatasetSchema.parse accepted it; a dataset query selecting `deals` + answered 500 DATABASE_ERROR + TO -> DatasetSchema.parse throws a ZodError at measures.0.field (custom): + `measures[].field` is the row wildcard `'*'` under `aggregate: 'sum'`. … + defineStack({ datasets }) refuses it at datasets.N.measures.0.field (422 + STACK_SCHEMA_INVALID), and POST /api/v1/analytics/dataset/query answers + 400 VALIDATION_FAILED for an inline or a saved copy + + measures: [{ name: 'deals', aggregate: 'count' }] // a row count + measures: [{ name: 'deal_value', aggregate: 'sum', field: 'amount' }] // an aggregate of a column + + FROM defineCube({ name: 'deals', sql: 'deal', + measures: { total: { label: 'Total', type: 'sum', sql: '*' } }, + dimensions: { everything: { label: 'All', type: 'string', sql: '*' } } }) + TO -> refused at measures.total.sql (custom) and dimensions.everything.sql (invalid_format) + + measures: { total: { label: 'Total', type: 'sum', sql: 'amount' } }, + dimensions: { stage: { label: 'Stage', type: 'string', sql: 'stage' } } + ``` + + **The one-line fix:** parse each cube and dataset; every refusal at `…sql` / + `…field` naming `'*'` is one member to change — declare a `count` to count rows, + or name the column the measure aggregates (a dimension names the column it + groups by). On a `derived` dataset measure, delete `field`: nothing read it. + There is no mechanical rewrite: `os migrate meta` rewrites nothing for it, and + lists the entry `analytics-row-wildcard-outside-count-refused` as a manual + change that requires your judgment. + + **What a stored document meets.** A metadata read still serves it as stored, + with the refusal on its read diagnostics (`_diagnostics`), and a re-save through + the metadata write door is refused at the slot. `POST + /api/v1/analytics/dataset/query` parses every dataset it is handed, inline or + saved, so a stored dataset carrying such a measure answers `400 + VALIDATION_FAILED` at `measures.N.field` on every query — including a query that + selects only its other measures, which used to answer — until the member is + fixed: it fails closed. An authored cube reaches the analytics runtime through + the stack definition, whose parse refuses it. + + ## The kit + + - **Schema.** `data/analytics-column-reference.ts` (not published API) declares + the predicate `rowWildcardOutsideCount` and its refusal once; `MetricSchema` + and `DatasetMeasureSchema` call both from a refinement, and + `DimensionSchema.sql` takes `ANALYTICS_COLUMN_PATH`. No export, key or enum + member changes, so the api-surface, authorable-surface and JSON-schema + manifest ratchets are unchanged. + - **ADR-0087.** D3 entry `analytics-row-wildcard-outside-count-refused`. No D2 + conversion: rewriting to `count` would change the figure the author asked for, + and only the author can name the column. No `RETIRED_KEYS_BY_MAJOR` row. + - **Dropped refinements.** `data/Metric` and `ui/DatasetMeasure` gain their root + site, and every published schema embedding them gains the embedded site. + - **Liveness.** `analytics_cube` `measures.sql` / `dimensions.sql` and `dataset` + `measures.field` stay `live`, re-verified, their notes re-pointed here. + - **Docs.** The `ui/dataset` reference page is regenerated. + - **Runtime.** Unchanged. + + ## Reach, measured + + - This repository: no example, platform object, doc, skill, script or test + fixture authors `'*'` outside a `count` at the three slots (`git grep` of every + `field` / `sql` value spelled `'*'`, 173 hits, each read in its enclosing + object: 154 under a `count`, the rest QueryAST aggregations, comments and + strategy-level literals). One spec pin admitted `'*'` on a cube dimension; it + now pins the refusal. + - objectui at the pinned `.objectui-sha`: zero `field` / `sql` values spelled + `'*'` (lit controls: 51 `aggregate: 'sum'`, 438 `field: 'amount'`). + - Out-of-repo authored metadata: NOT MEASURED. + + +- aa46322: feat(spec)!: an `object-grid` page block's props type the seven members the grid reads with a fixed shape, and the legacy `resizableColumns` spelling is retired in favour of `resizable` (#21445) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **Seven members of `ComponentPropsMap['object-grid']` are typed.** Each was `z.unknown()` (`bulkActionDefs` an array of it), although the console's `ObjectGrid` reads each with one shape. Any value passed, and the grid answered an off-shape one with a silent default: `rowHeight: 42` rendered as a compact grid, and an aggregation with an unknown function drew a zero nothing computed, or no number at all. Each member now takes the shape the grid reads: + - `rowHeight` is the list view's `RowHeightSchema`: `compact`, `short`, `medium`, `tall` or `extra_tall`. These are exactly the five values the grid admits. + - `rowColor` is the list view's `RowColorConfigSchema`, `{ field, colors }`. + - `navigation` is the list view's `NavigationConfigSchema`, the same carrier `object-kanban`, `object-calendar` and `object-timeline` take. + - `conditionalFormatting` is the list view's own member, `[{ condition, style }]`, with a CEL `condition` and a CSS `style` map. + - `bulkActionDefs` is an array of the list view's `BulkActionDefSchema`. + - `aggregations` is `[{ field, type }]`, with `type` drawn from the query AST's aggregation functions (`count`, `sum`, `avg`, `min`, `max`, `count_distinct`). No list-view schema declares this member, so the shape is the one the grid's grouping reads. + - `operations` is `{ create?, update?, delete?, export? }`, the four booleans a grid read point names. `read` and `import` are refused with the reason: no grid read point reads either. + - **`resizableColumns` is retired.** It was the legacy second spelling of `resizable`, read only when `resizable` was absent, so a grid authoring both silently ignored it. It is now a `retiredKey()` tombstone: writing it fails `tsc` (the input type is `never`) and fails the parse with a prescription naming `resizable`. Nothing in either repository wrote it. + - **`ObjectGridProps`** (and `ObjectGridPropsParsed`) carry those types instead of `unknown`, and `resizableColumns` is `never`. + + ## FROM → TO + + | you wrote on an `object-grid` | write instead | + |:--|:--| + | `resizableColumns: false` | `resizable: false` — the same boolean | + | `resizableColumns: true` beside `resizable: false` | `resizable: false` — the grid has always followed `resizable` | + | `rowHeight: 42`, `rowHeight: 'comfortable'` | `rowHeight: 'medium'`, or another of `compact` / `short` / `tall` / `extra_tall` | + | `rowColor: 'red'` | `rowColor: { field: 'status', colors: { overdue: 'red' } }` | + | `navigation: 'drawer'` | `navigation: { mode: 'drawer' }` | + | `conditionalFormatting: [{ field: 'status', operator: 'equals', value: 'late', backgroundColor: '#fee2e2' }]` | `conditionalFormatting: [{ condition: "record.status == 'late'", style: { backgroundColor: '#fee2e2' } }]` | + | `aggregations: [{ field: 'amount', type: 'median' }]` | a function the grid computes: `count`, `sum`, `avg`, `min`, `max` or `count_distinct` | + | `operations: { create: true, read: true, import: false }` | `operations: { create: true }` — delete `read` and `import`; nothing reads them | + + The one-line fix: rename `resizableColumns` to `resizable`, and write each of the seven members in the shape the list view declares for the same key (`aggregations` as `[{ field, type }]`, `operations` as four booleans). `os migrate meta --from 17` lists the mechanical `resizableColumns` edits for existing sources. + + ## The retirement kit + + - **Tombstone.** `resizableColumns` is a `retiredKey()` on `ObjectGridPropsSchema`; its authorable-surface line carries `[RETIRED]`. + - **Conversion.** `object-grid-resizable-columns-removed` (protocol 18, retired from the load path) follows the renderer's own precedence. It moves the value to `resizable` when `resizable` is absent, and deletes the key as a lossless strip when `resizable` holds a value. Its D3 record is the semantic entry `object-grid-resizable-columns-retired`, which carries the judgment for a grid that authored both keys with different values. + - **Registration.** `RETIRED_KEYS_BY_MAJOR[18]` carries `ui/ObjectGridProps:resizableColumns`. + - **The typed members** have the D3 entry `ui-object-grid-row-members-typed` and no conversion. Nothing on the load path refuses their shapes, and an off-shape value has no rewrite that keeps what the grid shows while honouring what the author wrote. + + ## Who is affected, measured + + - **objectstack.** Measured on `origin/main` `53fd35e3e3`: zero `object-grid` blocks author any of the seven members or `resizableColumns` in the examples, `@objectstack/platform-objects`, the spec tests, the documentation and the published skills. The control: the same census finds the two showcase grids' `columns`. + - **objectui.** Measured at the `.objectui-sha` pin, over 76 `object-grid` property bags in its sources, tests and documentation (23 of them in parsed JSON documents). One documentation example, the repository README's data grid, authors `operations.read: true`, which this row now refuses. No other bag authors a refused shape. The control: the same census finds `columns` in 46 bags. + - **Deployed metadata** was not measured. +- 100c394: A list at a scalar operator (`{ amount: { $gt: [10, 99] } }`) is refused at the shared comparand-shape face, whatever the column type, instead of being narrowed to its first member + + Clause-②: no (narrowing) + + + + **BREAKING**: this narrows what the shared filter faces accept. FROM: a list at a scalar operator passed the comparand-shape face, and each consumer answered it alone. The analytics lowering bound the list's first member (`{ note: { $gt: ['a', 'z'] } }` answered 200 as `$gt 'a'` on both analytics faces, and the engine-aggregate face did the same on a number column), `driver-sql` refused it in its own words, and `driver-memory` answered one at a text operator. TO: `INVALID_FILTER` / 400 at the face, before any read, on every door that runs it, with one sentence naming the operator, the field, the list and where. It ships as `minor` under the launch-window convention for accept-set narrowings. No export, type or error code changes. + + - **What changed.** `assertListComparandShapes` (`@objectstack/spec/data`) refuses a list under every scalar operator other than `$eq` / `$ne`, which keep their own refusals: `$gt`, `$gte`, `$lt`, `$lte`, the text operators (`$contains`, `$notContains`, `$startsWith`, `$endsWith`, `$icontains`, `$like`, `$ilike`) and the flags (`$null`, `$exists`, `$empty`). This covers every depth, both filter spellings (object and `[field, op, value]`), and the empty list. + - The engine's `where`, per-aggregation `filter` and `having`, `parseFilterAST`, both analytics doors, the read-scope compiler and the RLS compiler all run the face, so all of them refuse it. + - The save door asks the same face. A dataset, measure, dashboard-widget or report filter that carries one is refused on save, located on the member. The HTTP routes that parse a filter in their body (`POST /api/v1/data/:object/query`, `/api/v1/analytics/query`, `/api/v1/analytics/dataset/query`) answer `VALIDATION_FAILED` / 400 there, as for every other face refusal. + - **What you may notice.** A filter that put a list under `$gt`, `$contains` or a flag now refuses instead of answering. Write one value; for "one of these values" use `$in` (authoring `in`), and for a range use `$between` (authoring `between`). A list at a flag reads in this sentence now, not the boolean-flag one. + - **Unchanged.** A list at `$in` / `$nin` / `$between`, `$in: []` / `$nin: []`, every single value (`null`, a `Date` and a `{ $field }` reference included), a list nested inside `$in`, and an operator outside the declared vocabulary, which keeps its own refusal. +- 72217cd: feat(spec): `ApprovalActionRow` declares `acted_as`, the pending-approver slot an approval action was taken as, beside the person in `actor_id` + + Clause-②: yes + + **What it declares.** One optional string member on `ApprovalActionRow` in `@objectstack/spec/contracts`, the row type of an approval request's action log (`IApprovalService.listActions`, served at `GET /api/v1/approvals/requests/:id/actions` and typed by the client SDK): + + - `acted_as?: string` is the slot the action was admitted under, in the slot's stored spelling as it stood in the request's `pending_approvers`: a `position:` address (or `role:`, the deprecated pre-rename spelling), an email, or a user id. + - It is never a person. The person who acted is `actor_id`, which under ADR-0118 D1 holds a `sys_user` id or nothing. A slot addressed by a user id carries that id in `acted_as` as the slot's address, which makes no claim about who acted. + - Absent means the action was not admitted through a slot (a submitter's own action, a system action, or an admin override, which `via_override` marks), or the row was written before the slot was recorded. So absent alone never proves that no slot was involved. + + **What moves for consumers.** Nothing in this package writes the member, and every existing export and member is unchanged: a row without `acted_as` conforms exactly as before. The approvals service is its producer, and that package's own changeset states when `listActions` starts returning it. Until then every row omits it, which is the member's declared absent case. A client that renders the action log can show `acted_as` beside the actor's name as the capacity the actor acted in. +- 72af58c: A page's `requires` is accepted only on the kinds whose source is compiled at save: `html` and its deprecated alias `jsx`. On a `react`, `full` or `slotted` page, and on a page that omits `kind` (which is `full`), it is refused at parse. + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + + **Why.** `requires` is the list of plugin namespaces a page's source uses (ADR-0080 §5). It is derived from the source at save, and its describe has always said "omit it". On an html page, on a server that has the deployment's SDUI component manifest, the metadata save door compiles the source, stores the namespaces it uses as `requires`, and refuses a written list that disagrees. A `react` source is executed at render and never compiled at save, and `full` and `slotted` pages have no source. So on those three kinds nothing derived the key, the Studio page editor dropped it on every save, and its one reader was a load-time warning. `PageSchema` still accepted it there and never told the author it did nothing. The maintainer ruled that the key is accepted only on the compiled kinds. + + **What is refused.** `requires` on a page whose `kind` is `react`, `full` or `slotted`, or a page with no `kind`, at the `requires` path. An empty list is refused too, because the key is what is refused, not its contents. The issue's `code` is `custom`, and its message names the key, the page's kind and the compiled kinds. That covers `definePage()`, `PageSchema`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `pages.N.requires`), `os validate`, which runs the same stack parse, and the metadata save door (`422 INVALID_METADATA`). + + **What stays accepted.** `requires` on an `html` or `jsx` page, byte for byte. The save door still derives it, stores it, and refuses a written list that disagrees. Every page that omits `requires` parses as before, on every kind. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `requires: [...]` on a `kind: 'react'` page | nothing: delete the key. Nothing derived or enforced it | + | `requires: [...]` on a `kind: 'full'` or `kind: 'slotted'` page, or on a page with no `kind` | nothing: delete the key | + | `requires: [...]` on a `kind: 'html'` or `kind: 'jsx'` page | unchanged. The platform derives it from the source at save, so omitting it is still the intended authoring | + + **The one-line fix: delete `requires` from every page whose `kind` is not `html` or `jsx`.** `os migrate meta --from 17` lists the mechanical edits for existing sources. Stored pages and built artifacts are converted when they are read. + + **Who is affected, measured.** No page body authors `requires` on a `react`, `full` or `slotted` page in this repository at `c98a72d69e` (`examples/**`, `packages/apps/**`, `content/docs/**`, `skills/**`, tests and fixtures). Every `requires:` there is the stack-level capability list or an html page in a save-door test. The same holds in cloud (`c5a4c9e6cb`), hotcrm (`5ae524916d`) and objectui (`8366accd13`), per the ruling's census. Deployed metadata was not measured. + + ### The retirement kit + + - **The refusal.** `checkPageRequiresKind`, an exported object-level check attached to `PageSchema` beside `checkPageSourceCompleteness` (`@objectstack/spec/ui`), with `COMPILED_PAGE_KINDS` (`['html', 'jsx']`) as its vocabulary. A downstream mirror that derives its schema from `PageSchema.shape` re-attaches it with `.superRefine(checkPageRequiresKind)`. There is no tombstone and no `RETIRED_KEYS_BY_MAJOR` row, because the key stays live on html pages. + - **The conversion.** `page-requires-non-compiled-kind-removed` (protocol 18) deletes the key from `react`, `full`, `slotted` and kind-less pages. It is a lossless delete: on those kinds the list never had an effect. It is retired from the load path, so authored sources are refused at parse, while stored rows, built artifacts and `os migrate meta` replay it. Its D3 record is the semantic entry `page-requires-non-compiled-kind-refused`. + - **The ledgers.** The `requires` describe, its liveness row (`liveness/page.json`) and its form-reconciliation row now say the key exists only on html and jsx pages. +- 1289925: feat(spec)!: an `object-form` page block's `customFields` takes a closed runtime form field, and the `sections` of `object-form` and `object-master-detail-form` take a page-block section shape, instead of any value (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — two accept-set narrowings on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`object-form` `customFields` is a list of closed runtime form fields.** It was `z.unknown()`. Each member is the field the form merges over the fields it generates from the object's metadata and draws as written, and the spec now declares it: `name` (its identity), `label`, `description`, `type`, `inputType`, `widget`, `required`, `disabled`, `readonly`, `hidden`, `placeholder`, `options`, `validation`, `dependsOn`, `visibleWhen`, `readonlyWhen`, `requiredWhen`, `colSpan`, `span`, `group`, and the metadata a field widget reads off it — `multiple`, `rows`, `accept`, `dimensions`, `reference`, `min`, `max`, `minLength`, `maxLength`, `pattern`, `returnType`, `summaryOperations`, `columns`. Members this package already declares take that declaration by reference (the object field's own, the evaluated predicates); `label`, `description` and `placeholder` are plain strings. An `options` entry is the runtime option the form's option controls draw, closed: `label`, `value`, `description` (a lookup searches it), `visibleWhen` (the cascade offers the option only while it holds). Its `value` is a string, a number or a boolean, kept as written — an inline field binds no object column, so a stored field's lowercase-identifier rule does not apply, and `{ label: 'Box', value: 'Box' }` parses. + - **The `sections` of `object-form` and `object-master-detail-form` are one page-block section shape.** They were `z.array(z.unknown())`. A section takes the form view's section keys — `name`, `label`, `description`, `collapsible`, `collapsed`, `visibleWhen`, `columns`, `pane`, `group`, `fields` — and the form view's group-reference rule; each `fields` entry is a field name, the form view's `{ field, … }` entry, or an inline runtime form field (the `customFields` member). The stored form view's `FormSectionSchema` is unchanged. + - **Canonical spellings only.** A page block's `properties` is never parsed on the way to the form, so a form view's parse-time folds do not run there: a section `visibleOn` and a string `columns` reached the form raw and were dropped. Both are refused with the canonical spelling, and so is a `{ field }` entry's or an inline field's `visibleOn`. + - **Refused with what to write instead:** an inline field's legacy `condition`, its `defaultValue` (which seeds nothing), `id`, a `fields` member claim, the `grid` widget's eight snake_case keys (`min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label`, `sort_field` — they come in once the widget reads a camelCase spelling), a boolean `validation.required`, a `validation.pattern` / `validate` rule, an option's `color`, `default`, `disabled` or `icon` (no form option control reads them), and a locale map where the form draws a plain string. + - **`ObjectFormProps`, `ObjectMasterDetailFormProps`** and their parsed types carry the field and section types on these members instead of `unknown`; the shapes themselves are module-private. A bare CEL `visibleWhen` parses to its `{ dialect, source }` envelope, as on every evaluated slot, so the `object-form` row's input and parsed types now differ and it gains the one new export, the type `ObjectFormPropsParsed` (ADR-0122), as `ObjectMasterDetailFormPropsParsed` already is. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `customFields: [{ name: 'b', visibleOn: "record.a != ''" }]` | `customFields: [{ name: 'b', visibleWhen: "record.a != ''" }]` | + | `customFields: [{ name: 'b', condition: { field: 'a', equals: 'x' } }]` | `customFields: [{ name: 'b', visibleWhen: "record.a == 'x'" }]` (`notEquals` is `!=`, `in: [ … ]` is `record.a in [ … ]`) | + | `customFields: [{ name: 'memo', defaultValue: 'X' }]` | `customFields: [{ name: 'memo' }], initialValues: { memo: 'X' }` | + | `customFields: [{ name: 'a', validation: { required: true } }]` | `customFields: [{ name: 'a', required: true }]` (a string `validation.required` is the message) | + | `customFields: [{ name: 'a', validation: { pattern: { value, message } } }]` | `customFields: [{ name: 'a', pattern: '^[A-Z]+$' }]` | + | `customFields: [{ name: 'items', type: 'grid', min_rows: 1 }]` | `customFields: [{ name: 'items', type: 'grid', columns: [ … ] }]` — the widget's defaults until it reads a camelCase key | + | `customFields: [{ name: 'tier', type: 'select', options: [{ label: 'Gold', value: 'gold', default: true }] }]` | `customFields: [{ name: 'tier', type: 'select', options: [{ label: 'Gold', value: 'gold' }] }], initialValues: { tier: 'gold' }` | + | `options: [{ label: 'Gold', value: 'gold', color: '#d4af37' }]` on an inline field | `options: [{ label: 'Gold', value: 'gold' }]` — a colour belongs on the object field's own option | + | `sections: [{ fields: ['a'], visibleOn: 'record.b == 1' }]` | `sections: [{ fields: ['a'], visibleWhen: 'record.b == 1' }]` | + | `sections: [{ fields: ['a'], columns: '2' }]` | `sections: [{ fields: ['a'], columns: 2 }]` | + | `sections: [{ fields: [{ field: 'a', visibleOn: '…' }] }]` | `sections: [{ fields: [{ field: 'a', visibleWhen: '…' }] }]` | + | `sections: [{ label: { en: 'Basics' }, fields: ['a'] }]` | `sections: [{ name: 'basics', label: 'Basics', fields: ['a'] }]` — the heading translates through `objects.._sections.basics.label` | + + The one-line fix: write each inline field in camelCase with the members the form draws and each section in its canonical spelling. No conversion is registered: nothing on the load path refuses either shape, and the census below found no working value to respell — the D3 entries `ui-object-form-custom-fields-typed` and `ui-object-form-sections-typed` carry that judgment. + + ## Who is affected, measured + + A writer is a value written on the block: a page-component node (an object literal naming `object-form` or `object-master-detail-form`, flat or in its `properties` bag, or a literal annotated as one), a direct parse through the row, the block's React component inside `schema={{…}}`, the argument a local helper passes in that position at every same-file call site, or — the second pass — any object literal carrying `customFields` or `sections` in a file that names a form block. Values resolve through same-file constants and spreads, and through a `.map` over a constant list — the first run of this census read such a list as non-static and so never parsed the object manager's options; that miss is why an inline option's `value` is now a runtime value. Every static value was parsed through this branch's rows, and each value with a non-static part, and each refusal, was read by hand. + + - **objectstack** at `316be321ef`, this branch's merge base: three `object-form` `sections` writers (the showcase's new-project wizard, and one test each in `lint` and `spec`), field names only — all parse. No `customFields` writer. The other `sections` the second pass finds are form views and `record:details` sections, which these rows do not judge. + - **objectui** at the `.objectui-sha` pin `2e818d0b51ec` and at `main` `fd060f076` (every cited reader file is byte-identical between the two; `main` adds three test values, which parse): **`customFields`** — 31 values parse (four block literals; the designer's object manager, whose `icon` and `group` options `{ label: 'Box', value: 'Box' }`, `{ label: 'Custom Objects', value: 'Custom Objects' }`, … parse as runtime option values — typed as the form view's option, a stored field's lowercase identifier, both were refused; and 26 helper and embeddable-form arguments) and two are refused, both probes: a type-level test's `visibleOn` (never drawn) and the fixture pinning that an inline `defaultValue` seeds nothing; the 11 fully non-static values are run-time hand-offs and helper parameters, read by hand. **`sections`** — 102 `object-form` values with a static part parse (the field designer's inline fields and the plugin-form README's inline-field wizard among them), and so do four `object-master-detail-form` values. Two are refused, both probes of shapes objectui's own renderer test says this door refuses at parse: a section declaring neither `fields` nor `group`, and a group-owned `label` / `collapsible` beside `group`. The 26 fully non-static values are run-time hand-offs and helper parameters, read by hand: they use declared keys only, but for objectui's probe that a retired `className` / `gridClassName` reaches nothing. The second pass's other refused section values are `record:details`, detail-view or object-view form-slot sections, which these rows do not judge. + - **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no writer of either member. + - **Deployed metadata** was not measured. +- 958cfe2: feat(spec)!: four members of an `object-form` page block take the shape the form reads instead of any value — `contentLayout`, `submitBehavior`, `navigateOnSuccess` and `mobile` (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **Four members are typed.** `ComponentPropsMap['object-form']` declared `contentLayout`, `submitBehavior`, `navigateOnSuccess` and `mobile` as `z.unknown()`, although the form reads each with one shape. Any value passed, and an off-shape one was answered with a silent default: a `submitBehavior` whose `kind` the form does not know showed the thank-you panel; a misspelled `contentLayout` stacked the modal's sections; a `navigateOnSuccess` that is not a string failed the submit after the record had been written; a misspelled `mobile` member was ignored. + - **`submitBehavior` is the form view's own block, by reference** — `{ kind: 'thank-you', title?, message? }`, `{ kind: 'redirect', url, delayMs? }`, `{ kind: 'continue' }` or `{ kind: 'next-record' }` — with the same rule on a `redirect` `url` a form view carries: a relative path, interpolating declared record fields as `{{record.field_name}}`. + - **The measured shape, where no form view declares the member:** `contentLayout` is `'simple'` or `'tabbed'`; `navigateOnSuccess` is a relative path string (`{id}` / `{recordId}` interpolate the saved record's id); `mobile` is `{ stickyActions?, stepper?, stepperMinFields?, stepperFieldsPerStep?, fullscreenLongText? }`, with `stepper` `true`, `false` or `'auto'` and the two counts positive integers. + - **`ObjectFormProps`** carries these types on the four members instead of `unknown`. + - **The form's `fields` and `sections`, and the master-detail form's `fields` and `sections`, are not narrowed** and still accept any value. The form draws a top-level `fields` entry written as `{ name }` by that name, and it draws an inline runtime field (`{ name, type, … }`) written inside a section's `fields` as it stands — two shapes the typed members (field-name strings; the form view's section, whose field entry is keyed by `field`) would refuse. Each is held until that read is ruled. The master-detail form hands both members to its form unchanged, so they are held with the form's. + - **`customFields` is not narrowed either.** Its entries are the console's runtime form field (keyed by `name`), which the spec has not declared; it is typed once the spec declares it. + + ## FROM → TO + + | you wrote on an `object-form` | write instead | + |:--|:--| + | `submitBehavior: 'thank-you'` | `submitBehavior: { kind: 'thank-you' }` | + | `submitBehavior: { kind: 'toast' }` (any `kind` outside the four) | one of `thank-you`, `redirect`, `continue`, `next-record` | + | `submitBehavior: { kind: 'thank-you', heading: 'Done' }` | `{ kind: 'thank-you', title: 'Done' }` | + | `submitBehavior: { kind: 'redirect', url: 'https://app.example.com/done' }` | a relative path: `url: '/done'` | + | `contentLayout: 'tabs'` | `contentLayout: 'tabbed'` | + | `navigateOnSuccess: { url: '/orders/{id}' }` | `navigateOnSuccess: '/orders/{id}'`, or `submitBehavior: { kind: 'redirect', url: '/orders/{{record.id}}' }` | + | `mobile: { stepper: 'yes' }` | `mobile: { stepper: true }`, or `'auto'` for phone-width viewports only | + | `mobile: { stepperFieldsPerStep: 0 }` | delete the key (one field a step is the default), or a positive integer | + + The one-line fix: write each member as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the form shows today and honours what the author wrote; the D3 entry `ui-object-form-members-typed` carries that judgment. + + ## Who is affected, measured + + A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is read through same-file constants and local helpers. The control is `objectName` on the same nodes. + + - **objectstack** at `e909aa0a23`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 16 `object-form` nodes (the control on 13). Three values among the four members: the showcase's new-project wizard `submitBehavior` (a thank-you panel) and two copies of it in the lint and spec tests. All three parse. + - **objectui** at the `.objectui-sha` pin `89cad75d55`: 539 `object-form` nodes (the control on 522). Across the four members there are 73 values: 60 are static, and 56 of them parse. The 4 that do not are test fixtures of a protocol-relative redirect (`//example.com/thanks`), each asserting that the form refuses it and navigates nowhere. Of the 13 values that are not static, 9 are relative redirects that parse by inspection, and 4 are redirect fixtures the form refuses (three same-origin absolute URLs and one protocol-relative one). No refused value is one the form draws. + - **Deployed metadata** was not measured. +- ced3e1a: feat(spec)!: an `object-kanban` page block's `conditionalFormatting` takes the list view's own `[{ condition, style }]` rules instead of any value (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`object-kanban` `conditionalFormatting` is the list view's own member, by reference, as on `object-grid`.** It was `z.unknown()`, held while objectui's kanban also authored a native `{ field, operator, value, backgroundColor }` rule the list view refuses. objectui has since made the list view's `{ condition, style }` rule the member's only authoring dialect, and the board evaluates it with the evaluator the grid's rows use. So `42`, a bare string, a single rule outside a list or a rule with no `style` — values that passed and painted no card — are refused, and so are a blank `condition`, a non-string `style` value, the native rule, an `expression` rule and a colour written beside `condition` or `style`. + - **`ObjectKanbanProps`** carries the list view's rule type on `conditionalFormatting` instead of `unknown`. The member's string `condition` parses to the `{ dialect: 'cel', source }` envelope, exactly as it does on a list view and on `object-grid`. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `conditionalFormatting: [{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }]` | `conditionalFormatting: [{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }]` (`not_equals` is `!=`, `contains` is `.contains(…)`, `in` is `record.FIELD in [ … ]`) | + | `conditionalFormatting: [{ condition: "record.priority == 'high'", backgroundColor: '#fee2e2' }]` | `[{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }]` — every colour goes in `style` | + | `conditionalFormatting: [{ expression: "record.priority == 'high'", style: { color: 'red' } }]` | `[{ condition: "record.priority == 'high'", style: { color: 'red' } }]` | + | `conditionalFormatting: { condition, style }` (one rule, no list) | `conditionalFormatting: [{ condition, style }]` | + | `conditionalFormatting: [{ condition: '', style }]` | delete the rule — a blank condition matches no card | + + The one-line fix: write each rule as `{ condition, style }`, a CEL `condition` over the card's `record.*` and a CSS `style` map, the rule a list view declares. No conversion is registered: nothing on the load path refuses the shape, and the census below found no working rule to respell — the D3 entry `ui-object-kanban-conditional-formatting-typed` carries that judgment. + + ## Who is affected, measured + + A writer is a value written on the block: a page-component node (an object literal naming `object-kanban`, flat or in its `properties` bag, or a literal asserted as one), a direct parse through the row, the block's React component inside `schema={{…}}`, or the argument of a local test helper that mounts one. Values resolve through same-file constants and spreads, and parameters at every same-file call site. Each static value was parsed through the list view's member; a second pass parsed every rule-shaped object within 400 characters after a `conditionalFormatting` token, in any syntax, and each remaining hit was read by hand. + + - **objectstack** at `16d241a6af`, every tracked file: one writer, this package's own test that the key survived the `quickAdd` retirement, `[{ field: 'priority', value: 'high' }]` — no `operator`, so the board's evaluator built no predicate from it and painted nothing. Respelled to a `{ condition, style }` rule in the same change. + - **objectui** at the `.objectui-sha` pin `ab1879721595` and at `main` `2e818d0b51` (the readers are byte-identical between the two), every value a test fixture: nine `{ condition, style }` writers on the block (three through the board test's mount helper, one asserted node, one declared-keys row parsed through this very row, one live-member row, the dialect test's control, and the wire-slot test's string and envelope conditions), all parse. The ten refused values are refusal probes. Nine are refused by objectui's own faces too: the native rule, the flat colour rule, a colour beside `style` (three keys), an undeclared `label` and three malformed conditions. The tenth is objectui's probe that its mirror still admits a blank `condition`, which the board answers with no paint. Eight more rules mount the runtime `KanbanBoard` directly rather than the block, and the view-face relays carry a list view's rules; all of them parse. + - **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no `conditionalFormatting` at all (controls: `kanban` in 48 and 35 files). + - **Deployed metadata** was not measured. +- 7d674df: feat(spec)!: eight list members of an `object-grid`, `object-kanban` or `object-calendar` page block take the shape the block reads instead of any value — the grid's `fields`, `selection`, `selectable`, `rowActions`, `bulkActions` and `batchActions`, the kanban's `columns` and the calendar's `calendar` (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **Eight members are typed.** `ComponentPropsMap['object-grid']`, `['object-kanban']` and `['object-calendar']` declared these members as `z.unknown()` (an array of it for the lists), although each renderer reads them with one shape. Any value passed, and an off-shape one was dropped or substituted with no report: an object entry in `fields` named no field; a `{ name }` entry in `bulkActions` was skipped; a kanban lane list mixing objects and strings drew a blank lane; a calendar block with no `startDateField` placed no event. + - **The list view's own members, by reference**, where a list view declares one: the grid's `selection` (`{ type }`, with `none`, `single` or `multiple`), `rowActions` and `bulkActions` (action-name strings), and the calendar's `calendar` (`{ startDateField, endDateField?, titleField?, colorField?, allDayField? }`). `batchActions`, the second spelling of `bulkActions` that the grid reads first, takes `bulkActions`'s def. Neither spelling is retired here. + - **The measured shape**, where no list view declares the member: the grid's `fields` (field-name strings), the grid's `selectable` (`true`, `false`, `'single'` or `'multiple'`), and the kanban's `columns` (all lanes `{ id, title, cards?, limit?, className?, collapsed? }`, or all bare value strings). A lane `id` and `title` are strings, a static card carries a string `id` and `title` beside its row's own values, and `limit` is a positive integer. + - **`ObjectGridProps`, `ObjectKanbanProps` and `ObjectCalendarProps`** (and their `…Parsed` twins) carry these types on the eight members instead of `unknown`. + - **The grid's `columns` is not narrowed** and still accepts any value. The list view's column entry is its by-reference shape, and the grid's draw path reads exactly that, but the grid's group headers also draw the labels from an authored column's `options` (the column whose `field` is the grouping field, ahead of the field's own options). The list view's column entry declares no `options`, so typing `columns` now would refuse a value the grid draws. It is held until that read is ruled. + - **The enumeration pin** loses eight lines and keeps the grid's `columns` as held for that ruling. One `z.unknown()` member is added and recorded: the rest of a static kanban card (its row's own values, beside the typed `id` and `title`). + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `object-grid` `fields: [{ field: 'name', width: 240 }]` | `fields: ['name']`, or the entry on `columns` | + | `object-grid` `selection: 'multiple'` | `selection: { type: 'multiple' }` | + | `object-grid` `selectable: 'none'` | `selectable: false`, or `selection: { type: 'none' }` | + | `object-grid` `bulkActions: [{ name: 'approve' }]` (also `batchActions`, `rowActions`) | `bulkActions: ['approve']`, or the full def on `bulkActionDefs` | + | `object-kanban` `columns: [{ id: 'done', title: 'Done' }, 'todo']` | one spelling per list: `columns: [{ id: 'done', title: 'Done' }, { id: 'todo', title: 'To Do' }]` | + | `object-kanban` a lane `color: 'red'` | `className: 'border-t-2 border-red-500'` | + | `object-kanban` a lane `{ id: 1, title: 'One' }` | `{ id: '1', title: 'One' }` | + | `object-calendar` `calendar: { dateField: 'kickoff', endField: 'wrapup' }` | `calendar: { startDateField: 'kickoff', endDateField: 'wrapup' }` | + + The one-line fix: write each member as the list view declares it, or as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entry `ui-object-grid-kanban-calendar-list-members-typed` carries that judgment. + + ## Who is affected, measured + + A writer is a page-component node: an object literal naming the type, a literal annotated with the block's type, a `schema={{…}}` on the block's React component, a call into a local helper that builds the node, or a direct parse through the row. Each member's value is read through same-file constants, and the control is `objectName` on the same nodes. + + - **objectstack** at `49161683fb`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 57 `object-grid`, 30 `object-kanban` and 5 `object-calendar` nodes (the control on 47 / 27 / 4 of them). The one authored value among the eight members is a kanban `columns` (lanes, in the protocol docs), and it parses. No node authors another of the eight. + - **objectui** at the `.objectui-sha` pin `89cad75d55`: 689 `object-grid`, 240 `object-kanban` and 160 `object-calendar` nodes (the control on 293 / 108 / 98). Across the eight members, 241 values are static, and 233 of them parse. Each of the 8 that do not is a test fixture whose value the renderer drops, skips or refuses: 2 object entries in `bulkActions` (the renderer skips them, and the tests assert the skip), 3 object entries in `fields` that copy the hand-off the list view makes to the grid at run time (not an authored page), a lane `color` (retired in the console; the test marks it an undeclared member), and 2 uses of the calendar's retired `dateField` / `endField` aliases (the test asserts their refusal). No refused value is one the renderer draws. 24 values are not static (helper parameters, `.map` results and the run-time hand-offs); none of them is an authored page. The grid's `columns` (310 static values) is held because 2 of them author a column `options` the grid draws in its group headers, a fixture written to pin that behaviour. + - **Deployed metadata** was not measured. +- 3f1bc81: feat(spec)!: an `object-metric` page block's `aggregate` and `trend` take the shape the tile reads instead of any value (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **Two members are typed.** `ComponentPropsMap['object-metric']` declared `aggregate` and `trend` as `z.unknown()`, although the tile reads each with one shape. Any value passed, and an off-shape one was answered in silence: `aggregate: 'count'` or a function the engine does not have asked the server for a measure it does not have, so the tile showed an error or, on the client-side fallback, a sum it was not asked for; `groupby` for `groupBy` drew one ungrouped number; a `trend` with no `value` painted a lone `%`, and a misspelled member or direction was not drawn. + - **`aggregate` takes its vocabulary by reference** — `{ field?, function, groupBy? }`: `function` is the query engine's own `AggregationFunction` (`count`, `sum`, `avg`, `min`, `max` or `count_distinct` — the six the tile forwards to the data source), with a `field` for every function but `count`; `groupBy` is the chart aggregate's own `ChartGroupBySchema`, a field name or a `{ field, dateGranularity?, alias? }` date-bucket node, and here it is optional, because a metric paints one number over every row. The chart's aggregate is not taken whole: it requires `groupBy`, and its five functions leave out the `count_distinct` the tile draws. + - **`trend` takes the badge's measured shape**, `{ value, label?, direction? }`: `value` a number (painted as a percentage), `label` a string or an inline locale map, `direction` `up`, `down` or `neutral`. + - **`ObjectMetricProps`** carries these types on the two members instead of `unknown`. + - **`drillDown` and `compareTo` are not narrowed** and still accept any value. Each by-reference candidate disagrees with what the tile reads: the chart's drill-down declares a `filter` the tile never reads and refuses the `report` the tile draws as a report body; the dashboard widget's comparison declares a `dimension` that this path never reads. Each is typed once that fork is ruled. + + ## FROM → TO + + | you wrote on an `object-metric` | write instead | + |:--|:--| + | `aggregate: 'count'` | `aggregate: { function: 'count' }` | + | `aggregate: { function: 'sum' }` | name the field: `aggregate: { field: 'amount', function: 'sum' }` | + | `aggregate: { field: 'amount', function: 'median' }` (any function outside the six) | one of `count`, `sum`, `avg`, `min`, `max`, `count_distinct` | + | `aggregate: { field: 'amount', function: 'sum', groupby: 'stage' }` | `groupBy: 'stage'` | + | `aggregate: { function: 'count', dateGranularity: 'month' }` | `aggregate: { function: 'count', groupBy: { field: 'closed_at', dateGranularity: 'month' } }` | + | `trend: 'up'` | `trend: { value: 12, direction: 'up' }` | + | `trend: { value: '12%' }` | `trend: { value: 12 }` (the badge adds the `%`) | + | `trend: { value: 12, direction: 'rising' }` | `direction: 'up'` | + + The one-line fix: write each member as the table above shows. No conversion is registered, because an off-shape value has no rewrite that both keeps what the tile shows today and honours what the author wrote; the D3 entry `ui-object-metric-aggregate-trend-typed` carries that judgment. + + ## Who is affected, measured + + A writer is a value written on an `object-metric`: a page-component node (an object literal naming the type, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one. Values resolve through same-file constants; helper arguments and `it.each` rows were resolved by hand. Each value was parsed through the built row. + + - **objectstack** at `b610eabf72`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/` and `apps/`: 22 `aggregate` values, all `{ field, function }` with `count` or `sum` — the showcase's thirteen KPI tiles (`index.ts`, `my-work.page.ts` and the `kpi()` helper in `command-center.page.ts`), two in the layout-DSL protocol page and six copies in the spec and lint tests. 21 parse. The 22nd is the `kind: 'html'` example in `skills/objectstack-ui/rules/pages.md`, `aggregate="count"`, which this row does not judge (the html tier is compiled against the component manifest, not parsed through `ComponentPropsMap`). No `trend` is authored. + - **objectui** at the `.objectui-sha` pin `89cad75d55`: 66 `aggregate` values (64 static) and 12 `trend` values (all static). 63 and 11 parse. The two refused values are test fixtures probing that the tile does not draw them: an array `groupBy` the data adapter refuses at the producer (`objectMetricStructuredGroupBy-8613`), and a `trend` carrying `percent` and `caption`, which the test asserts the badge never draws (`objectMetricTrendMembers-8071`). The two values that are not static are the dashboard relays' run-time hand-offs (`DashboardRenderer`, `DashboardGridLayout`). objectui's unit-rule pin mounts a `count_distinct` tile, which parses. + - **Deployed metadata** was not measured. +- 72f3c74: feat(spec)!: an `object-metric` page block's `drillDown` and `compareTo`, and an `object-grid` page block's `columns`, take the shape each block reads instead of any value (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`object-metric` `compareTo` takes the tile's read, `{ kind }`.** It was `z.unknown()`, although the tile reads `kind` alone: a bare `'previousYear'` or a kind outside the two compared against the previous period, and a `dimension` was carried and never read. `kind` is the dashboard widget comparison's own vocabulary by reference (`previousPeriod`, `previousYear`). `dimension` is refused by name, with the prescription: this inline tile shifts the date macros in its own `filter` and never reads a dataset time dimension, so state the window on the tile's `filter`. + - **`object-metric` `drillDown` takes the tile's read.** It was `z.unknown()`: a drill `filter`, a `mode` or a misspelled member passed and was ignored. Its five list members — `enabled`, `title`, `target` (`drawer`, `dialog`, `navigate`), `columns` (field names) and `maxRows` (a positive whole number) — are the chart drill-down's own by reference. `filter` and `mode` are refused by name: a metric tile has no click event for a drill filter to resolve against (the drilled list is scoped by the metric's own `filter`), and no row for `mode` to open as a record. The chart's drill-down shape is not taken whole, because it declares `filter`. + - **The drill-down's `report` is not narrowed** and still accepts any value. The tile draws a dataset-bound report through the shared drill drawer, but no spec drill shape declares a `report` member yet; it is typed once the spec declares the drill report. + - **`object-grid` `columns` takes the list view's own `columns`**: all field-name strings, or all column entries `{ field, label?, width?, align?, hidden?, sortable?, resizable?, wrap?, type?, pinned?, summary?, prefix?, link?, action? }`. It was an array of `z.unknown()`, held at the second stage because the grid's group headers drew a column's `options`, which the column entry does not declare. The renderer has since retired that read (the group-header labels come from the object field's `options` only), so the hold is lifted. A column keyed `accessorKey` / `header` or `name`, a column with no `field`, a list mixing strings and entries, or a column key the entry does not declare (`editable`, `options`, `reference`, or a footer number hint such as `currency` or `precision`) is refused. + - **`ObjectMetricProps` and `ObjectGridProps`** carry these types on the three members instead of `unknown`. A parsed column's `prefix.type` now carries the list view's `'text'` default. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `object-metric` `compareTo: 'previousYear'` | `compareTo: { kind: 'previousYear' }` | + | `object-metric` `compareTo: { kind: 'previousYear', dimension: 'close_date' }` | `compareTo: { kind: 'previousYear' }`, with the window stated on the tile's own `filter` (date macros such as `{current_quarter_start}`) | + | `object-metric` `compareTo: { kind: 'previousQuarter' }` | `previousPeriod` (the equal-length window before the one the filter resolves to) or `previousYear` | + | `object-metric` `drillDown: { filter: { stage: 'won' } }` | delete it, and scope the metric with its own `filter` (the drilled list follows it) | + | `object-metric` `drillDown: { mode: 'record' }` | delete it — a metric always lists the records behind its number | + | `object-metric` `drillDown: { limit: 50 }` | `drillDown: { maxRows: 50 }` | + | `object-grid` `columns: [{ accessorKey: 'amount', header: 'Amount' }]` | `columns: [{ field: 'amount', label: 'Amount' }]` | + | `object-grid` `columns: [{ name: 'salary' }]` | `columns: [{ field: 'salary' }]` | + | `object-grid` `columns: ['name', { field: 'amount', width: 120 }]` | all entries: `[{ field: 'name' }, { field: 'amount', width: 120 }]` | + | `object-grid` a column `editable`, `options`, `reference`, `currency` or `precision` | delete the key: inline editing is the grid's own `editable`, and option labels, relational metadata and number formats are the object field's | + + The one-line fix: write each member as the table above shows. No conversion is registered, because a refused value has no rewrite that both keeps what the block shows today and honours what the author wrote; the D3 entries `ui-object-metric-compare-to-typed`, `ui-object-metric-drill-down-typed` and `ui-object-grid-columns-typed` carry that judgment. + + ## Who is affected, measured + + A writer is a value written on the block: a page-component node (an object literal naming the type, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one (positional helper parameters resolved at every call site). Values resolve through same-file constants. Each static value was parsed through the row. + + - **objectstack** at `6ec54f00ba`, over `examples/`, `packages/` (with `packages/apps/`), `content/`, `skills/`, `apps/` and `docs/`: 5 `object-grid` `columns` values, all field-name strings (the showcase's `my-work.page.ts` and `command-center.page.ts` grids, and three test copies), all parse. No `object-metric` `drillDown` or `compareTo` is authored. + - **objectui** at the `.objectui-sha` pin `ab1879721595`, every one a test fixture or a run-time hand-off: + - `compareTo`: 5 values, 4 parse. The refused one is the test that asserts a `dimension` never touches the query (`ObjectMetricWidget.compareTo.test.tsx`). + - `drillDown`: 26 values, 25 static; 23 parse. The two refused are the compile-time refusal probes for `filter` and `mode` (`ObjectMetricWidget.drillDownRefusal-9002.test.tsx`). The one that is not static carries a report probe, which parses, since `report` stays open. + - `object-grid` `columns`: 362 values, 353 static (147 distinct); 312 parse. Each of the 41 refused is a test fixture whose refused key or entry the grid does not draw: 17 columns keyed `accessorKey` / `header` and 5 keyed `name` (the column-spelling diagnostic, identity and field-security tests); 14 columns carrying `editable: false` and 1 carrying `reference`, keys no read takes off an authored column; 2 carrying `options`, the tests asserting that the group headers no longer read them; 1 column with no `field`; and 1 numeric `columns` refused by objectui's own mirror. The 9 values that are not static are 3 run-time hand-offs (the object view and two designer grids) and 6 test lists built from `{ field, label, type }` entries, which parse. + - **Deployed metadata** was not measured. +- 529d971: feat(spec)!: `navigation` on an `object-map`, `object-gantt` or `object-tree` page block takes the list view's navigation block instead of any value, and every remaining `z.unknown()` member of `ComponentPropsMap` is enumerated with its recorded reason (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the row: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`navigation` is typed on three rows.** `ComponentPropsMap['object-map']`, `['object-gantt']` and `['object-tree']` declared `navigation` as `z.unknown()`, although each renderer hands it to the console's shared navigation hook, which reads `navigation.mode` and falls back to `page` when it finds none. Any value passed, and an off-shape one was answered with a silent default: `navigation: 42` and a bare mode string such as `'drawer'` both opened the record page, whatever they named. Each row now takes the list view's `NavigationConfigSchema` by reference, the same block `object-grid`, `object-kanban`, `object-calendar` and `object-timeline` already take: `{ mode?, size?, openNewTab?, preventNavigation? }`, with `mode` one of `page`, `drawer`, `modal`, `split`, `popover`, `new_window` or `none`. + - **`ObjectMapProps`, `ObjectGanttProps` and `ObjectTreeProps`** (and their `…Parsed` twins) carry `NavigationConfig` on `navigation` instead of `unknown`. + - **No other member changes.** Every other `z.unknown()` member across `ComponentPropsMap` (107 of them) is now listed, with its recorded reason, by a test that fails on a new one until it carries one. The reasons are composition slots, the action blocks' runner-forwarded members, record rows and field values, members of schemas another file owns, a value shown as-is, a deliberately open bag, and a row no renderer draws. The list also holds 28 members a renderer reads with a fixed shape. 27 of them are typed in later changes, and one, `object-kanban`'s `conditionalFormatting`, waits for a ruling, because the console's own kanban fixtures author two rule dialects the list view's schema refuses. + + ## FROM → TO + + | you wrote on an `object-map` / `object-gantt` / `object-tree` | write instead | + |:--|:--| + | `navigation: 'drawer'` | `navigation: { mode: 'drawer' }` | + | `navigation: { mode: 'tab' }` | a mode the hook knows: `page`, `drawer`, `modal`, `split`, `popover`, `new_window` or `none` | + | `navigation: { mode: 'drawer', target: '_blank' }` | `navigation: { mode: 'new_window' }`, or `openNewTab: true` beside a `page` mode | + + The one-line fix: write `navigation` as the block a list view declares, `{ mode, size?, openNewTab?, preventNavigation? }`. No conversion is registered, because an off-shape value has no rewrite that both keeps what the block shows today (the record page) and honours what the author wrote; the D3 entry `ui-object-map-gantt-tree-navigation-typed` carries that judgment. + + ## Who is affected, measured + + - **objectstack.** Measured on this branch after merging `origin/main` `100c394f6f`, over the 5 files per row that name `object-map` / `object-gantt` / `object-tree` in the examples, `packages/apps`, `@objectstack/platform-objects`, the plugins and services, the spec sources, the documentation and the published skills: no block of the three authors `navigation`. The one file that co-mentions a row and a `navigation:` key writes app navigation arrays, not this member. The control: the same census finds `objectName` in 4 of the 5 files per row. + - **objectui.** Measured at the `.objectui-sha` pin, over the 88 / 98 / 59 files that name `object-map` / `object-gantt` / `object-tree` (the control: `objectName` in 55 / 70 / 40 of them): every authored `navigation` is `{ mode }` with one of the seven modes, some with `size: 'lg'` or `openNewTab`, and each of those parses on all three rows. The non-object values are probes that expect a refusal: `navigation: 'anything'` in objectui's mirror tests, which assert that both faces answer alike, and a `navigation: 'drawer'` under a `@ts-expect-error`. Neither authors anything. + - **Deployed metadata** was not measured. +- 16d241a: feat(spec)!: an `object-gantt` page block's `markers`, an `object-timeline` page block's `mapping`, and the top-level `fields` of the `object-form` and `object-master-detail-form` page blocks take the shape each block reads instead of any value (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`object-gantt` `markers` takes `{ date, label?, color? }` entries.** Its entries were `z.unknown()`, because the marker contract lived only in objectui: a marker with no `date`, a numeric `date` or a misspelled member passed, and the chart drew no line, or drew it with no label and in the default colour. The spec now declares objectui's own authoring declaration of a marker — `date` an ISO date or date-time string, `label` the text drawn against the line, `color` any CSS colour — closed, and the row takes it. A marker `title`, `text` or `name` is pointed at `label`, and a `colour` at `color`. + - **`object-timeline` `mapping` takes `{ title?, date?, description?, variant? }`**, each a field name. It was `z.unknown()`, for the same reason: a bare field name, a non-string binding or a misspelled member (`titleField` inside `mapping`) passed, and the rail drew the default field. The spec now declares objectui's own declaration of the binding record, closed. `titleField`, `dateField` / `startDateField`, `descriptionField` and `variantField` written inside `mapping` are pointed at the member they meant. + - **`object-form` and `object-master-detail-form` `fields` take field names.** The top-level list was an array of `z.unknown()`, held while the form drew a `{ name }` entry its page-builder guide taught, with a `label`, `type` and `required` it silently dropped. objectui has since retired that entry from every authoring face (the form still draws a stored one by its name), so both rows take field-name strings, objectui's own declaration of the member. A `{ name: 'email' }` entry is refused with `write 'email'` and where a per-form override goes; a `{ field: 'email' }` entry — the `sections[].fields` vocabulary, which the form skips at the top level — is refused with the same name and that pointer. + - **Not narrowed, and still accepting any value:** the `object-metric` drill-down's `report`, `object-form` `customFields`, both forms' `sections`, `object-timeline` `items` and the members of `action:group` / `action:menu`. Each contract still lives in objectui and has more than one viable spec shape that no ruling decides yet; each is typed once one is chosen. + - **`ObjectGanttProps`, `ObjectTimelineProps`, `ObjectFormProps` and `ObjectMasterDetailFormProps`** carry these types on the four members instead of `unknown`. No new member carries a default, so each parsed value is the authored one. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `object-gantt` `markers: [{ date: 5 }]` | `markers: [{ date: '2026-07-01' }]` — an ISO date or date-time string | + | `object-gantt` `markers: [{ label: 'Freeze' }]` | give it a `date`: `[{ date: '2026-07-01', label: 'Freeze' }]` | + | `object-gantt` `markers: [{ date: '2026-07-01', title: 'Freeze', colour: 'red' }]` | `[{ date: '2026-07-01', label: 'Freeze', color: 'red' }]` | + | `object-timeline` `mapping: 'subject'` | `mapping: { title: 'subject' }` — name the member the field binds | + | `object-timeline` `mapping: { titleField: 'subject', variantField: 'status' }` | `mapping: { title: 'subject', variant: 'status' }` | + | `object-form` `fields: [{ name: 'email', label: 'Email', required: true }]` | `fields: ['email']`, with the label and `required` on the object field or on a `sections[].fields` entry | + | `object-form` `fields: [{ field: 'email' }]` | `fields: ['email']`, or move the entry into a section's `fields` | + | `object-master-detail-form` `fields: [{ name: 'note' }, 'status']` | `fields: ['note', 'status']` | + + The one-line fix: write each member as the table above shows. No conversion is registered: a misspelled marker or mapping member has no rewrite that says which member the author meant, and a form already draws a stored `{ name }` entry by its name, while an override written beside it has nowhere to go but a section — the D3 entries `ui-object-gantt-markers-typed`, `ui-object-timeline-mapping-typed` and `ui-object-form-fields-names-typed` carry that judgment. + + ## Who is affected, measured + + A writer is a value written on the block: a page-component node (an object literal naming the type, flat or in its `properties` bag, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside `schema={{…}}`, or the argument of a local test helper that mounts one (positional helper parameters resolved at every call site). Values resolve through same-file constants. Each static value was parsed through the row; a text search for each member key beside the block's name found the writers the walk does not reach, and each was read by hand. + + - **objectstack** at `7d0781482d`, over `examples/`, `packages/`, `content/`, `skills/`, `apps/` and `docs/`: no `markers` and no `object-form` `fields`; one `mapping` (this package's own navigation test, `{ title, variant }`) and four `object-master-detail-form` `fields` (the showcase's project workspace, the objectui layout DSL page, and two test copies), all field names. All parse. + - **objectui** at the `.objectui-sha` pin `ab1879721595` and at `main` `94985a92ba` (every read point identical between the two), every value a test fixture, a document or a run-time hand-off: + - `markers`: 9 values, 8 parse. The refused one is objectui's own compile-time probe that a numeric `date` is refused (`gantt-declared-keys.test.ts`). Five more mount `GanttView`, the runtime chart, directly rather than the block, and are not writers of this member. + - `mapping`: 9 values (the timeline inputs test and the absent-date-axis refusal test), all parse. + - `fields`, both forms: 73 values at `main` — 56 parse, 11 are run-time hand-offs that are not static, and the 6 refused are fixtures probing the read: three `{ field }` entries asserting the form skips them with a warning, a `{ name }` entry asserting objectui's own mirror refuses it, and two `{ name }` entries asserting a stored one still draws. At the pin a seventh is refused: the page-builder guide's `{ name, label, type, required }` example, respelled to names on objectui `main`. (Fourteen more matches are object definitions or permission maps whose own `fields` key the walk read as the block's, and are not writers.) + - **hotcrm** at `4054ec2680` and **cloud** at `b2d7a7f6f8`: no writer of any of the four members. + - **Deployed metadata** was not measured. +- 4331a6b: feat(spec)!: an `object-metric` drill-down's `report` takes a report definition, an `object-timeline`'s `items` take the entry kind its `variant` selects, and each `action:group` / `action:menu` member takes a closed inline action, instead of any value (#21464) + + Clause-②: yes (narrowing) + + + + **BREAKING** — three accept-set narrowings on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **`@objectstack/spec`** + + - **`object-metric` `drillDown.report` is a report definition — `ReportSchema`, by reference.** It was `z.unknown()`. The tile hands it to the shared drill drawer, which draws a dataset-bound report (with the metric's filter joined into the report's own `runtimeFilter`) and lists the records for any other value. A joined report already refuses a block that binds no `dataset`, so every report the member admits is one the drawer draws; a report with no `dataset`, a bare report name, a `{ name }` reference or the retired `objectName` / `columns` form is refused. The drawer still draws a few incomplete reports the member refuses (no `name` / `label`, a non-joined report with no `values`, a joined one with a container `dataset` or with only some blocks bound) — no measured writer authors one. + - **`object-timeline` `items` takes the entry kind the block's `variant` selects.** It was `z.array(z.unknown())`. On `vertical` (the default) or `horizontal` an entry is a feed entry `{ time?, title, description?, variant?, icon?, content?, className? }`; on `gantt` it is a gantt row `{ label, items? }` whose bars are `{ title?, startDate?, endDate?, variant? }`, each date a string or epoch milliseconds — objectui's two ruled element kinds, closed. A row refinement pairs each entry with its kind: a feed entry with no `title`, a gantt row with no `label`, and a key of the other kind are refused at the entry, by path. `variant` is one of `default`, `success`, `warning`, `danger`, `info`. A feed entry's `content` (child components) is not judged yet. The keys the record-bound rail composes onto its entries (`color`, `startDate`, `endDate`, `group`, `meta`) are refused on an authored entry with what to write instead. + - **Each `action:group` / `action:menu` member is a closed inline action.** It was an open record. A member takes `action:button`'s keys with its executor spelled `type` (a member is an action entry): `name`, `label`, `icon`, `type`, `variant`, `visible`, `disabled`, `tags`, `params`, `description`, `target`, `openIn`, `method`, `bodyExtra`, `bodyShape`, `operation`, `patch`, `confirmText`, `successMessage`, `errorMessage`, `refreshAfter`, `locations`, `toast`, `resultDialog`, `onSuccess`, `objectName` — and `size` on an `action:group` member, whose inline button reads it (an `action:menu` item reads none). `tags` takes `separator-before`, the one tag the containers draw. Refused, each with what to write instead: `actionType` (write `type`), `endpoint` / `url` / `path` / `href` (write `target`), `enabled` (write `disabled`, inverted), `autoTrigger`, `outcomeMessages` (write `successMessage`), a member `className`, a member `properties` bag, `undoable` and `recordIdField`. `outcomeMessages` stays undeclared on all four action blocks (`action:button`, `action:icon`, `action:group`, `action:menu`). + - **`ObjectMetricProps`, `ObjectTimelineProps`, `ActionGroupProps`, `ActionMenuProps`** and their parsed types carry these shapes instead of `unknown`; the member and entry shapes are module-private. `ObjectMetricPropsParsed` now also differs from the authored type on `drillDown.report`, whose `ReportSchema` defaults (`type`, `drilldown`) materialize on parse. No export is added or removed. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `drillDown: { report: 'pipeline' }` or `{ report: { name: 'pipeline' } }` | `drillDown: { report: { name: 'pipeline', label: 'Pipeline', dataset: 'deals_ds', values: ['amount_sum'] } }` | + | `drillDown: { report: { name, label, objectName: 'deal', columns: [ … ] } }` | the dataset-bound report: `{ name, label, dataset, rows, values }` | + | `drillDown: { report: { …, type: 'joined', blocks: [{ name: 'notes' }] } }` | bind every block: `blocks: [{ name: 'notes', dataset: 'notes_ds', values: [ … ] }]` | + | `items: [{ date: '2026-01-15', title: 'Kickoff' }]` | `items: [{ time: '2026-01-15', title: 'Kickoff' }]` | + | `items: [{ title: 'Kickoff', color: 'green' }]` | `items: [{ title: 'Kickoff', variant: 'success' }]` | + | `items: [{ label: 'Backend', items: [ … ] }]` with no `variant` | `variant: 'gantt', items: [{ label: 'Backend', items: [ … ] }]` | + | `variant: 'gantt', items: [{ title: 'Kickoff' }]` | `variant: 'gantt', items: [{ label: 'Kickoff', items: [{ startDate, endDate }] }]`, or drop `variant: 'gantt'` | + | `actions: [{ name: 'go', actionType: 'url', target: '/x' }]` | `actions: [{ name: 'go', type: 'url', target: '/x' }]` | + | `actions: [{ name: 'save', type: 'api', endpoint: '/api/save' }]` | `actions: [{ name: 'save', type: 'api', target: '/api/save' }]` | + | `actions: [{ name: 'del', outcomeMessages: { archived: 'Archived' } }]` | `actions: [{ name: 'del', successMessage: 'Archived' }]` | + | `actions: [{ name: 'del', className: 'text-red-600' }]` | `actions: [{ name: 'del', variant: 'destructive' }]` | + | `actions: [{ name: 'run', enabled: "record.status == 'open'" }]` | `actions: [{ name: 'run', disabled: "record.status != 'open'" }]` | + | `actions: [{ name: 'edit', properties: { params: { … } } }]` on `action:group` / `action:menu` | `bodyExtra` for a `type: 'api'` request body, or the action as its own `action:button` node with a `params` object | + | `action:menu` `actions: [{ name: 'a', size: 'sm' }]` | `actions: [{ name: 'a' }]` — the menu's own `size` sizes the trigger | + + The one-line fix: write a drill report as the report definition, each timeline entry as the kind the block's `variant` draws, and each container member with `action:button`'s keys and `type` as its executor. No conversion is registered: nothing on the load path refuses these shapes, and the census below found no working value to respell — the D3 entries `ui-object-metric-drill-down-report-typed`, `ui-object-timeline-items-typed` and `ui-action-group-menu-members-typed` carry that judgment. + + ## Who is affected, measured + + A writer is a value written on the block: a page-component node (an object literal naming the block, flat or in its `properties` bag, or with its `type` arriving through a spread constant), a literal annotated as one, a direct parse through the row, the block's React component (`schema={…}`, or its props), and the argument a local helper passes in that position at every same-file call site. Values resolve through same-file constants and spreads, a `.map` over a constant list, templates and same-file helper calls (`member('alpha', { size })`). Every static value was parsed through this branch's rows; each value with a non-static part, and each refusal, was read by hand. + + - **objectstack** at `1289925c0a`, this branch's merge base: one drill report (the metric pin's own), one timeline `items` (a spec test, a feed entry) and three `action:group` / `action:menu` members (a spec test) — all parse. No example, doc or skill writes any of the three. + - **objectui** at the `.objectui-sha` pin `2e818d0b51ec` and at `main` `2abec3a96` (every cited reader byte-identical between the two, and the same census at both): **drill report** — the drawn report drill (`objectMetricDrillDownMembers-8071.test.tsx:281`) parses; refused are only the probes of what the drawer does NOT draw (that file's two `it.each` values, and the `@object-ui/types` drill-mirror tests' `{ name }` / incomplete-report refusal probes). **Timeline `items`** — 18 values: 15 parse (feed entries and gantt rows); the 3 refused are the render-time gantt date diagnostic's own probes (an array, `false` and `null` bar date). **Members** — `action:group` 40 values and `action:menu` 19, all in tests but for one run-time hand-off: every static member parses except the probes of the very reads the ruling refuses — the member pin's `className` (`action-group-menu-inputs-11168.test.tsx:249`), the `outcomeMessages` forward tests, the `properties.params` static-value tests, and the host's `autoTrigger` flag in the overflow / forward tests. The hand-off is `action:bar`'s overflow menu (`action-bar.tsx:287`), which hands the bar's own members — a host's registered actions — to `action:menu` at run time, never through the component-props gate. + - **hotcrm** at `4054ec2680` and **cloud** at `2205b53010`: no writer of any of the three (hotcrm's four `object-metric` tiles declare no drill-down). + - **Deployed metadata** was not measured. +- 6c5697d: fix(runtime,cloud-connection)!: a job's sandboxed `body` is scheduled on every door that brings an artifact in, and install-local refuses an enabled job with no `body` (#21489) + + Clause-②: yes (narrowing) + + + + **BREAKING**: `os package install` (the install-local door, `POST /api/v1/marketplace/install-local`) now refuses a package that declares an **enabled job with no `body`**. Such a job names its code only through `handler` — a `defineStack({ functions })` entry, which travels in the artifact's runtime module and never in the package JSON this door installs — so it used to install with a 200 and never run, hot or after a restart, with nothing saying so. + + - **Job bodies run.** A job's sandboxed `body` (`JobSchema.body`, the hook body shape) is now scheduled on every door that brings an artifact in: the boot (`os start --artifact`, a `defineStack` config) and install-local, on install and on every rehydrate after a restart. One binder does it for all of them. With both `body` and `handler` declared, the `body` wins. The body runs in the QuickJS sandbox with `ctx.api` (as system: a job has no caller), `ctx.log` and `ctx.crypto` behind its declared `capabilities`. The job's `timeoutMs` is its one time limit; with none, a job body gets a 5000 ms CPU budget. A body may return `{ outcome: 'degraded', reason }` to report a run that did not do its work. + - **A package's jobs stop with it.** Re-scheduling a package's jobs replaces its set: a reinstall whose new version drops, disables or can no longer run a job cancels that job, and a version with no jobs cancels them all. Uninstalling a package cancels its scheduled jobs through a new uninstall cleanup, `runtime.package-jobs`, on the protocol's uninstall-cleanup registry, so install-local's `DELETE` and the protocol's package uninstall both stop them and report it in `cleanups`. Another package's jobs are never touched. + - **The refusal.** The install answers `422` with `VALIDATION_ERROR`, names each refused job and the function its `handler` declares, and installs nothing: nothing is registered, persisted or scheduled. A disabled job (`enabled: false`) is not judged. A package installed by an earlier version keeps rehydrating; its handler-only job is reported at `warn` and does not run. + - **CLI.** `os package install` prints a refusal's code beside its status (`Install failed (422 VALIDATION_ERROR): …`), for every refusal alike. + - **Spec.** The shipped liveness ledger records `job.body` (`language`, `source`, `capabilities`, `memoryMb`) as live, so `os validate` / `os build` no longer warn that a job's `body` is planned and not read yet. `body.timeoutMs` stays refused on a job. `JobSchema.body`'s description and the `defineJob` example no longer say to keep a `handler` until the runtime runs job bodies. + - **Unchanged:** a `handler` job on a boot that loads the artifact's runtime module (`os start --artifact`, a `defineStack` config) still runs its `functions` entry; a package without jobs installs exactly as before. + + The route for a refused package: give each enabled job a `body` (sandboxed JS that reaches data through `ctx.api`), or boot the artifact with `os start --artifact`, which loads its runtime module. It ships as `minor` under the launch-window convention for accept-set narrowings. +- 9a4182a: fix(spec,runtime,cli)!: the in-memory (mingo) engine is no longer a boot store — every boot door refuses it and names SQLite instead (#21492, #21572) + + Clause-②: yes (narrowing) + + + + **BREAKING**: the in-memory (mingo) engine can no longer be selected as the store a server, a migration or an embedded stack boots on. It refuses every tenant-scoped read by design, so a boot on it signed a user in and then answered `503` to every data request; there was nothing working to keep. The retirement is made at the declaration: `@objectstack/spec`'s driver table withdrew `memory`, `mingo` and `in-memory` from its selection face (they stay on the config-contract face beside `inmemory`), and every boot door refuses the engine with one sentence that names the replacement. + + - **`@objectstack/spec`** — `DATABASE_DRIVER_SELECTION_ALIASES` no longer lists `memory`, `mingo` or `in-memory`; `DATABASE_DRIVER_SELECTION_IDS` no longer lists `memory`; `resolveDatabaseDriverId` answers `undefined` for all four spellings. `resolveDriverId`, `DRIVER_ID_ALIASES`, `BUILTIN_DRIVER_IDS` and the `memory` config contract are unchanged. + - **`@objectstack/cli`** — `--database-driver memory` is refused while the flags parse (`os dev`, `os start`); `OS_DATABASE_DRIVER=memory` / `mingo` / `in-memory` is refused before `os dev` or `os start` prints its Database row; `os serve`'s legacy path refuses the spellings and the `memory://` / `mingo://` schemes as a fatal boot error. The help no longer offers `memory://`. + - **`@objectstack/runtime`** — `createStandaloneStack`, `createDefaultHostConfig` and `resolveStandaloneDatabase` (every ordinary `os dev` / `os start` / `os serve` boot and every `os migrate` subcommand) refuse the spellings, the `memory://` and `mingo://` schemes, and a project whose default datasource is declared with `driver: 'memory'`. `resolveProjectDatabaseUrl` refuses a retired driver selection ahead of every rung, and its `ProjectDatabaseUrlSource` type no longer has the `'memory-driver'` member. `ResolvedStandaloneDatabase.driver` never names `memory`. Two exports are added for hosts that refuse the engine themselves: `namesRetiredMemoryEngine` and `retiredMemoryEngineMessage`. + - **Unchanged:** the `@objectstack/driver-memory` package; a declared non-default datasource with `driver: 'memory'` and a directly constructed `InMemoryDriver`, both still built; SQLite's dev step-down, whose last rung is still this driver. + + Migration — one flag change: + + - FROM `os dev --database-driver memory` (or `OS_DATABASE_DRIVER=memory`) TO `os dev --fresh` for a throwaway database deleted on exit. + - FROM `OS_DATABASE_URL=memory://…` / `--database memory://…` / `databaseUrl: 'memory://…'` TO `:memory:` (SQLite's own in-memory database), e.g. `OS_DATABASE_URL=:memory:`. + - FROM a default datasource declared `{ driver: 'memory' }` TO a SQLite one, e.g. `{ driver: 'sqlite', config: { filename: ':memory:' } }`. + + No shipped example selects the engine. It ships as `minor` under the launch-window convention for accept-set narrowings. +- f1e4ae5: A job can carry a sandboxed `body`, the same JavaScript body hooks and script actions carry, so its work travels with the metadata; `handler` is deprecated beside it (#21515). + + Clause-②: yes (widening) + + - **`JobSchema.body`** is `ScriptBodySchema` by reference: `{ language: 'js', source, capabilities, memoryMb }`, strict as on hooks. It runs in the QuickJS sandbox with no module scope, reaches data only through `ctx.api` under its declared `capabilities`, and logs through `ctx.log`. The in-process `JobHandlerContext` members (`ql`, `logger`, `bundle`) do not exist there. + - **`handler` is optional and DEPRECATED, "prefer `body`".** When both are present `body` wins, as for hooks. A job that declares neither is refused at parse, located at `body`, with a message naming both keys. The rule is published in the JSON Schema too (`anyOf` of one `required` per key), not only enforced by the parse. + - **Only the L2 body.** An expression (L1) body is refused on a job at `body.language`, and the message says why: an expression performs no I/O, so its only effect would be a returned value, and a job runs for its effects. The message lives on `ScriptBodySchema.language` and fires only where that shape is used on its own; hook and action bodies are unchanged. + - **One time limit.** A body job's limit is the job's own `timeoutMs`: one attempt is one sandbox run, bounded by that value. `body.timeoutMs` (capped at 30 s on hooks and actions) is refused on a job, with the prescription to move the value to `timeoutMs`. The job-level key has no cap, so long-running work states its limit there or splits into bounded runs. The `timeoutMs` describe is the one place this is stated. + - **Not yet run by the runtime.** Scheduling a job's `body` is a separate change. Until it lands a job runs through `handler`, and a body-only job is skipped at boot with a warning. The liveness ledger grades `job.body` `planned`, so `os validate`, `os lint` and `os build` warn wherever a job sets a `body`. `objectstack build` does not mint a job body from the function a `handler` names; write it as data. + + Nothing that parsed before is refused now: every existing job declares `handler`, and none declares `body`. +- 9e9d693: A hook whose `body` targets a table of stored metadata, `sys_metadata` or `sys_metadata_history`, is refused at parse, with the runtime's prescription: change metadata through the metadata API. + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + + **Why.** An app-authored body may not touch the two stored-metadata tables: for a body, the metadata protocol is their only writer, where a change is validated and its provenance is recorded. The runtime already enforces that where a body hook becomes a handler: such a hook is refused at registration and never runs. But `HookSchema` still accepted it, so the metadata save door answered 200 for a hook that would never fire, and the author learned otherwise only from a server log. + + **What is refused.** A hook carrying a `body`, in any form, whose `object` names `sys_metadata` or `sys_metadata_history`, as the string or as any member of the list. One such member refuses the whole hook, as the runtime does. The issue's `code` is `custom`, at `object` (or `object.N` for a list member), and its message names the table and ends with the runtime's prescription. The membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `HookSchema`, `defineHook()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `hooks.N.object`), `os validate`, which runs the same stack parse, an artifact's parse, and the metadata save door (`422 INVALID_METADATA`). + + **What stays accepted, byte for byte.** A hook with no `body` on those tables (a code `handler`, which is how the platform writes its own hooks), a wildcard (`object: '*'`) hook with a `body` (it names neither table: the runtime binds it and never runs its body for those tables' events), and every hook on any other object. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | a hook with a `body` and `object: 'sys_metadata'` or `object: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the hook | + | a hook with a `body` whose `object` list includes either table | drop those tables from the list; change metadata through the metadata API instead | + | a hook with a `body` on `'*'` or on any other object | unchanged | + + **The one-line fix: delete the hook, or remove `sys_metadata` and `sys_metadata_history` from its `object`, and make the change through the metadata API.** The runtime never ran such a hook, so removing it changes nothing an app does. + + **Who is affected, measured.** No authored hook targets either table in this repository's `packages/**` and `examples/**` at `44072fc2b9` (317 hook-shaped declarations, 24 of them outside tests; the only hits are the runtime's own tests of its registration refusal) or in hotcrm at `94668373f2` (44 declarations, 40 outside tests, no hit). Deployed metadata was not measured. A stored hook row of this shape still loads, now with a `[metadata_spec_invalid]` warning and a `_diagnostics` badge, and is still never bound. + + ### The kit + + - **The refusal.** An object-level check attached to `HookSchema` with `.superRefine(...)`. A schema derived from `HookSchema` by overriding a key must use `.safeExtend()`, which keeps the check; zod refuses `.extend()` over a refined object. The artifact-stage hook in `@objectstack/spec` now derives that way. + - **The ledger.** The D3 semantic entry `hook-body-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused hook carries no intent a rewrite could keep. +- 6ec54f0: feat(spec)!: an `object-master-detail-form` detail entry's `sortField` is retired — the console derives the line-position field from the child object and reads no authored value (#21589) + + Clause-②: no (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + + `ComponentPropsMap['object-master-detail-form'].details[].sortField` named the child field the line grid stamps with each line's position on drag-reorder. The console stopped reading it: the field it stamps is derived from the child object, and the pinned console crossed that change while the spec still declared the key. So an authored `sortField` went through `os validate` clean and was dropped, and a drag-reorder stamped the derived field, or none (ADR-0049 enforce-or-remove). + + ### FROM → TO + + | before | what to write instead | + | --- | --- | + | `details: [{ childObject: 'crm_invoice_line', sortField: 'line_no' }]` | delete `sortField`. The grid stamps the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`. | + | `sortField` naming a field outside that list | give the child object one of those fields; the line order is kept there. | + | an entry that names `relationshipField` and at least one column and gives every column a `type` | unchanged: the renderer keeps that entry exactly as authored, loads no child schema for it, and stamps no line position, before and after the upgrade alike. | + + **The one-line fix: delete `sortField` from every `object-master-detail-form` detail entry.** `os migrate meta --from 17` lists the mechanical edits for existing sources; apply them by hand. + + **What an author now sees.** Writing the key fails `tsc` (its input type is the retired-key mark), and `os validate`, `os build` and `os lint` report it as a `component-props-invalid` warning carrying the prescription at `properties.details.N.sortField`. A page that carries it still saves and loads: a page component's `properties` is not parsed on the metadata save or load path. + + ### The retirement kit + + - **Tombstone.** `sortField` is a `retiredKey()` on the strict detail entry. Its prescription prints the derived field names from their one declaration, a module reached by relative import only (`data/inline-grid-sort-fields.ts`), which the derived inline-grid columns read too. + - **D2 conversion `object-master-detail-form-detail-sort-field-removed`** (step 18, retired from the load path): a lossless delete of `sortField` from every `properties.details[]` entry of an `object-master-detail-form`, scoped by component type and by position. Stored `sys_metadata` pages and built artifacts replay it, one notice per entry. + - **D3 entry `object-master-detail-form-detail-sort-field-retired`** carries the judgment the delete cannot make: whether the child object declares the field the line order is kept in. + - **`RETIRED_KEYS_BY_MAJOR[18]`** registers the nested key `ui/ObjectMasterDetailFormProps:details.sortField`. + - **`record:line_items`' answer to `sortField`** no longer sends the author to the detail entry: no block takes an authored `sortField` any more. + - **No deprecation window**: the writer census is zero. + + **Measured producers: none.** On origin/main 9a4182a752, no `object-master-detail-form` detail entry in `examples/`, `apps/`, `packages/`, `skills/` or `content/docs/` writes `sortField`, against the sibling detail-entry key `addLabel` on the showcase project workspace's entry as the control, through the same instrument. At the objectui pin `89cad75d5570` the only detail entries that write it are probes asserting that nothing reads it. Deployed metadata NOT MEASURED. +- 98eb3b9: fix(objectql,spec)!: a hook's `handler` name resolves inside the hook's own package only (#21604) + + Clause-②: yes (narrowing) + + + + **BREAKING**: a hook whose `handler` is a function NAME (the deprecated form, `handler: 'my_fn'`, with no `body`) now binds only to a function its own package holds. It used to fall back to the engine-wide function registry, which is keyed by bare name, so the hook could bind to a function another package registered under the same name and run that package's code on its own events. + + - **Accepted before:** a string `handler` resolved against the functions handed to the hook's bind, then against every function any package had registered on the engine. A name found nowhere was skipped with a `warn`. + - **Accepted now:** a string `handler` resolves against the functions handed to the hook's bind (the package's `functions`, which an `--artifact` runtime module supplies), then against the functions the same package (`packageId`) registered on the engine. Nothing else. + - **Refused now, at registration:** a name the hook's own package does not hold, whether another package registered it or nobody did. The hook is not bound. The refusal carries `INVALID_REFERENCE` with status `400` (ADR-0112), names the hook, the function and the package, and is recorded on the bind result (`BindHooksResult.errors[]` gains `code` and `status`) and logged at `error`. Under `strict` (`OBJECTQL_STRICT_HOOKS=1`) it is thrown. + - **The doors:** a hook authored at runtime through the metadata API (`PUT /api/v1/meta/hook/:name`) ships with no code package and holds no functions, so a `handler`-only hook authored there is refused when the door binds it; the save itself still answers as before. In a composition of several apps, one app's hook can no longer bind to another app's function. A bind that names no owning package (direct `bindHooksToEngine` use without `packageId`) resolves only the functions handed to it. + - **Unchanged:** a hook with a `body` binds as before. An app's hook naming its own `defineStack({ functions })` entry, or a function its own `--artifact` runtime module exports, binds as before. The install-local door's refusal of a hook with no `body` is unchanged. + + What to do with a refused hook: give it a `body` (sandboxed JS), or declare the function in the hook's own package's `functions`. To reuse another package's function, import it from the package that owns it and declare it there. This ships as `minor`, under the launch-window convention for narrowings of an accept set. +- a2aadab: A flow `create_record`, `update_record` or `delete_record` node whose `objectName` is `sys_metadata` or `sys_metadata_history` is refused at parse, with the runtime's prescription: change metadata through the metadata API. + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + + **Why.** App-authored work may not write the two stored-metadata tables: the metadata protocol is their only writer, where a change is validated and its provenance is recorded, and a flow is app-authored automation. The runtime already enforces that at the node: the three write nodes refuse such a target before they resolve a filter, compute a field or call the data engine, under every run identity. But `FlowSchema` still accepted the flow, so `objectstack validate` passed it, the metadata save door answered 200 for it and `registerFlow` registered it, and the author learned otherwise only at its first run. + + **What is refused.** A `create_record`, `update_record` or `delete_record` node, at any depth including an ADR-0031 region body, whose `config.objectName` is a string naming `sys_metadata` or `sys_metadata_history`. The issue's `code` is `custom`, at `nodes.N.config.objectName`, and its message names the node type and the table and ends with the runtime's prescription. The judge is `flowNodeConfigRefusals`, the one `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share, and its membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.objectName`), `os validate`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`). The refusal joins the closed flow slot refusal set as `write-node-stored-metadata-target`, with `params: { nodeType, objectName }`. + + **What stays accepted, byte for byte.** A `get_record` node on those tables (a read is not a write; the runtime judges its reach at the run), a write node whose `objectName` is dynamic (a `{token}` template or an expression envelope: the parse cannot read it as a name, and the runtime judges the name it hands the data engine), and every write node on any other object. + + **One prescription sentence.** `@objectstack/spec/kernel` now exports `STORED_METADATA_BODY_PRESCRIPTION`, the sentence the hook refusal and this flow refusal both end on. It was the hook refusal's private constant, moved unchanged. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | a `create_record` / `update_record` / `delete_record` node with `objectName: 'sys_metadata'` or `objectName: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the node | + | a write node on any other object, a `get_record` node, or a dynamic `objectName` | unchanged | + + **The one-line fix: delete the node, or point its `objectName` at the object the flow really means to write, and make the metadata change through the metadata API.** The runtime never ran such a write, so removing it changes nothing a flow does. + + **Who is affected, measured.** No authored flow writes either table in this repository's `packages/**`, `examples/**`, `skills/**`, `content/docs/**` or `docs/**` at `417443eb27` (229 write-node declarations); the only hits are the runtime's own tests of its node refusal. Deployed metadata was not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register. + + ### The kit + + - **The refusal.** A third arm of `flowNodeConfigRefusals` (`automation/flow-node-config-refusals.ts`), beside the executor-contract arm and the decision arm. + - **The ledger.** The D3 semantic entry `flow-write-node-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused node carries no intent a rewrite could keep. +- ed15448: Every block of a `joined` report must bind a `dataset`: a block with none is refused at `blocks[i].dataset`, by name, with the prescription to bind the block to a dataset. + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + + **Why.** A `joined` report carries its data on `blocks`, each an independent query over that block's own `dataset`, and the container selects nothing. `ReportSchema`'s refinement comment and the reports guide both said each block is dataset-bound, but the joined arm only required `blocks` to be non-empty, and a block's `dataset` is optional on its shape. So a block with no `dataset` parsed, `objectstack validate` exited 0 on it, the metadata save door stored it, and the report drew nothing for it: the joined renderer issues no query for an unbound block and draws it as an empty table, a report whose blocks all lack one falls through to the pre-9.0 presentation bridge, which issues no query either, and a dashboard drill-down that opens that report lists the records instead of drawing it. + + **What is refused.** On a report whose `type` is `joined`, each block with no `dataset`. The issue's `code` is `custom`, at `blocks.N.dataset`, one per unbound block, and its message names the block: *a `joined` report draws each block from that block's own `dataset`, and block `NAME` binds none, so nothing queries it and it draws no rows. Bind the block to a dataset: set its `dataset` to the dataset whose measures (`values`) and dimensions (`rows`) it shows.* It is an arm of `ReportSchema`'s own refinement, so it reaches `defineReport`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `reports.N.blocks.M.dataset`), `os validate` / `os build`, and the metadata save door (`422 INVALID_METADATA`). A stored report row is not rewritten: it carries the same issue in its read-side `_diagnostics` and is refused on its next save. + + **What stays accepted, byte for byte.** A `joined` report whose blocks all bind a `dataset`; every non-joined report, including one that carries `blocks` (they are read on a `joined` report only); and `JoinedReportBlockSchema` parsed on its own, where `dataset` stays optional. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | a `joined` report block with no `dataset`, e.g. `{ name: 'open_block', label: 'Open' }` | the same block bound to the dataset it shows: `{ name: 'open_block', label: 'Open', dataset: 'task_metrics', rows: ['status'], values: ['task_count'] }` | + | a `joined` report whose blocks all bind a `dataset`, or any non-joined report | unchanged | + + **The one-line fix: set each block's `dataset` to the dataset whose measures it shows, or delete a block that has nothing to show (a `joined` report keeps at least one block).** The renderer never drew an unbound block, so binding it is the first time it draws anything. + + **Who is affected, measured.** No joined report with an unbound block exists in this repository's `examples/**`, `packages/**`, `skills/**` or `content/docs/**`: the showcase's one joined report, the reports guide's example and every test fixture bind each block, apart from one metadata-door test fixture that left its block unbound on purpose and is bound in this change. The hotcrm application's one joined report binds every block, and the cloud repository has no joined report. Deployed metadata was not measured. Studio's report inspector can still produce one: its `blocks` repeater adds a blank row and requires no column of it, so a block saved with only a name is now refused at save, at `blocks.N.dataset`, where it used to be stored and draw nothing. + + ### The kit + + - **The refusal.** A per-block arm of the joined branch of `ReportSchema`'s refinement (`ui/report.zod.ts`), beside the container refusals for `dataset` / `rows` / `columns` / `values`, `order` and `chart`. The block's `dataset` description now says a joined report refuses a block without one, and the generated reference page carries it. + - **The ledger.** The D3 semantic entry `ui-report-joined-block-dataset-required` (protocol 18) and its step-18 rationale fragment. No key is removed, so there is no tombstone, and there is no D2 conversion: which dataset a block shows is the author's decision, and no rewrite can name it. +- 309224d: A refused flow resume answers the engine's own code, on the REST resume door and the MCP `resume_run` tool alike, as the 17.1.0 release notes, `client.automation.resume()`'s documentation and the flows guide already state (#21724). + + Clause-②: yes (widening) + + - **What moves on the wire.** `POST /api/v1/automation/:name/runs/:runId/resume` used to hand the error builder a status and no code for the engine's refusals, so `error.code` was derived from the status. It now carries the engine's code: + + | Refusal | Status | `error.code` before | `error.code` now | + |---|---|---|---| + | a screen input that breaks the screen's declared fields | 400 | `VALIDATION_ERROR` | `INVALID_SCREEN_INPUT` | + | a signal that writes an engine-reserved `$` name | 400 | `VALIDATION_ERROR` | `INVALID_SIGNAL` | + | an unknown run, a run whose flow is gone, or a run whose paused node was edited away | 404 | `RESOURCE_NOT_FOUND` | `RUN_NOT_FOUND` | + | the suspended-run store is unreadable | 503 | `SERVICE_UNAVAILABLE` | `STORE_UNAVAILABLE` | + | another resume already holds the run | 409 | `RESOURCE_CONFLICT` | `RESUME_IN_PROGRESS` | + + `PERMISSION_DENIED` (403) is unchanged. No status moves, nothing that was accepted is refused, and a refused resume still leaves the run paused, so a corrected resume still completes it. A caller that branched on the status-derived code reads the documented one instead: `err.code` from `client.automation.resume()` is now `INVALID_SCREEN_INPUT`, `INVALID_SIGNAL` or `RUN_NOT_FOUND`, which is what lets it tell a bad screen value from a reserved signal name. + - **`@objectstack/spec` — `minor`.** The ADR-0112 error-code ledger registers `INVALID_SCREEN_INPUT` under `@objectstack/service-automation`, beside `INVALID_SIGNAL` and `RUN_NOT_FOUND`. The engine already returned it and the docs already promised it, but `ErrorCode`, and therefore `ApiErrorSchema`, refused it. The published vocabulary gains one member and loses none. + - **`@objectstack/runtime` — `minor`.** The resume door's answer set gains the five codes above. Both doors read one classifier, so the REST route and `resume_run` answer the same code for the same engine result. +- e83c9f6: A package install refused by the ADR-0087 D1 protocol handshake now answers `422 OS_PROTOCOL_INCOMPATIBLE`, with its structured diagnostic in `error.details`. It used to answer the `500` server-fault fallback, with the diagnostic only inside the message. + + Clause-②: yes + + - **`POST /api/v1/packages`:** a manifest whose declared range (`engines.protocol`, then `engines.platform`, then `engine.objectstack`) excludes this runtime's protocol major answers `422`, with `error.code: 'OS_PROTOCOL_INCOMPATIBLE'` and `error.details: { requiredRange, rangeSource, protocolVersion, targetMajor, migrateCommand }`. `error.message` is unchanged, and the command after its `Run:` equals `migrateCommand`. Nothing is installed, so a later `GET /api/v1/packages/{id}` still answers `404`. + - **The no-protocol-service fallback:** in a composition without a `protocol` service, `POST /api/v1/packages` used to install such a manifest. It now runs the same handshake and answers the same `422`. + - **`@objectstack/metadata-core`:** `ProtocolIncompatibleError` declares `status` and `statusCode` `422`, with `code` as the literal `'OS_PROTOCOL_INCOMPATIBLE'`. It also carries a `Symbol.for` brand, and the new `isProtocolIncompatibleError(e)` recognises it across module instances, where `instanceof` would not. Any other caller that resolves the error (the boot-time `AppPlugin` load included) reads `422` rather than the `500` fallback. + - **`@objectstack/spec`:** the error-code ledger's `OS_PROTOCOL_INCOMPATIBLE` row states its status (422) and the door that carries the diagnostic. The vocabulary does not change. + + A client that branched on `500` for this code should branch on `422`, or on `error.code`. None was found in this repository, the SDK or the console. + + +- 045f764: `ISecurityService` declares the two members the registered `security` service already served without a declaration: `discardPermissionSetOverlay` and `contributeOwnershipFloorAlternates`. Both are optional, and callers feature-detect them. + + Clause-②: yes (widening) + + - **`discardPermissionSetOverlay(callerContext, id)`** (`@objectstack/spec/contracts`). The audited operator action behind a permission set's "Discard Overlay" Setup action: it deletes the stale environment overlay that shadows a package-declared permission set, then re-projects the row from the declared artifact before it resolves. Its docblock names the refusals it throws and the codes they carry: `PERMISSION_DENIED` (403) when the caller is not a tenant-level administrator or no installed package declares the set, `NOT_FOUND` (404) for an unknown row, and `INVALID_STATE` (409) when there is no active overlay to discard. It resolves with the new `PermissionSetOverlayDiscardResult` type. The REST route answers `501 NOT_IMPLEMENTED` when the method is absent. + - **`contributeOwnershipFloorAlternates(plugin, alternates)`**. The seam through which a plugin that installs a tighter row gate stops the platform's `created_by` write floor pre-empting that gate on one object and one limb. Its docblock names what it refuses (a wildcard object, an operation other than exactly `update` or `delete`, a missing or malformed policy), and says a second call replaces the same plugin's first and an empty list withdraws it. The new `OwnershipFloorAlternate` type is the minimal contract shape of one alternate. + - **Optional, and absence is typed.** A security service without either member still satisfies the contract, and an unguarded call does not compile. + + Nothing an author writes changes. An implementation typed as `ISecurityService` that serves either name must now serve it under the declared signature; `@objectstack/plugin-security` already does, and needs no change. +- 6fb7115: feat(spec): an inline `object-form` field declares the `grid` widget's eight camelCase field-level keys, and each snake_case spelling is refused naming its camelCase key (#21768) + + Clause-②: yes (widening) + + A widening of a published authoring surface: every value that parsed before still parses, and eight keys that were refused now parse. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`. + + **`@objectstack/spec`** + + - **The runtime form field takes the `grid` widget's field-level keys.** An `object-form` `customFields` member, and the inline entry of an `object-form` or `object-master-detail-form` section's `fields`, now declare `minRows` and `maxRows` (numbers), `allowAdd`, `allowDelete` and `allowReorder` (booleans, on unless `false`), and `totalField`, `addLabel` and `sortField` (strings). These are the value types objectui's `GridFieldMetadata` declares. The `grid` widget reads each one off a `type: 'grid'` field: `minRows` stops Remove, `maxRows` stops Add, Duplicate and the blank entry row, `addLabel` labels the Add button, and `sortField` names the row field the grid stamps with each row's index, so a drag-reorder is saved. objectui renamed the eight from snake_case to camelCase, with no dual read, and the `.objectui-sha` pin `9dfaca654311` carries that rename. + - **`totalField` here is the CHILD column the grid sums into its footer.** On `record:line_items` and on an `object-master-detail-form` detail entry, the same spelling names the PARENT field the sum is saved to, and their child column is `amountField`. The describe states the difference. The other blocks' keys are unchanged. + - **The snake_case spellings are still refused, and each refusal now names its own replacement.** `min_rows`, `max_rows`, `allow_add`, `allow_delete`, `allow_reorder`, `total_field`, `add_label` and `sort_field` are each answered with "Rename the key to `minRows`" (and so on); the value stays the same. The old answer said the keys would come in once the widget read a camelCase spelling, and the widget now does. Two retired spellings on one field get one line each. + - **`record:line_items`' `sortField` refusal** now says no block takes an authored `sortField` *for child records*. An inline `grid` field takes one for the rows of its own value, so the unqualified sentence was no longer true. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `customFields: [{ name: 'items', type: 'grid', min_rows: 1, allow_add: false }]` | `customFields: [{ name: 'items', type: 'grid', minRows: 1, allowAdd: false }]` | + | `total_field: 'amount'` on an inline grid field | `totalField: 'amount'`, naming the child column summed | + | `add_label: 'Add line'`, `sort_field: 'position'` | `addLabel: 'Add line'`, `sortField: 'position'` | + + The one-line fix: rename each key to its camelCase spelling, keeping its value. Nothing that parsed before is refused, so no ADR-0087 conversion or D3 entry is owed. + + ## Who is affected, measured + + - **objectstack** at `75ddcd1b41` (this branch's base): no writer of either spelling on an inline form field in `examples/`, `skills/`, `content/docs/` or `apps/`. The spec's own pin is the only one: its `min_rows` refusal probe. + - **objectui** at the pin `9dfaca654311`: the camelCase writer is the schema catalog's `fields-grid/line-items-grid` example, a `grid` field carrying all eight keys, and it parses. No production source reads a snake_case spelling. The only snake_case occurrences left are objectui's own refusal faces: the TS tombstones, the zod alias refusals, and the widget's refusal. + - **hotcrm**, **cloud** and deployed metadata were not measured. +- a43d90a: Phone-number OTP with no deliverable SMS service now answers `400 SMS_SERVICE_REQUIRED` instead of a `500` with an empty body (#21793). + + Clause-②: yes (widening) + + - **`@objectstack/plugin-auth`.** `POST /api/v1/auth/phone-number/send-otp` on a deployment that turned phone sign-in on but has no SMS service that can deliver a code (none wired, or only the log transport in production) used to answer `500` with a `null` body: the send callback threw a plain `Error`, and better-auth's router turns anything but its own `APIError` into a bare 500. The login page had nothing to branch on and showed a generic failure. It now answers `400` with the body `{ "code": "SMS_SERVICE_REQUIRED", "message": "…" }`, a typed `APIError`, as the daily-quota branch of the same send already was. The message names the missing SMS delivery service and where an administrator configures it, and never carries the one-time code. `request-password-reset` is unchanged: it still answers `{ "status": true }` and sends nothing, so it reveals nothing about which numbers are registered. + - **`@objectstack/spec`.** `SMS_SERVICE_REQUIRED` is registered for `@objectstack/plugin-auth` in the ADR-0112 error-code ledger, beside its email sibling `EMAIL_SERVICE_REQUIRED`. `ErrorCode` (and so `ApiErrorSchema.code`) accepts one more value. Nothing that parsed before is refused now. + + **Action for clients.** A client that branched on the old `500` for this case should branch on `code === 'SMS_SERVICE_REQUIRED'` instead. The public config already advertises the capability as `features.phoneNumberOtp`, which stays `false` on such a deployment. +- 607463d: An action can declare which organization membership grades it is offered to: `requiresMembershipReach` names a row of the new `MEMBERSHIP_REACH` table and is lowered at parse time into `visible`, the way `requiresFeature` is. + + Clause-②: yes (widening) + + - **`MEMBERSHIP_REACH`** (`@objectstack/spec/identity`) says which membership grades reach which better-auth organization endpoint. `invite_member` is reached by owner, admin and delegated_admin. `cancel_invitation`, `update_member_role`, `remove_member`, `create_team`, `update_team`, `remove_team`, `add_team_member` and `remove_team_member` are reached by owner and admin. `transfer_ownership` (setting the creator role on a member) is reached by the owner alone. The rows are read off better-auth's own access-control statements plus the `delegated_admin` registration, and plugin-auth pins them equal to the door. It is reach, not authority (ADR-0108 D1): a fourth fact beside the membership names, the administrative-grade rule and the identity projection, and merged into none of them. Also exported: `MEMBERSHIP_REACH_NAMES`, `membershipReachPredicate`, `lowerRequiresMembershipReach`, and the `MembershipReachEntry`, `MembershipReachName` and `MembershipReachStatement` types. + - **`ActionSchema.requiresMembershipReach`** is optional and enum-checked against the table's row names. At parse time it becomes one `'' in current_user.positions` term per grade, in the names `mapMembershipRole` projects them to (`org_owner`, `org_admin`, `delegated_admin`). The terms are AND-composed with an explicit `visible`, and the key is stripped from the parsed output. It composes ahead of `requiresFeature`, so a feature gate stays the last term. `visible: false`, a non-CEL or AST-only `visible`, and a blank `source` are refused at parse time, at the key. + - It is UI courtesy, not authorization: the endpoint's own door stays the authority. The capability channel (`current_user.can`) is unchanged and carries no grade (ADR-0108). + + Nothing that parsed before is refused now, and an action without the key lowers exactly as before. +- e864db5: fix(service-datasource): a destructive re-import's refusal names the remedies that work from the import route, instead of a `?force=true` that route never reads (#21841) + + Clause-②: yes (widening) + + - **What was wrong.** "Import as Object" (`POST /api/v1/datasources/:name/external/tables/:remote/import`) saves through the metadata door's own `saveMetaItem`. A re-import that would drop or retype a field the stored object still carries is refused by that save's destructive-change gate, and the import route relays the refusal as `400 EXTERNAL_IMPORT_ERROR`. The refusal ended `re-submit with ?force=true to proceed.` The import route reads no `force`, so a caller who did exactly that got the identical refusal back. + - **What the refusal says now.** The import states its own write face, and the refusal ends: this import cannot be forced, because the external-table import route accepts no `force`. Import the table under a new `name`, or save the changed definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Both remedies are measured on the showcase: each one answers `201` or `200` where the re-import answered `400`. The same words appear in this package's earlier changeset for the import. + - **What widens.** `SaveMetaItemRequestSchema.writeFace` (`@objectstack/spec`) and `saveMetaItem`'s `writeFace` parameter (`@objectstack/metadata-protocol`) gain one member, `'external-import'`. The member is stated by the server. No door reads it from a request body, and the import's own options cannot carry it, or a `force`, into the save. Nothing accepted today is refused. + - **What does not change.** The refusal itself stays: a destructive re-import is still `400 EXTERNAL_IMPORT_ERROR`, and the stored definition does not move. The import route gains no `force`. Acknowledging a destructive change stays on the metadata door. The other faces' wording is unchanged. A `422 INVALID_METADATA` relayed by the import keeps its full findings in the message, because the import route's envelope carries no `issues`. +- 866683f: A flow `approval` node's `config` is judged at parse against the contract the spec declares for it, `ApprovalNodeConfigSchema`, whole: an undeclared key, a refused value and a required key left out are each refused with a location, in the contract's own words. + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + + **Why.** The approval node's executor parses `node.config` against `ApprovalNodeConfigSchema` before it does anything else and fails the node on any issue. Registration already refused an undeclared key, but a refused value such as `escalation.timeoutHours: 0.5` registered and then failed every run that reached the node, and no build door asked about either: `objectstack validate` and `objectstack compile` exited 0 on an `escalation.bogusKey` or a `timeoutHours: 0.5`, and compile copied it into `dist/objectstack.json`. + + **What is refused.** An `approval` node, at any depth, whose `config` the approval contract refuses. The judge is `flowNodeConfigRefusals`, the one `FlowSchema.parse`, `AutomationEngine.registerFlow` (which parses first) and `objectstack validate` share; the approval contract joins it as a declared contract map beside the builtin executor contracts, with no plugin loaded. Every issue that contract raises is refused, because the executor refuses on every one: + + - an undeclared key, at the key (`nodes.N.config.escalation.bogusKey`, one issue per key, the top level included), and a refused value, at its key (`nodes.N.config.escalation.timeoutHours` for `0.5` under its minimum of 1): the new closed-set code `node-config-refused-by-contract`, `params: { nodeType, key }`, whose message carries the contract's own sentence — for an alias, its did-you-mean (`timeout` → `timeoutHours`); + - a required key left out (`approvers`; `timeoutHours` inside an `escalation` block): `node-config-key-missing`, as for a builtin node, or `node-config-key-required-by-rule` where a rule of the contract requires it. + + The issue's `code` is `custom`. That covers `FlowSchema`, `defineFlow()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `flows.N.nodes.M.config.`), `os validate`, `os compile`, an artifact's parse, `registerFlow` and the metadata save door (`422 INVALID_METADATA`). + + **What stays accepted, byte for byte.** Every approval node the contract accepts, an `approval_revise` node, and every builtin node: the builtin arm still judges only a key left out, so an undeclared key or a wrong-typed value on a builtin node is judged where it was before. A plugin node type whose contract the spec does not declare stays outside the build doors. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `escalation: { …, bogusKey: 1 }`, or any key the contract does not declare | delete the key, or rename it to the one the refusal's did-you-mean names (`timeout` → `timeoutHours`, `mode` → `behavior`, `quorum` → `minApprovals`) | + | `escalation: { timeoutHours: 0.5 }` | `escalation: { timeoutHours: 1 }` — whole wall-clock hours, at least 1 | + | `escalation: { enabled: false }` with no `timeoutHours` | delete the `escalation` block | + | `steps`, `entryCriteria`, `onApprove`, `onReject` or `rejectionBehavior` on the node | the flow graph, as the refusal's guidance says (successive nodes, the entering edge's `condition`, the `approve` / `reject` out-edges, a back-edge) | + | an approval node with no `approvers` | `approvers: [{ type: 'position', value: '' }]` (or any approver the contract accepts) | + + **The one-line fix: write the shape the approval contract declares at the key the refusal names.** The runtime never ran such a node, so the fix changes nothing a working flow does. + + **Who is affected, measured.** At `5e0b489bca`, every approval node `config` authored in this repository parses under the contract: `examples/**` (15 nodes, all in the showcase), `content/docs/**` (6 snippets), `skills/**` (5 snippets) and the `packages/qa/dogfood` fixtures (6 nodes), and so does the Studio designer's approval seed at the pinned objectui commit. Deployed metadata, and repositories other than these two, were not measured. Where such a node already sits in a stored flow, the whole flow is refused at registration: at boot it is skipped with a warn naming it, its trigger not armed, while the flows beside it register. + + ### The kit + + - **The refusal.** The declared contract map in `automation/flow-node-config-refusals.ts`, read by the same executor-contract arm of `flowNodeConfigRefusals`; the new code joins `FLOW_SLOT_REFUSAL_CODES`. + - **The ledger.** The D3 semantic entry `flow-approval-node-config-contract-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: the platform cannot know the approvers, the key or the value the author meant. +- 88a39c0: feat(spec)!: an `action:group` / `action:menu` member refuses a non-array `params` unless its `type` is `api`, with the prescription to author an action with static parameter values as its own `action:button` node + + Clause-②: yes (narrowing) + + + + **BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports the refusal as an advisory `component-props-invalid` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path. + + **Why.** A container member's `params` is its input list: both containers forward an array as the `ActionParam[]` inputs to collect before the action runs. They forward any other `params` value only for a `type: 'api'` member, as its request payload, and drop it for every other `type` (an absent one included), with a development-build warning only. The member declared `params` as any value, so an object `params` on a `navigate_edit` member passed the gate and then had no effect: no error and no static values. `params` carries one shape, and no second value-bag key is declared; a member's `properties.params` is already refused. So static parameter values are not part of the inline action vocabulary at all, and an action that needs them is its own `action:button` node, whose `params` object carries them. + + **What is refused.** On an `action:group` or `action:menu` member whose `type` is not `api`, a `params` that is not an array — an object, a string, a number or `null`. The issue's `code` is `custom`, at `actions.N.params`, and its message names the container, the member's `type` and the prescription: *to run an action with static parameter values, author it as its own `action:button` node, whose `params` object carries them* (and, for a `type: 'api'` member's request body, `bodyExtra`). + + **What stays accepted, byte for byte.** An array `params` on any member; every `params` value on a `type: 'api'` member (its request-payload window, unchanged); a member with no `params`; and an `action:button` / `action:icon` node's object `params`, which is its static values. The member's `params` stays `unknown` in the types — the narrowing is a refinement on the member, and no export, key or type moves. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | `actions: [{ name: 'edit', type: 'navigate_edit', params: { objectName: 'account', recordId: '${record.id}' } }]` on `action:group` / `action:menu` | the action as its own node: `{ type: 'action:button', properties: { name: 'edit', label: 'Edit', actionType: 'navigate_edit', params: { objectName: 'account', recordId: '${record.id}' } } }` | + | `actions: [{ name: 'save', type: 'api', target: '/api/save', params: { status: 'closed' } }]` | unchanged — or, preferred, `bodyExtra: { status: 'closed' }` | + | `actions: [{ name: 'ask', type: 'script', params: [{ name: 'reason', type: 'text' }] }]` | unchanged — an array is the input list | + + **The one-line fix: move a member that carries static parameter values out of its container into its own `action:button` node, with the same `params` object; drop a non-array `params` from any other member.** The container never forwarded those values, so the `action:button` node is the first place they reach the handler. + + ## Who is affected, measured + + A writer is an `action:group` / `action:menu` member authoring a non-array `params` on a non-`api` type. The census walked every `params` key in every file that names either block (TypeScript AST over `.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`/`.mts`/`.json` and fenced Markdown code, same-file constants resolved; YAML by text), plus every non-array `params` on an element of any `actions` array corpus-wide, and each hit was read by hand. Lit control: a planted fixture with four non-`api` object members (flat, inside a node's `properties` bag, through a same-file constant, in a Markdown fence), an `api` member and an array member — all six found and classified. + + - **objectstack** at `5b2d189e28`, this branch's base: 24 files name a block, holding 32 `params` keys, 23 not an array; none is a container member's — conversion fixtures of the inline `element:button` action, schema source, the liveness ledger, CHANGELOG quotations, and one `properties.params` refusal probe. No example, doc, skill or fixture writes the refused spelling. + - **objectui** at the `.objectui-sha` pin `0abd4f9f8` and at `main` `f1a177c41` (the same census at both; the action renderers byte-identical): 89 files, 47 `params` keys, 41 not an array. The only container members with a non-array `params` on a non-`api` type are objectui's own tests asserting that the container drops it (`action-entry-object-params-10462.test.tsx`, `action-container-member-params-10290.test.tsx`); the `type: 'api'` controls beside them stay accepted. + - **hotcrm** at `4054ec2680`: no file names either block. + - **cloud** was not reachable from this session. **Deployed metadata** was not measured. + + ### The kit + + - **The refusal.** A refinement on each container member (`ui/component.zod.ts`), applied to the `action:group` and the `action:menu` member alike; the member's `params` description says what it now takes, and the generated reference page carries it. + - **The ledger.** The D3 semantic entry `ui-action-group-menu-member-params-array-only` (protocol 18) and its step-18 rationale fragment. No key is removed, so there is no tombstone, and there is no D2 conversion: the static values belong on a different node, which no rewrite can build in the author's place. +- 48eb9c1: feat(spec)!: `ai:chat_window` is retired — refused by name at the schema door, the floating chat overlay is the AI chat entry point (#21504, ADR-0049) + + Clause-②: yes (narrowing) + + + + **BREAKING** — an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings (the `user:profile`, `element:filter` and `element:form` retirements shipped the same way). + + `ai:chat_window` was declared in `PageComponentType` and mapped to `AIChatWindowProps` (`mode`, `agentId`, `context`, `aria`) in `ComponentPropsMap`, and no renderer for it ever shipped — not in objectui, framework or cloud. The console leaves it unregistered on purpose: the floating chat overlay it mounts on every page is the supported AI chat entry point, and an inline page-level chat window is not part of the supported surface. So an authored `ai:chat_window` node validated clean and then drew "Unknown component type" in front of an end user, and none of its four props configured anything. The triage ruling retired it under ADR-0049 enforce-or-remove, refused by name, following the `user:profile` precedent. + + **What is refused:** an authored `ai:chat_window` component node, at `PageComponentSchema.type`. That covers `definePage()`, `PageSchema`, and every door that parses pages: `os validate`, `os build`, `os lint` and the metadata save door. The issue is located at the node's own path, with `code: 'custom'` and `params.retiredComponentType`, and its message is the retirement prescription. `PageComponentType`'s own error map refuses the name with the same text when the enum is parsed alone. `ComponentPropsMap['ai:chat_window']` stays as a row, so every reader that dispatches on it keeps recognising the name: the component-props gate, `check-yaml-examples` and the type vocabulary's known set. The row now refuses every props bag, `{}` included, with the same prescription. One prescription string, `RETIRED_PAGE_COMPONENT_TYPES` in `@objectstack/spec/ui`, answers at all three doors. + + **What is removed from the exports:** `AIChatWindowProps` (`@objectstack/spec/ui`), the props schema the element no longer has. Its JSON Schema (`ui/AIChatWindowProps`) is no longer published. + + **What stays accepted:** every other member of `PageComponentType` and `ComponentPropsMap`, byte-identically. That includes `ai:suggestion`, which keeps its row and its place in the enum, so `ai:` stays a namespace the `component-type-unknown` authoring rule claims. The open string arm also stays open: custom and plugin-registered types keep parsing. The only string refused is the retired name itself. + + ## FROM → TO + + | you wrote | write instead | + |:--|:--| + | a `{ type: 'ai:chat_window' }` component node in a page region, slot or container | nothing: delete the node. The floating chat overlay is on every page already | + | `properties: { agentId: '…' }` on that node | the app's `defaultAgent` (a platform agent: `ask`, the default, or `build` on an authoring surface) | + | `properties: { mode, context, aria }` on that node | nothing: none of them was ever read, and the overlay is not configured per page | + + The one-line fix: delete the `ai:chat_window` component node. No ADR-0087 conversion is registered, because the only edit is deleting an authored page node, and a mechanical conversion does not delete page nodes: which region closes up is a layout decision. The D3 entry `ui-ai-chat-window-retired` carries that delegation, so `os migrate meta --from 17` lists it as a manual change for every stack that still names the type. + + ## Who is affected, measured + + - **objectstack** at `529d9711fb`: zero authored `ai:chat_window` nodes in `examples/**`, `packages/apps/**`, `apps/**`, `skills/**` and `content/docs/**` code samples. The only hits were the spec's own type list, its row, its tests and the generated reference docs. The control in the same query shape: `element:divider` is authored in 3 example files and `record:details` in 12. + - **objectui** at the `.objectui-sha` pin `89cad75d55`: no renderer is registered. `components/src/renderers/placeholders.tsx` omits the type on purpose, and Studio's page palette excludes it. The remaining hits are tests asserting its absence, the palette exclusion, a parity-ledger entry and comments. No non-test source imports `AIChatWindowProps` or indexes the row. + - **cloud** and **hotcrm** (triage's census): zero producers. hotcrm names it once, in a comment, as dropped. + - **Deployed metadata** was not measured. + + The retirement kit: + + - the retired-type map entry, the enum value removed (`packages/spec/src/ui/page.zod.ts`), and the row turned into a whole-bag refusal, with `AIChatWindowProps` removed (`packages/spec/src/ui/component.zod.ts`) + - the D3 semantic entry `ui-ai-chat-window-retired`, its step-18 rationale fragment, and the `RETIRED_DEFS_BY_MAJOR` entry `ui/AIChatWindowProps` + - pin tests: in `component.test.ts`, `code`, `path`, `params` and the first sentence at each of the three doors, with `ai:suggestion` as the control and the open arm left open. In `component-type-vocabulary.test.ts`, the type stays known, leaves the typo candidates, and `ai:` stays reserved. The `ComponentPropsMap` `z.unknown()` enumeration loses its `ai:chat_window` `context` line with the row's keys. + - generated baselines and docs follow the schema: `api-surface/`, `export-origins/`, `declaration-map/`, `authorable-surface/`, `authorable-defaults/`, `json-schema.manifest/`, `spec-changes.json`, the upgrade guide and the reference docs. The hand-written `content/docs/ui/pages.mdx` component list now says the truth. + +### Patch Changes + +- 135daaa: Liveness ledger: `agent.guardrails` (`maxTokensPerInvocation`, `maxExecutionTimeSec`, `blockedTopics`) is now `live`, not `experimental`. The cloud AI runtime enforces it on every user turn. The token and time limits are checked before each model round, with each limit refusal audited, and a blocked tool name or category is removed from the offer and refused at call time. + + Clause-②: no + + - The `guardrails` describe drops its `[EXPERIMENTAL — not enforced]` marker. It now says the cloud AI runtime enforces the block and the open framework edition does not run agents. The generated agent reference page follows. + - Author-facing effect: `os lint` / `os validate` no longer warn `liveness-experimental-property` on an agent that sets `guardrails`. A warning is not a refusal, so the accept set is unchanged. + - The ledger row cites the cloud readers and producer, dated to the reading they come from. + - The liveness README no longer says its gate refuses `live` on evidence attributed only to the closed cloud runtime. The gate never did. + - `tool.outputSchema` stays `experimental`, because nothing reads it on a tool record. Its describe and the tools guide now say where output validation actually lives: `ai.outputSchema` on the action, against which the cloud AI runtime checks the action's result. The old claim that the keys are folded into the tool description is gone. + - ⛔ No schema, parse, export or accept-set change. `agent.memory`, `agent.structuredOutput` and `agent.lifecycle` stay `experimental`. +- 0721848: Liveness ledger: a permission set's row-level security policy `label` and `description` (`rowLevelSecurity[].label` / `.description`) are now `live`, not `dead`. Studio's permission editor shows both on every policy. Ledger data and its generated count shard only. + + Clause-②: no + + - **What shows them.** These are display keys, so under the ledger's "Designer previews count as consumers" ruling, being shown to a human is the whole of their claimed effect. The Row-Level Security section of the permission editor (`PermissionAdvancedFacets` in objectui) now heads each policy card with the policy's `label` and, beneath it, its `description`, exactly as written. Both rows cite that reader at the `.objectui-sha` pin `89cad75d557`. The registered permission preview also draws both, but no route mounts it for `permission`, so it is not cited. + - **Where the values come from.** Each row names its producer: the `permission` edit page registration and the Studio edit route that mounts it, the editor's `GET /api/v1/meta/permission/:name/layers` read, and this repo's shared layered answer (`createMetaLayeredAnswer`), which serves a permission set whole. The showcase's contributor permission set authors both keys on all three of its policies. + - **Author-facing effect.** `os lint` / `os validate` no longer warn `liveness-dead-property` on a policy that sets `label` or `description`. A warning is not a refusal, so the accept set is unchanged. + - **Still kept.** The re-grade reverses no ADR-0033 decision. Both rows stay docs-shaped annotation, deliberately kept and not `authorWarn`'d. + - The regenerated liveness count is the `liveness/state-counts/permission.md` shard: `permission` has 38 live and 4 dead (was 36 and 6). The `view` container's own `label` stays `dead`. + - ⛔ No schema, parse, `.describe()`, export or accept-set change. +- bdd3654: Liveness ledger: the view container's body `name` row stays `dead`, and its note now states what the platform actually does with the key + + Clause-②: no + + - The old note said the body copy was "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body that has none, containers included (`normalizeViewMetadata` in `@objectstack/metadata-protocol`). Its overlay paths key on that stamped copy: `hydrateOverlayIntoRegistry` registers no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by it. + - The verdict is unchanged, because the ledger's `live` means that authoring the key changes runtime behaviour. An authored container `name` only restates the key the container already registers under, or contradicts it. `os validate` and `os lint` keep warning `liveness-dead-property` ("drop it"). + - The note records why the key is kept rather than tombstoned: the door's own saves stamp it, so a tombstone would refuse the platform's own writes. A maintainer ruling also refused a spec-level forbid of a container's `name`. + - It corrects the old attribution too. Artifact-shipped containers and the metadata-validation sweep author no `name`; what was read as theirs is the door's stamp. + - The ledger README's `view` cell says the same. The `view.list.tabs` row's note now records that the two author-time walks that still read a list view's own `tabs` are deleted. + - A comment in `system/i18n-resolver.ts` that still called the list view's own `tabs` a live carrier now says the key is a tombstone and `UserFiltersSchema.tabs` is the one carrier. + - ⛔ No schema, parse, export, status or accept-set change. +- aead296: Liveness ledger: the `flows` translation group is `live`, and so are both its children. The console's screen-flow runner now names the flow by `flows..label` in the active language, in the runner's header and in its completion toast. A locale the bundle does not cover shows the label authored on the flow, and then the flow's API name. `screens` was already `live`. + + Clause-②: no + + - The `flows` row drops `authorWarn` and its `authorHint`. `os lint` and `os validate` no longer warn `liveness-planned-property` on a bundle that authors `flows`. A warning is not a refusal, so the accept set is unchanged. + - Dropping that bit switches on the CLI's i18n coverage demand for `flows.*`. `os lint` now reports a `flows..*` key that a supported locale is missing as `i18n/missing-flow`, and `os i18n extract` scaffolds the group into the bundle. The flow's own `label` is demanded only for a flow with a screen node (see the `@objectstack/cli` entry). Under `--i18n-strict` a missing key is an error: translate it, or run `os i18n extract` to scaffold it. + - The `flows` TSDoc in `translation.zod.ts` and the translations guide's boundary note now say that both halves are applied. + - ⛔ No schema, parse, export or accept-set change. +- ad7c351: Field-key guidance, the retired `DriverCapabilities` tombstones, the datasource `readOnly` guidance, the retired filter operators and the legacy `apiMethods` strip warning no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + These are the `@objectstack/spec` texts an author meets at the moment something is refused or rewritten: the unknown-field-key guidance that `os validate` and the lint print, the parse errors for retired `DriverCapabilities` keys, the guidance for `readOnly` written inside a datasource driver's `config`, the `INVALID_FILTER` refusal every driver face prints for `$regex` / `$options`, and the warning `enable.apiMethods` prints when it strips a retired legacy value. They pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - Field-key guidance: `index` and `indexed` say the field-level index flag built no index and was removed under ADR-0049 enforce-or-remove; `dataQuality` and `cached` say their leftover `DataQualityRules` and `ComputedFieldCache` schemas were deleted from the public API too, and that computed-field caching returns only together with a runtime consumer. + - `DriverCapabilities` tombstones: the `bulkCreate` / `bulkUpdate` / `bulkDelete` prescriptions name discovery's `transactionalBatch` bit, derived from the live composition so a client negotiates instead of probing; the `fullTextSearch` prescription says `$contains` itself stays case-sensitive while textual search is case-insensitive. + - Datasource `readOnly` guidance: says a managed datasource has no read-only gate by decision, because a flag only the application checks cannot stop direct connections, migrations or DDL. + - Retired filter operators: the `$regex` and `$options` refusals say they are retired under ADR-0049 enforce-or-remove, refused rather than reinterpreted. + - Legacy `apiMethods` strip warning: the `restore` and `purge` prescriptions say `enable.trash` was retired because no runtime ever read it, and that the recycle-bin (soft-delete) work `restore` would need is parked. + + Text only: no key, schema shape, condition, error code or status moves. A tool or test that matches the old text (for example a tracker-number suffix) needs the new spelling. +- e901c27: The protocol 16 → 17 conversion summaries, the `autonumberFormat` description and two metadata route descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + A conversion's `summary` is the line an author reads when upgrading metadata: it is the "Change" column of `docs/protocol-upgrade-guide.md`'s protocol 16 → 17 table, the `to` text of `spec-changes.json`'s `converted[]` records, and what `os migrate meta --json` reports under `specChanges`. Fifty-six of the protocol-17 summaries pointed at an issue-tracker number for the reason behind a rewrite. The number goes; where the sentence did not already say what was decided, it now does. For example: + + - `action-execute-to-target` says the spec and the renderer had resolved `execute` / `target` in opposite directions, so one key now names the handler. + - `stack-api-require-auth-removed` names the declarations that replaced the deployment-wide opt-out: a public form, a share link or `book.audience: 'public'`. + - `retry-policy-converged` says why the merged default is 0 / 1: retry is opt-in, because a retry replays whatever the attempt already did. + - The flow-node alias entries say each one was an undeclared executor fallback that graduates into the conversion layer. + + The same goes for `FieldSchema.autonumberFormat`'s description (the `{0000}` default is a contract default every driver and the engine fallback read) and the descriptions of `GET /meta/:type/:name/layers` and `POST /meta/:type/:name/publish`. + + Text only: no conversion's id, surface, protocol step, transform or order changes, and no schema key, shape or default moves. A tool or test that matches the old summary text (for example a tracker-number suffix) needs the new spelling. The protocol 17 → 18 summaries are a later change. +- a387354: The protocol 17 → 18 conversion summaries no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + A conversion's `summary` is the line an author reads when upgrading metadata: `os migrate meta --json` reports it under `specChanges` (its chain already runs to protocol 18), and it becomes the "Change" column of the upgrade guide's protocol 17 → 18 table and the `to` text of `spec-changes.json`'s `converted[]` records once protocol 18 ships. Thirty-five of the protocol-18 summaries pointed at an issue-tracker number for the reason behind a rewrite. The number goes; where the sentence did not already say what was decided, it now does. For example: + + - The six duration-key renames (`hook.timeout` → `timeoutMs`, `apis[].cacheTtl` → `cacheTtlSeconds` and the rest) say the rule they follow: a duration key carries its unit in its name. + - `translation-per-app-settings-removed` says why both application doors lose `settings`: settings copy belongs to the platform, and the bundle entry and the translation item are two doors of one type that accept one shape. + - `flow-decision-mode-inclusive-explicit` says the decision node now follows mainstream engines (first match wins) and that taking every true edge must be declared. + - `list-view-sort-string-clause-to-array` and `page-component-filter-record-to-rule-array` say what "one orthography platform-wide" means for each, and why combinator filters are named rather than flattened. + + Text only: no conversion's id, surface, protocol step, transform or order changes, and no schema key, shape or default moves. A tool or test that matches the old summary text (for example a tracker-number suffix) needs the new spelling. +- f6b7520: The shared conformance tables' case notes and names no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + The conformance tables in `@objectstack/spec` (`FILTER_LOGIC_CASES`, `FILTER_TEXT_CASES`, `FILTER_COMPARAND_TYPE_CASES`, `AGGREGATION_CASES`, `TEMPORAL_ROWS` / `TEMPORAL_CASES` / `TEMPORAL_TIME_CASES`, `VALUE_ROUNDTRIP_CASES`, `TEXT_OPERATOR_DOOR_TYPE_CLASSES` and `METADATA_ROUNDTRIP_CASES`) are what every driver, and any third-party implementation, is measured against. A case's `note` or `why` is printed when that case fails, and some drivers print it in the test title. Fifty-eight of those texts pointed at an issue-tracker number for the reason a case exists. The number goes; where the sentence did not already say what was decided, it now does. For example: + + - The four empty-combinator cases say every face reduces an empty combinator to its boolean identity, and why `{}` and `$not: {}` follow from it. + - The no-value cases say `$ne`, `$nin`, `$notContains` and `$not` are NULL-safe on every face, and that `$exists` means "has a value" because SQL cannot tell a missing key from a stored null. + - The boolean-aggregand cases say a boolean is worth 1 or 0 on every face, including for `min` / `max`, and that this ruling superseded an earlier `false` / `true` answer. + - The `$empty` cases say every face answers `$empty` by the field's declared type. + + Six case names change with them: `icontains (the infix/view spelling, ruled never an alias of ilike) lowers to $icontains — …` in `FILTER_TEXT_CASES`, and five names in `FILTER_COMPARAND_TYPE_CASES` (the control cell, the bigint crash cell, and the three array-in-the-equality-slot refusals, which now say they are refused at the shared face). + + Text only: no case's filter, input, expected rows, verdict, error code, order or count changes, and no export, type or schema moves. A tool or test that selects or pins a case by its old note or name (for example by a tracker-number substring) needs the new spelling. +- 36e4647: The error-code waiver reasons and the public auth-feature registry's notes no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Two registries in `@objectstack/spec` carry a written reason beside each entry. In `@objectstack/spec/api`, every `STANDARD_SYNONYM_WAIVERS` and `PROVENANCE_WAIVERS` entry records why a registered error code is kept or placed where it is. In `@objectstack/spec/kernel`, `PUBLIC_AUTH_FEATURES` records how each public auth flag is consumed. Seventeen of those reasons pointed at an issue-tracker number for the decision behind them. The number goes; where the sentence did not already say what was decided, it now does. For example: + + - The five grandfathered synonyms (`CONFLICT`, `FORBIDDEN`, `INTERNAL`, `NOT_FOUND`, `UNAUTHORIZED`) say their consolidation onto the standard member is deferred until a code has a measured victim. + - The `FLOW_DISABLED` waiver says every door that dispatches a flow answers from one status table. The `TENANT_SCOPE_REQUIRED` waiver says an uninstall across every organization must be declared, never inferred from a missing one. + - The `phoneNumber` note names the fix the registry generalizes: create-user's phone field follows the opt-in phoneNumber plugin. + + One reason also corrects a stale fact. The `deviceAuthorization` exemption described a known gap in objectui's `DeviceAuthPage`. That gap was closed in objectui on 2026-07-15: the page reads the flag and says device authorization is not enabled, rather than calling the device-auth endpoints. The text now says so. + + Text only: no waiver's code, package, shadowed member or registration, no flag's surface, semantics or gated inputs, and no export, type, schema, order or count moves. Each reason still parses under its schema's non-empty rule. A tool that matches one of these reasons by its old text (for example by a tracker-number substring) needs the new spelling. +- 93a54b8: The protocol 16 → 17 upgrade rationale no longer cites tracker numbers; each cited decision is stated in words + + Clause-②: no + + `MIGRATIONS_BY_MAJOR[17].rationale` in `@objectstack/spec` is the prose an author reads when upgrading metadata from protocol 16. `os migrate meta` prints it for each hop, and `docs/protocol-upgrade-guide.md` reproduces it word for word under "Protocol 16 → 17". It pointed at 116 issue-tracker numbers (91 distinct records, four of them in sibling repositories). Every number is gone. Where the sentence already said what was decided, the number was dropped. Where the number stood in for the decision, the decision is now stated. For example: + + - The app-area paragraph says the fail-open area gates' caveat "was CLOSED by a server-side fix inside this same 17.0.0 window". The fix is described in the same sentence: `filterAppForUser` now runs the same `filterNav` over every `areas[].navigation`. + - The datasource paragraphs name the change that validates `datasource.config` against the declared driver's own config contract, and the follow-up that retired the factory's legacy key aliases. + - The aggregation paragraph names the freeze it relied on as the maintainer's freeze on the in-memory and MongoDB drivers, lifted 2026-08-11. It names the divergence class as the one closed when every SQL face got one aggregate spelling and one refusal. + - The `view.exportOptions` paragraph says PDF export was declined platform-side as not planned. + + Text only. No step, conversion, semantic entry, retired key or retired def changes: no id, order, schema or behaviour. The generated upgrade guide was regenerated from the new text. A tool that matched this rationale by its old text, for example by a tracker-number substring, needs the new spelling. +- f623e2f: The protocol 17 → 18 upgrade rationale no longer cites tracker numbers; each cited decision is stated in words + + Clause-②: no + + `MIGRATIONS_BY_MAJOR[18].rationale` in `@objectstack/spec` is the prose an author reads when upgrading metadata to protocol 18. `os migrate meta` prints it for that hop today, because its chain runs to the highest registered major, and `docs/protocol-upgrade-guide.md` will reproduce it once protocol 18 is cut. It is built from one fragment per retirement, and 49 of those fragments pointed at 88 tracker numbers: issue numbers in this repository and in objectui, and four decision-batch numbers. Every number is gone. Where the sentence already said what was decided, the number was dropped. Where the number stood in for the decision, the decision is now stated. For example: + + - The export-wildcard paragraph says the admin sets' wildcard was the export-axis twin of "the earlier removal of `member_default`'s CRUD wildcard". + - The `allowRestore` / `allowPurge` paragraph says the ruling "chose retiring the two bits over gating operations that do not exist", and that `allowTransfer` stays because the server guards who may rewrite a record's owner. + - The `reference_to` paragraph says the conversion is the server half of the ruling that the server normalizes the protocol and the renderer only executes it. + - The translation paragraphs give each ruling its date and its content: settings copy belongs to the platform, and one app metadata type has two authoring doors and one accepted shape. + + Text only. No fragment id or order, conversion, semantic entry, retired key or retired def changes: no schema or behaviour. No generated artefact prints step 18 yet, so none was regenerated. A tool that matched this rationale by its old text, for example by a tracker-number substring, needs the new spelling. +- 41a3c8d: Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it + + Clause-②: no + + `driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships: + + - `@objectstack/spec`: + - The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it. + - `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`). + - `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did. + - A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it. + - `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too. + - `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`. + - `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it. + + Comment only: no export, type, error code, status, message text or runtime behaviour changes. +- cfa4d74: `page.requires` says what the runtime now does with it: refused at save, reported at load (ADR-0080 §5). + + Clause-②: no + + The key's description used to say the list is "validated at save and load" while the liveness ledger recorded it as not enforced yet. Both are now true and say so. On a server that has the deployment's SDUI component manifest, saving a `kind: 'html'` page compiles its source, refuses a written `requires` that disagrees with it (`422 INVALID_METADATA`, `page-requires-disagrees-with-source`; a draft at its publish) and stores the derived list. At load, a stored page whose list names a plugin no manifest component carries is reported and still served. A server with no manifest checks neither and says so once at boot. Omit `requires`: it is derived from the source. The liveness row moves from `planned` to `live`, and the generated page reference carries the new description. + + `validateJsxPages`' reason for staying off the runtime publish gate no longer says it parses through `typescript`/`sucrase`. It parses with the dependency-free `@objectstack/sdui-parser`, and it stays CLI-only because the save door already runs that compiler on every html page. The `ui-html-page-div-refused` upgrade-guide entry now names that save door too: on a server with a manifest, a `div` page saved from Studio or through the metadata API is refused under the same rule ids. + + No schema accepts or refuses anything it did not before, and no runtime behaviour changes. +- 9b7a0ef: Liveness ledger README: the "Author warnings" section now describes the model the liveness lint ships. A `dead`, `live-elsewhere` or `experimental` verdict warns on its own, and `authorWarn` only opts a `planned` row in. + + Clause-②: no + + - The section said warnings were opt-in per ledger row, and that only `experimental` warned without the marker. That stopped being true when the lint made a `dead` or `live-elsewhere` verdict warn on its own. The section now has one table of which verdicts warn, and under which rule id. + - `authorHint` no longer "falls back to `note`". Every warning shows the row's `authorHint`, or else the verdict's default hint. The `note` never reaches an author. + - Rule 1 now talks about the verdict, not the marker. Grading a row `dead`, `live-elsewhere` or `experimental` warns every author who sets the key, and fails their `os lint --strict` / `os validate --strict` run. No marker keeps it quiet, so a benign display key is measured against the designer-previews ruling before it is graded `dead`. + - Rule 2 (booleans) now covers any key whose schema default materializes. It no longer points at an `_authorWarnSkipped` marker, which no ledger carries. + - The coverage paragraph states the walk's real reach: the types it visits, one level of `children`, and that a governed type it does not visit warns no author through this lint. + - Two sentences elsewhere in the README said a `dead` row needs `authorWarn` to warn. Both are corrected the same way. + - ⛔ Documentation only: no ledger row, schema, export or lint behaviour changes. +- 1c52a5e: fix(spec): the strict blueprint nav item's `label` describe says `null` inherits the target's current label + + Clause-②: no + + `SolutionBlueprintStrictSchema` is the output contract the AI design step generates against, and + strict mode makes every nav entry's `label` a required decision. Its describe read only "Nav entry + label, or null", so nothing the model reads said which of the two choices follows a rename of the + target, and the model was steered toward writing one. The describe now states the lenient + `BlueprintNavItemSchema.label` rule in the strict spelling: `null` ⇒ the entry inherits the CURRENT + label of what it opens at render time (a renamed target shows its new name); a string ⇒ rendered + verbatim, never a copy of the target's label. Write a label only when the entry must read + differently from what it opens. + + Describe text only: the key stays `z.string().nullable()`, so the schema accepts and refuses the + same blueprints. A pin holds the lenient and strict `label` describes to one rule. +- 3911901: fix(spec): a bound action's translation is read only under its own object, never from `globalActions` + + Clause-②: no + + The i18n resolver reads an action's translated copy at one address, chosen by the action's own `objectName`. This covers `translateAction`, `resolveActionLabel`, `resolveActionConfirm`, `resolveActionSuccess`, `resolveActionResultDialog`, and `translateObject` for an object's inline actions. + + - An action with an `objectName` reads only `objects.OBJECT._actions.ACTION`. + - An action with no `objectName` reads only `globalActions.ACTION`. + + Before this, a bound action with no object-scoped copy fell back to `globalActions.ACTION`. The fallback covered its label, description, confirm text, success message, outcome messages, params and result dialog. `TranslationDataSchema.globalActions` declares that group for object-less actions only. `os validate` already refuses, at error level, a `globalActions` key that names a bound action, and says the key is never read. The resolver now matches both. + + **What changes for a project.** A bundle that passes `os validate` is not affected. A bundle that keeps a bound action's copy under `globalActions` now shows that action's source text instead of the translation. `os validate` does not check a translation stored at runtime, so such a translation changes the same way. The fix is to move the keys from `globalActions.ACTION` to `objects.OBJECT._actions.ACTION`, where OBJECT is the action's `objectName`. The example apps under `examples/` and the translation bundles shipped in this repository's packages have no such key. +- 3a6d92f: fix(spec): `record:activity`'s props row names `items` / `loading` as the host's feed slot when it refuses them + + Clause-②: no + + `ComponentPropsMap['record:activity']` (`RecordActivityProps`) refused an authored `properties.items` or `properties.loading` with only the generic "Unrecognized key(s) on this `record:activity`" line. Both are keys the objectui `record:activity` renderer reads, as a feed a host that composes the block in code already owns, so an author copying a TSX composition into a JSON page met no reason for the refusal. + + - The refusal now says who reads each key on each mount the row reaches. On a standalone `record:activity`, `items` is the host's data channel and `loading` the host's fetch state for that feed. On a `record:chatter` / `record:discussion` `feed`, which is the same object, nothing reads either. The remedy is the same on both: omit them. The block then presents the record page's discussion feed, and a standalone `record:activity` with no discussion context fetches the record's own `sys_activity` rows. This is the same `guidance` shape `record:history`'s row already uses for `entries` / `loading`. + - The accept set does not change. Both keys stay refused, through the row and through `record:chatter` / `record:discussion`'s `feed`, which is the same object. Only the message text changes; `record:history` is unchanged. +- 7526058: docs(spec): an `object-master-detail-form` detail entry's `inlineMode` and `formFields` describes say what happens when the key is omitted on both of the renderer's paths (#21284) + + Clause-②: no + + - **Derived entry** (any entry that does not name both `relationshipField` and at least one column): an omitted `inlineMode` is resolved from the relationship field's `inlineEdit`, else from the child object's shape, and an omitted `formFields` is derived from the child object's fields. This is unchanged. + - **Entry kept as authored** (one that names both `relationshipField` and at least one column): the renderer resolves and derives nothing. An omitted `inlineMode` renders the collection as a grid, which offers the per-row form only when `formFields` lists more fields than `columns`. An omitted `formFields` means the per-row form is offered only when `inlineMode` is `form`, and it then draws the child object's full field list. + - The `inlineMode` describe used to say only "resolved from the relationship field's `inlineEdit` when omitted", and the `formFields` describe only "derived from the child object's editable fields when omitted". Neither holds for an entry kept as authored. The `formFields` describe also no longer says "editable": the derived list keeps `readonly` fields, as `deriveInlineRowFormFields` (`@objectstack/spec/data`) does. + - No schema accepts or refuses anything new. Only the two describes, the reference page that lifts them, and one source comment change. +- 53fd35e: The `kernel/cli-extension` module documentation no longer tells plugin authors to run `os plugins install`. Step 2, "Discover", said the plugin is listed in `@objectstack/cli`'s `oclif.plugins` array, or that users install it with `os plugins install`. Neither is true: `@objectstack/cli` declares no `oclif.plugins` and ships no plugin manager, so `os plugins` is not a command. The step now says what loads a plugin: oclif loads a plugin that the CLI's own `package.json` lists in both `oclif.plugins` and `dependencies`. To add a plugin's commands to `os`, build an `os` distribution whose own `package.json` lists the plugin in both places. The generated reference page carries the same text. + + Clause-②: no + + Documentation only. No schema, export or type changes. +- 16eefc6: docs(spec): an `object-master-detail-form` detail entry's `sortField` and `amountField` describes say what happens when the key is omitted on each of the renderer's paths (#21315) + + Clause-②: no + + - **Entry the renderer resolves** (any entry that does not name `relationshipField` together with at least one column whose every column has a `type`): an omitted `sortField` is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, and an omitted `amountField` is picked from the grid's number and currency columns. This is unchanged. It includes an entry that names `relationshipField` and columns of which some have no `type`: the renderer keeps that entry's `formFields` and `inlineMode` as authored, but it still derives these two. + - **Entry kept exactly as authored** (one that names `relationshipField` and at least one column, and gives every column a `type`): the renderer derives neither. An omitted `sortField` means the grid stamps no line position, so a drag-reorder is not saved. An omitted `amountField` means the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set. + - The `sortField` describe used to say only "derived from a `position` / `sort_order` / … field when omitted", which does not hold for an entry kept exactly as authored. The `amountField` describe said nothing about omission. + - No schema accepts or refuses anything new. Only the two describes and the reference page that lifts them change. +- db3fee3: A plain member's share-link list now answers: `GET /api/v1/share-links` is self-scoped for every signed-in caller, as ADR-0111 rules it + + Clause-②: no + + `ShareLinkService.listLinks` read `sys_share_link` under the caller's context. Both share-link doors force the list's `createdBy` to the caller, but the read still needed an object-level grant on `sys_share_link`, and the platform's member baseline does not grant one. So every plain member's list was refused, with or without an object filter, and the Share dialog, which loads this list when it opens, showed an error for them on every record. An admin's list answered. + + - The caller's own list is now read under the system context. This happens only when the caller has a non-empty user identity and the creator filter equals it. The read is constrained server-side to that identity, and each row it returns must pass the creator rule before it leaves. + - One creator rule now serves both `listLinks` and `revokeLink`. It never matches a caller with no user identity. Neither HTTP door reaches that case, because both answer 401 first, so for an internal caller with no user identity, `revokeLink` now refuses a link whose `created_by` is absent or empty instead of treating it as theirs. + - Every other list shape keeps the caller's context, as before: no creator filter, another user as creator, no user identity, or an admin listing someone else's links. A system caller keeps its bypass. + - The rows carry the same columns as before. The token comes back so the console can build the link URL, and the password hash never does. + - `@objectstack/spec`: the `IShareLinkService.listLinks` doc comment now describes the self-scoped own list. It previously said every listing is read under `context`. This is a doc comment only, with no type or export change. + - ⛔ No permission set changes, and no new grant on `sys_share_link`. +- 4c8363f: feat(plugin-sharing): the record owner and an explicit Modify-All holder may mint a share link on a record the data door refuses them (ADR-0111 D8 rule 1, ruling A′) (#21329) + + Clause-②: yes (widening) + + - **Who may mint.** `ShareLinkService.createLink` admits the caller when they can see the record, **or** own it, **or** hold `modifyAllRecords` on the object. The object's `publicSharing` opt-in is still checked first, and `publicSharing.eligibility` still last. On an object declared `access: { default: 'private' }` no wildcard grant covers the record, so its owner's own read is refused; the owner can now share it anyway. A member who neither sees nor owns the record is refused exactly as before, with the same envelope. + - **Who still needs visibility.** A hierarchy manager whose write depth covers the record's owner manages the record's shares (revoke, grant, list), but is not admitted to mint without seeing the record: a link creates access. + - **The organization wall.** Under the `group` and `isolated` tenancy postures the owner and Modify-All alternatives are withheld and visibility alone admits, as before this release. A member who left an organization still owns the records they created there, and must not be able to publish them by link. + - **A required capability.** Neither alternative applies past a capability the object requires (`requiredPermissions`). An owner or Modify-All holder who lacks it is refused with the capability gate's own refusal, as before this release; an owner who holds it, refused only because no permission set grants the object, mints. The verdict is read from the `required_permissions` layer of `ISecurityService.explain`, so a security service the sharing service reaches must implement `explain`. If it does not, the two alternatives are withheld. + - **API.** `SharingService.canMintWithoutVisibility(object, recordId, context)` answers the two alternatives with the owner and Modify-All branches `canManageShares` reads. `ShareLinkServiceOptions.canMintWithoutVisibility` is the late-bound probe `createLink` asks once the visibility read refuses, and `SharingServicePlugin` wires it. A host that constructs `ShareLinkService` itself without it keeps the visibility rule alone. The probe slice `SharingServiceOptions.securityService` returns gains an optional `explain`, the part of `ISecurityService.explain` the capability verdict reads. + - **`@objectstack/spec` (documentation only).** The `IShareLinkService.createLink` TSDoc states who may mint, replacing "you may only link-share a record you can yourself see". The `ISharingService.canManageShares` TSDoc describes the hierarchy-manager branch, which is implemented, and says it is not mint authority. No schema, key, type or export changes. +- c9c555a: fix(plugin-approvals)!: `role:` is no longer a position address, and the deprecated `role` approver type stops writing `role:` slots (ADR-0090 D3) + + Clause-②: no (narrowing) + + `position:` is now the one spelling of a position address. ADR-0090 D3 retired the word `role` with no alias window; the approvals service still read `role:` as a second spelling of the same position everywhere it compares a slot with the caller ("My Pending", the participant gate, `viewer.can_act`, and the slot test of every decision). The stock console now sends `position:`, so that arm is gone. + + **FROM → TO.** FROM `role:` → TO `position:`, wherever a caller names a position: the `approverId` filter of `GET /api/v1/approvals/requests`, and the `actorId` of approve, reject, send back, reassign, request info and comment. A `role:` ask now matches only a slot stored under that exact spelling, and a `role:` actor is refused with 403 `FORBIDDEN` ("cannot act as …"). + + **The writer.** An approver authored with the deprecated type `{ type: 'role', value: … }` already resolved as `org_membership_level` (the org-membership tier: owner, admin, member). When that lookup found no one, the request's fallback slot kept the authored spelling, `role:`, and a holder of a position with the same name decided it through the `role:` arm. That fallback now writes the canonical `org_membership_level:`, so no path writes a `role:` slot. A stored slot is never rewritten. + + Two classes of pending request are now decided only by an admin override: + + - a request a 15.x-era release opened, whose slot is stored as `role:`; + - a new request opened from a flow that still authors `{ type: 'role', value: '' }` and whose membership-tier lookup finds no one (its slot is `org_membership_level:`). + + **Author's one-line fix:** write `{ type: 'position', value: '' }`. `os lint` already reports the old form as `approval-approver-not-membership-tier` or `approval-approver-type-deprecated`. + + **Admin's one-line handling, both classes:** a platform admin (`admin_full_access`) or a tenant admin of the request's organization approves or rejects it (`POST /api/v1/approvals/requests/:id/approve` or `/reject`; recorded with `via_override: true`, and the flow run resumes), or reassigns it to the position's holder (`POST /api/v1/approvals/requests/:id/reassign` with `{ "to": "" }`), who then decides it normally. + + +- 68c5ab7: fix(spec): the null ordering-comparand refusals name only evaluation faces that exist, and say only what was measured + + Clause-②: no + + `FieldOperatorsSchema` and `ComparisonOperatorSchema` refuse a `null` comparand of `$gt` / `$gte` / + `$lt` / `$lte` with a pointed message. Its example of the evaluation faces disagreeing named + driver-memory's reference matcher, which has been deleted, so an author or agent reading the + refusal went looking for a face that no longer exists. The example now names two faces that exist + and were measured to disagree: driver-memory's query path reads a stored `null` as equal to the + comparand, so `{"$gte": null}` admits that row, while driver-sql compares against SQL `NULL` and + admits no row. + + That refusal and its runtime twin, the `parseFilterAST` refusal for the same comparand + (`Operator "$gt" on field "…" does not accept a null comparand …`), both said "no two evaluation + faces agree" on what an ordering against `null` matches. Measured, two faces do agree (driver-sql + and formula both admit no row), so both now say "the evaluation faces do not agree". + + Text only: each message's first sentence, its prescription (`{"$eq": null}` / `{"$ne": null}`), the + schema door's ruling sentence and the runtime door's "NOT applied" sentence are unchanged, and both + doors accept and refuse exactly the same filters. A client or log filter that matches the old + wording needs the new spelling. +- 5555047: The comment above `ViewSchema`'s `guidance:` states who writes a view container's `name`, and the rule every door applies to it + + Clause-②: no + + `src/ui/view.zod.ts` ships as source, and the comment also ships in the `ui` JavaScript output. It used to say that `saveMetaItem` sends a container's `name`, that artifact-shipped containers do, and that the validation sweep injects it. It now says the metadata door's own stamp (`normalizeViewMetadata`) is the only platform writer of the key. Artifact-shipped containers carry none, and the sweep passes its name as the request name. It also states the rule: when an authored `name` is set, it must equal the key the door files the container under, or the door refuses it. ⛔ No schema, parse, export or accept-set change. +- 41b1333: A view container's `form` is its default form: it is never collapsed into a named form, and no named form is promoted to default + + Clause-②: no + + `ViewSchema` declares `form` the container's default form and `formViews` additional named forms. `expandViewContainer` / `expandViewContainerWithDiagnostics`, which every view registrar shares, now serves exactly that. Behaviour changes for authors: + + - **A container with no `form` no longer serves its first named form as the default create/edit form.** Before, the first `formViews` entry was flagged `isDefault`, whatever it was: in the CRM example that was the anonymous Web-to-Lead form. Now no form item is flagged, and each named form is served only where it is asked for by name (a form action's `target`, `addRecord.formView`, a public `sharing.publicLink`). If you relied on the old promotion, move the intended create/edit form into `form`: `formViews: { edit: { … } }` becomes `form: { … }`, and a reference to `.edit` becomes `.form`. + - **A named form no longer replaces `form`.** Before, `form` was dropped when any named form shared its `type`, `label` and `columns`, even with different sections, and the first named form became the default. Now `form` is always served as `.form`, flagged `isDefault`, and is the only form item flagged. A named form whose body equals `form` stays its own named item. + - **The default `list` collapses only into a named list that restates its whole body.** A `listViews` entry that repeats `list` key for key, the list's own `name` aside (the "default == `listViews.all`" pattern), still folds into that one named item. A named list that shares `list`'s `type`, `label` and `columns` but differs in anything else (a filter, a sort) is now its own view, and `list` is served beside it as `.default`, the default list. Before, such a named list took the default's place, and the default list's own body was not served. + + The CRM and showcase examples move their create/edit form into `form`. The showcase task's `showcase_log_time` and `showcase_new_task` actions now target `showcase_task.form`. The public Web-to-Lead and contact-us forms stay named. + + ⛔ No schema, parse, export or accept-set change. +- eb9ef79: The `IObjectQLEngine.judgeFilter` docblock states that execution refuses an object the registry does not know before admission + + Clause-②: no + + The comment ships in the package's type declarations (`dist/*.d.ts`); `src/contracts/objectql-engine.ts` itself is not in `files[]`. It used to say that, for an object the registry does not know, the schema-free doors still judge "as at execution". Execution now refuses such an object before admission (`OBJECT_NOT_FOUND`, 404), so the comment says that answer is about the object, not the filter, and is not this member's verdict. ⛔ No schema, parse, export or accept-set change. +- f83d066: The `ApprovalActionRow` documentation now says what `reassign_from` and `reassign_to` hold. It said both were users. They hold a slot address in its stored spelling: a user id, an email, or a position address such as `position:legal`. A reassignment moves a slot, not necessarily a person, and the person who made the move is `actor_id`. The `reassign_from_name` and `reassign_to_name` documentation now says when a name resolves: only for a user id, or for an email an account carries. A position address never resolves, so a consumer renders the address when the name is absent. + + Clause-②: no + + Documentation only. No schema, export or type changes. +- fe10172: The metadata reads' `lock` / `editable` / `deletable` now say what the write doors do with a packaged item + + Clause-②: no + + Both metadata reads publish the ADR-0010 protection envelope beside the item: `GET /api/v1/meta/:type/:name/layers` (and its deprecated `?layers=true` spelling), and the by-name read `GET /api/v1/meta/:type/:name` where it resolves the envelope. The envelope was resolved from the item's own `_lock` alone, so it ignored the other refusal the write doors apply: an item a code package ships, on a type with no per-org overlay channel, is locked against in-place edits. + + **Before.** A packaged flow, action, object, hook, seed, mapping, datasource, external catalog, doc, picklist, field, job, api, capability or agent with no `_lock` read `lock: 'none'`, `editable: true` and `deletable: true`. A packaged page, app, dataset, book, permission set, position, tool or skill read the same. Yet `PUT` refused each of them with `403 NOT_OVERRIDABLE` (or `403 ITEM_LOCKED` when the write names the read-only package), and the removal of the first group was refused too. + + **After.** Each read reports what its doors answer: + + - The first group reads `lock: 'full'`, `editable: false` and `deletable: false`. + - The second group reads `lock: 'no-overlay'`, `editable: false` and `deletable: true`. Removing a leftover overlay row of these types is allowed: that is the repair path for overlays written before their per-org channel was withdrawn. + - Items of the overlay types (`view`, `dashboard`, `report`, `translation`, `email_template`) are unchanged. So are items no package ships, such as an organization's own flows and actions, and every item while the `OS_METADATA_WRITABLE` operator hatch opens its type. + + An item's own `_lock` still applies on top: the two refusals join, and neither replaces the other. `lockReason`, `lockSource` and `lockDocsUrl` are still present only when the item declares them. `provenance` and `packageId` already name the package. + + The verdict is the one the write doors already share, read rather than re-derived, so the read moves whenever a door moves. The `lock` field's description in `@objectstack/spec` now names both refusals it reports. No key, type or accepted value changes. + + **What to do.** Nothing, unless a client gated an edit or delete affordance on `editable` / `deletable`: it now hides that affordance for packaged items the server refuses, instead of offering a write that answers 403. The refusal itself names the sanctioned route for each type: for a packaged flow, clone it under a new name or switch it off; for a packaged action, switch it off. +- 9d91f58: `JobSchema.body`'s description now says an enabled `pull` job installs through `os package install` when its `pull` binds and is refused when it does not + + Clause-②: no + + The description said `os package install` refuses an enabled job with no `body`, "a `pull` job excepted: it is data too". Read plainly, that says the install never refuses a `pull` job. That stopped being true when the install-local door (`os package install`, `POST /api/v1/marketplace/install-local`) began refusing an enabled job whose `pull` does not bind, with the same `422 VALIDATION_ERROR` it gives a job whose `body` the declaration refuses. + + The sentence now reads: a `pull` is data too, so an enabled `pull` job is judged by its `pull` instead. It installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. The generated reference page for `job` carries the same text. + + Text only: no key, schema shape, condition, error code or status moves. A tool or test that matches the old sentence needs the new one. +- 9059082: `reportForm`: the "Joined blocks" repeater's `dataset` column now declares `widget: 'ref:dataset'` and `required: true`. Studio's report inspector draws a joined report's block `dataset` as the dataset picker, with the required marker, instead of a free-text cell with no marker. + + Clause-②: no + + - **Why.** A `joined` report refuses a block that binds no `dataset`, at `blocks.N.dataset`. The form offered that column as plain text and did not mark it, so a newly added block failed on save. + - **Which renderer honours it.** The pinned console registers `ref:dataset` in its widget registry. Its repeater takes each column's widget and `required` from the row spec, in both the grid and the card layout. + - ⛔ Only this one form row changes. No schema, parse, export or accept-set change: `JoinedReportBlockSchema.dataset` stays optional on the block shape, and the joined arm's refusal is unchanged. +- 2df3d13: Studio's object form offers `imageField`, the record's picture, as a text row beside `nameField` + + Clause-②: no + + The object form in the metadata form registry now has an `imageField` row, a plain text input placed beside `nameField`. Until now the only way to set the record picture from Studio was the Source tab's raw JSON. The help text says what the parse accepts: a field of this object whose type is `image` or `avatar`. Left empty, the object has no record picture and no placeholder is drawn. The row brings no picker and no validator of its own. A name that is not an `image` / `avatar` field of the object is refused when the object is saved, by the same parse rule as before. + + `@objectstack/platform-objects` ships the row's label and help text in its metadata-form translation catalogs, translated for `zh-CN`, `ja-JP` and `es-ES`. + + No schema key, accept set, refusal, error code or status changes, and you have nothing to re-author. +- 07bf21f: `ObjectSchema.imageField`'s description no longer says the renderer is pending: the record page header draws the picture + + Clause-②: no + + The console's record page header now reads `imageField`: it draws the named `image` / `avatar` field's value in the record chip beside the title, an `avatar` round and cropped, an `image` whole, and a record whose field is empty shows no picture. The `.describe()` text that `os validate`, the JSON Schema and the reference docs carry dropped its last sentence, "Pending renderer: the record chrome does not draw it yet.", and now says the header draws the picture rather than is to draw it. + + Text only: no key, schema shape, refusal, error code or status moves. An `imageField` you already authored takes effect as it is, with nothing to re-author. +- 53021e3: fix(plugin-security)!: on the write doors, a row the caller cannot read answers what a nonexistent id answers + + Clause-②: no (narrowing) + + + + **BREAKING**: a by-id update or delete of a row the caller cannot read now answers `404 RECORD_NOT_FOUND`, with exactly the body an id that names no row gets, for every principal class. On the write doors, "hidden" and "gone" are now one answer to a caller who cannot read the row. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new. + + **What changed.** The answer used to depend on which gate saw the row first. Where a write-class row filter binds the caller, the by-id write pre-image check answered `403 PERMISSION_DENIED`. Where none binds it, a later gate answered with its own 403: `FORBIDDEN` from record sharing, or a parent-derived gate's code on attachments and comments. Meanwhile a nonexistent id answered `404`. So the write door could tell a hidden row apart from a missing one. The pre-image check now asks the read door's own question first, for the by-id write the caller addressed: a by-id read in the caller's context, every data middleware's visibility included. A row that read does not return gets the read door's not-found producer. A store fault propagates as raised, and a read-time policy refusal is not treated as absence. + + **What is refused now that was not.** A principal that no write-class row filter binds could have its by-id write admitted on a row the read door hides from it. One example is the uploader of an attachment, or the author of a comment, whose parent record they can no longer read. That write is now refused with the not-found answer, as it already was for every principal a row filter binds. + + **FROM → TO.** A by-id update or delete of a row hidden from the caller: FROM a `403` (`PERMISSION_DENIED`, `FORBIDDEN`, or a parent-derived gate's code) → TO `404 RECORD_NOT_FOUND`, the body a nonexistent id gets. + + **If you are affected.** A client that read a by-id write's `403` as "the row exists, but you may not change it" should read `404 RECORD_NOT_FOUND` the way the read door means it: no row you can see has this id. + + **Unchanged.** + - A caller who can read the row but may not write it keeps its 403. They already see the row. + - By-id writes the platform issues under the caller's context keep their previous answer, because the caller never named their target: the engine's cascade delete of a dependent row, a hook's write, and the referential clear of a lookup. + - Writes that are not routed by id are unchanged. + + `security/explain` follows enforcement. Its record verdict for an update or delete of a record the principal cannot read is now the missing-record shape: `visible: false`, with no decider. +- ba57588: The settings audit trail records a secret-valued setting (an encrypted key) with the crypto provider's keyed digest, never an unkeyed one (#21792). + + Clause-②: no + + - **Both ledgers.** The `sys_audit_log` `config_change` row (`valueDigest`, spelled ``) and the `sys_setting_audit` row (`new_hash`) now carry `ICryptoProvider.keyedDigest(value)` for a secret-valued setting. Before, they carried the unkeyed `digest` (`sha256:…`). This covers the `sys_secret` path and the legacy inline-adapter path. + - **Change detection still works.** The keyed digest is stable for equal values under one key, so the trail still shows whether a secret changed and whether it went back to an earlier value. Rotating the data key changes every later fingerprint. Rows written before this release keep their old `sha256:` value. + - **No keyed digest, no fingerprint.** When no crypto provider is wired (a host that builds `SettingsService` with only a `CryptoAdapter`), or the provider refuses a keyed digest, the audit rows record the write with no value fingerprint: `valueDigest` is `` and `new_hash` is null. The service logs this once per key at `warn`. The settings write itself is never refused for it. The adapter's own `digest` is no longer used for secrets. + - **Non-secret settings are unchanged.** They keep the adapter's `digest` of the canonical JSON. + - **Contract text (`@objectstack/spec`).** The `ICryptoProvider` docs for `digest` and `keyedDigest` now state the rule: a secret's audit fingerprint comes from `keyedDigest`, never from `digest`, and with no keyed digest the trail records none. No type, export or schema changes. +- cab6396: fix(plugin-security)!: a predicate-scoped update or delete matches only the rows the caller can read + + Clause-②: no (narrowing) + + + + **BREAKING**: a predicate-scoped (`multi: true`) update or delete now matches only the rows the caller can read. A row the read door would not return to the caller is not written, not counted and not refused, so a predicate that reaches only such rows answers exactly what a predicate that matches nothing answers: success, zero rows. This is the by-id write doors' rule ("hidden" and "gone" are one answer to a caller who cannot read the row) carried to the predicate door. It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed, and no error code is new. + + **What changed.** The rows a predicate write matched came from its write scope alone. A row the caller cannot read was matched whenever that scope reached it, for example through a write-class row-level policy wider than the read policy, or on an object whose read visibility follows a parent record. A per-row gate then refused the whole write with a `403`, or the row was written and counted. Either answer told a hidden row apart from no row. The write middleware now asks the read door which rows the caller's own predicate returns, through a read in the caller's context that every data middleware's visibility applies to, and narrows the write to those rows: readable ∩ writable. A read the read door refuses (no read grant on the object) keeps the write's previous answer. A store fault on that read propagates as raised. + + **What is refused or narrowed now that was not.** + - A predicate write no longer writes, counts or refuses rows its caller cannot read, including rows its write scope reaches. + - A predicate write whose predicate matches more than 10 000 rows the caller can read is refused with `400 INVALID_FILTER`, before anything is written, rather than narrowed by a cut-off list. The limit is the platform's existing row ceiling for one predicate write. + + **FROM → TO.** + - A predicate update or delete reaching rows the caller cannot read: FROM a per-row gate's `403`, or those rows written and counted → TO those rows not matched; success with zero rows when no readable row matches. + - A predicate update or delete whose readable match exceeds 10 000 rows: FROM attempted → TO `400 INVALID_FILTER`, nothing written. + + **If you are affected.** An operator who needs a user to change rows grants that user read access to them first. A caller that read a predicate write's `403` as "a row exists here" reads the result as the count of rows it can see. A predicate whose readable match is over the ceiling is narrowed and written in batches. + + **Unchanged.** + - A caller who can read a matched row but may not write it keeps its answer. + - Writes the platform issues under the caller's context keep their previous answer, because the caller never addressed them: a cascade, a hook's own write, and the referential clear of a lookup. + - By-id writes keep their answers. System-context writes are not narrowed. + - `security/explain` takes no predicate, so it has no predicate-write verdict to change. +- 8e35895: Studio's action form now offers `onSuccess` (the route an `api` or `script` action opens once it succeeds, and whether it opens in place or in a new tab) and `outcomeMessages` (a JSON map from each `outcome` the handler returns to the success message shown for it), with their labels and help text translated for `zh-CN`, `ja-JP` and `es-ES`. +- 1f04696: fix(trigger-record-change)!: a record-change flow's trigger record carries the credential mask and omits internal fields + + Clause-②: no + + + + **BREAKING**: the `record` and `previous` a record-change flow receives are now served on the generic read path's terms (ADR-0100). A credential-class field — every `secret` field, and every `password` field outside the exempt `managedBy` buckets — reads as the mask `SECRET_MASK` when set and `null` when unset, and a field declared `internal: true` is absent. It ships as `minor` under the launch-window convention for a changed answer. No export, schema key or error code is added or removed. + + **What changed.** The trigger built both roots from the engine's own write result, which keeps the stored row whole for privileged in-process callers. A credential's stored value and an internal field's value therefore reached the flow, and from there its variables map, a paused run's persisted state and the read doors over that state. The trigger now projects both roots through `omitInternalFieldsFromWriteResponse` from `@objectstack/core`, the helper every external write response already uses, with the trigger object's definition. Everything downstream inherits the projection: the variables map, a paused run's persisted state and its read doors, and the run a resume rehydrates, in the same process and after a restart. + + **FROM → TO.** + - `{record.}` and `{previous.}` in a record-change flow: FROM the stored value (the plaintext password, or the secret's stored handle) → TO `SECRET_MASK` when set, `null` when unset. + - `{record.}` and `{previous.}`: FROM the stored value → TO absent. + + **If you are affected.** A flow that needs a credential reads it through a privileged binder (the flow credential channel, or a privileged server-side read such as the engine's `resolveSecretField`), never off the trigger record. A start or edge condition that compared such a field with a literal tests whether it is set (`!= null`) instead. A condition that compares `record.` with `previous.` now sees two equal masks whenever the field is set on both sides, so it can no longer detect a change; use a privileged binder to detect a credential change. + + **Runs stored before this release.** The mask applies to trigger records built after the upgrade. Paused runs, and terminal runs that keep a restorable snapshot, created before it still hold the clear values in `variables_json`, `context_json` and `steps_json`. After upgrading, resume, cancel or purge those runs. + + **Unchanged.** + - Every ordinary field of the trigger record keeps its value, and every other flow variable is untouched. + - The engine's own write result, the stored row and the privileged read paths (`resolveSecret`, `resolveSecretField`) are unchanged. + - Records a flow reads later through its data nodes already came through the generic read path, which masks them. +- bab7685: A served object field no longer carries an undeclared `help` key. `translateObject` now serves a field's translated help on the field's `description` (#21948). + + Clause-②: no + + - **What moved.** A bundle's `objects..fields..help` entry is the translation of the field's `description`, because the i18n extractor writes it from that key. `translateObject` (and so `GET /api/v1/meta/object/:name` in a non-English locale) used to put it on a `help` key that `FieldSchema` does not declare. The served field then failed `FieldSchema` with `unrecognized_keys`. A consumer that reads only declared keys rendered the English `description`, and the console logged one ingestion warning per such field. The translation is now served on `description`, and the served field carries no `help`. + - **Precedence (ADR-0029 D9.2a).** The catalog applies only while the served field's `description` still equals the packaged field's. This is judged by the same comparison the object scalars, views and dashboards use. A description that diverged (an `objectExtensions` field, or a tenant's own edit) keeps its authored value in every locale. With no packaged base supplied, the catalog applies, as before. A field's `label` is unchanged and stays a flat `catalog ?? document`. + - **`ObjectFieldLike`** (`@objectstack/spec/system`) drops its `help?: string` member. Its `[key: string]: any` index signature still accepts and types a `help` key, so a caller that writes or reads one still compiles. `inlineHelpText` is not touched. + - Readers that fall back from `help` to `description` (`field.help || field.description`) render the same translated text as before. + - ⛔ No schema, parse or export change. The translation bundle's own field `help` key is unchanged. +- fb69825: The datasource read redaction resolves a driver's identity the way its sibling helper does. The per-driver half of `redactableConfigKeys` now looks a driver up through `resolveDriverId`, the resolver `passthroughSecretPaths` and the write door's contract lookup already use. So every spelling the write door accepts as a builtin driver is redacted as that driver. + + Clause-②: no + + - The still-writable credential key is withheld on the datasource admin read (`GET /api/v1/datasources/:name`) and on the metadata read under every accepted spelling of its driver, and `redactedConfigKeys` names it. + - A crafted driver id that made the read throw now answers as a driver the platform ships no contract for: the canonical credential spellings and the former aliases are withheld, and the read succeeds. + - `restoreRedactedConfig` and the credential migration read the same list, so an untouched Save still restores the stored value under every accepted spelling, and the migration names the still-writable key as residue there too. + - Unchanged: what the write door accepts, every export and its type, and the answer for a canonical spelling. +- 8832655: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `0abd4f9f8769`. + + Clause-②: no + + Every anchor was mapped through the objectui diff `9dfaca654311..0abd4f9f8769`, 50 paths over five commits. None of those paths is an objectui file that an asserting record cites, so every cited file is byte-identical across the hop (`git diff --quiet`) and every anchor held unmoved. The seven quoted anchor lines verify against objectui at the new pin. Three records carry a count, and each count was re-taken by its record's own method with the same reading: the `keyboardNavigation` hit lines (15, against 3 for the `schema.editable` control), `ObjectKanban.tsx`'s `quickAdd` / `onQuickAdd` (2 each, against 11 for `onCardClick`), and the `ElementDataSourceGate` occurrences in five `src/index.tsx` shells (0, 3, 3, 3 and 4). + + The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that first reproduced every `9dfaca654311` number. The corpus is now 7650 tracked files. All 98 checked tokens read as before: every zero still reads zero, and `Span` / `SpanSchema` still read 508 / 57. + + No key, default, enum member or export moves. +- 100f68b: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `2e818d0b51ec`. + + Clause-②: no + + Every anchor was mapped through the objectui diff `ab1879721595..2e818d0b51ec`. Every file a current anchor cites is byte-identical across the hop except four, and in those the cited text is byte-identical too: + + - `plugin-dashboard/src/index.tsx`: objectui#11466 added lines above the `object-metric` registration, so the `object-metric` icon input record moves from `:269` to `:281`. It is still `{ name: 'icon', type: 'string' }`. + - `packages/types/src/objectql.ts`: objectui#11216 declared `grouping` on `ObjectKanbanSchema` below the cited `limit` member. + - `plugin-kanban.mdx`: objectui#11216 added a `grouping` row below the cited `limit` row. + - `SchemaRenderer.tsx`: objectui#11466 changed the type of `schema`. Its `properties.*` hoist and `createElement` spread are unchanged. + + The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that reproduces the previous pin's numbers. objectui's 17.7.0 release removed 2726 consumed changesets, so the corpus is now 7579 tracked files. Every zero still reads zero. + + No key, default, enum member or export moves. +- 8963dbf: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `89cad75d5570`. + + Clause-②: no + + Several records were corrected rather than moved, because objectui changed what they describe on this hop: `object-map` now reads `mapStyle` ahead of `map.style` on the declared-block path as well (objectui#11168 slice 3), so the `getMapConfig` return quote is rewritten; `object-tree`'s `navigation` read and `@object-ui/types`' `ObjectTreeSchema` mirror now carry the spec row's keys with no cast, and objectui's record-source table no longer lists the retired bare `tree` / `view:tree` keys (objectui#10859 batch 8); `object-map`, `object-gantt` and `object-timeline` publish the further keys their rows declare (objectui#11168 slices 3–5). Two stale `object-timeline` anchors (`filter` and `variant`) that were already one line off at the previous pin are corrected. No key, default, enum member or export moves. +- 1354e7b: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `9dfaca654311`. + + Clause-②: no + + Every anchor was mapped through the objectui diff `2e818d0b51ec..9dfaca654311` and re-read at the new pin. One cited line changed: `ObjectKanban.tsx:10`, the type import, gained `SortConfig` beside the `ObjectKanbanSchema` the record cites. Every other change in a cited file sits outside the cited lines, and the anchors moved with their text byte-identical: + + - `plugin-grid/src/ObjectGrid.tsx`, `plugin-tree/src/ObjectTree.tsx`, `plugin-gantt/src/ObjectGantt.tsx`, `plugin-calendar/src/ObjectCalendar.tsx`, `react/src/SchemaRenderer.tsx`, `plugin-map/src/index.tsx`, `plugin-gantt/src/index.tsx` and `plugin-view/src/ObjectView.tsx`: objectui#8347 re-worded docblocks that described `BaseSchema`'s index signature. Anchors below those docblocks moved by at most three lines. + - `plugin-kanban/src/ObjectKanban.tsx`: objectui#8347 added a private `GateBoundKanbanSchema` read type above the board's fetch, so the fetch, the navigation reads and the spread into `KanbanBoardCore` moved by 30 lines. The board still reads `limit` and `navigation` as `ObjectKanbanSchema` declares them. + - `plugin-kanban/src/index.tsx`, `plugin-dashboard/src/index.tsx` and `react/src/element-data-source/ElementDataSourceGate.tsx`: objectui#11605 made `objectName` a non-required input and added the "no object named" hint. The `object-metric` icon input moved from `:281` to `:299`, and it is still `{ name: 'icon', type: 'string' }`. + - `components/src/renderers/layout/containers.tsx`: objectui#11619 added the record picture to the record chrome. The `page:tabs` and `page:accordion` icon anchors moved by three lines. + - `packages/types/src/objectql.ts` and `packages/types/src/zod/objectql.zod.ts`: objectui#11615, objectui#11266 and objectui#8347 grew declarations above the cited members. `ObjectKanbanSchema.limit`, `ObjectMapConfigSchema` and `LIST_VIEW_LOCAL_OVERRIDES` moved with their text byte-identical. + + The six migration entries' corpus counts were re-taken with `git grep -o -F`, the method that first reproduced every `2e818d0b51ec` number. The corpus is now 7632 tracked files. Every zero still reads zero; the three new `Span` hits are a `colSpan` in an objectui test. + + No key, default, enum member or export moves. +- 1cbe165: The spec's objectui citations, and the shipped description text that names the `.objectui-sha` pin (the `FormField.span` describe and six migration-entry descriptions), are re-measured against the new console pin, objectui `ab1879721595`. + + Clause-②: no + + Several records were corrected rather than moved, because objectui changed what they describe on this hop. + + - **`object-grid` `keyboardNavigation`.** The grid now reads the key (objectui#11068), so its describe drops the `[EXPERIMENTAL — not enforced]` marker and the sentence that said no renderer reads it and authoring it changes nothing. The describe now says what the grid does: the data cells become one Tab stop that the arrow keys, Home / End and Ctrl+Home / Ctrl+End move between. It is on by default when the grid renders editable, `true` turns it on for a read-only grid, and `false` turns it off on an editable one. + - **`object-grid` `emptyState`.** Its record now says the grid resolves `title` and `message` against the display locale (objectui#11227), so an inline locale map draws. + - **`ActionSchema.outcomeMessages`.** The console reader landed (objectui#11344). The key's liveness row is now `live`, and authoring it no longer draws the "the console does not show outcome copy yet" author warning. The `successMessage` row records that `${result.*}` is interpolated. + - **The four `action:*` rows.** The action renderers forward `outcomeMessages` to the action runner. The `action:button` and `action:icon` rows record it as a key the renderer forwards and the row does not declare. The `action:group` and `action:menu` rows record that each member's own `outcomeMessages` rides the member forward. + + Every other anchor either held on a byte-identical file or moved with its cited text byte-identical. The corpus counts in the six migration entries were re-taken with the method that reproduces the previous pin's numbers. No key, default, enum member or export moves. +- 15fe567: Six provenance comments in `src/data/` and `src/ui/` were re-anchored + + Clause-②: no + + Six comment and docblock lines in `src/data/datasource.zod.ts`, `src/data/filter.zod.ts`, + `src/data/value-roundtrip-conformance.ts` and `src/ui/component.zod.ts` cited tracker numbers + that no longer resolve on GitHub. Each now cites the commit in this repository's history that + decided the matter, and the datasource comment also says in words which refusal it means: a + credential written into mongo's `options` passthrough. The live numbers beside them stay. + Comments only: no type, schema, export or runtime behaviour changes. +- 0bddffd: Nine migration-step rationale passages state their decisions in words instead of tracker numbers, and two registry comments cite the commit that decided them + + Clause-②: no + + The protocol 17 and protocol 18 step rationales are what `os migrate meta` shows per hop + and what the protocol upgrade guide prints. Nine of their passages named GitHub issues + that no longer exist, so an upgrading author met a number with nothing behind it. Each of + those passages now carries no number at all and says what was decided: why `mongo` and + `mongodb` are both accepted, why the form-view option `default` and `connector.errorMapping` + were retired, which earlier cleanup the import mapping `lookup` params finish, what the + memory driver's placeholder refusal extends, how the plugin manifest's `contributes` + members and `routes` were retired, and why the stack `themes` carrier and the + component-translation `submitLabel` key went. Two source comments of the migration + registry now cite the commit behind them. Text only: no migration step, entry, retired key + or def, conversion, schema, export or runtime behaviour changes. +- 7e0066a: Two provenance comments that tests read literally were re-anchored + + Clause-②: no + + The removal note on `DATA_ACTION_TO_API_OPERATION` in `src/data/api-derivation.ts` and the + explanatory block about `ApiKeySchema` in `src/identity/identity.zod.ts` cited tracker numbers + that no longer resolve on GitHub. Each now opens with the commit in this repository's history + that decided the matter: 6968885ef removed the producer-less `batch: 'bulk'` alias row, and + 2c86fe3ea deleted `ApiKeySchema` on the maintainer's ruling. The unit tests that read those two + comments moved with them, and the same re-anchoring was applied to the comments in the + package's test files, which do not ship. Comments only: no type, schema, export or runtime + behaviour changes. + ## 17.6.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index 7df91746a50..509499f26c6 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.6.0", + "version": "17.7.0", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index 297066d1828..e1ec6b43ea0 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,142 @@ # @objectstack/trigger-api +## 17.7.0 + +### Minor Changes + +- 96a9719: feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790) + + Clause-②: yes (widening) + + A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it. + + **⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '' (): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification. + + What else changes: + + - **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`. + - **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential. + - **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials. + - **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing. + - **Refused now, loudly**: + - With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again. + - The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret. + + + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 5dd26fd9602..4c2e1f4f6e8 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index 24cd1b90c12..2143e70430d 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,160 @@ # @objectstack/plugin-trigger-record-change +## 17.7.0 + +### Minor Changes + +- 1f04696: fix(trigger-record-change)!: a record-change flow's trigger record carries the credential mask and omits internal fields + + Clause-②: no + + + + **BREAKING**: the `record` and `previous` a record-change flow receives are now served on the generic read path's terms (ADR-0100). A credential-class field — every `secret` field, and every `password` field outside the exempt `managedBy` buckets — reads as the mask `SECRET_MASK` when set and `null` when unset, and a field declared `internal: true` is absent. It ships as `minor` under the launch-window convention for a changed answer. No export, schema key or error code is added or removed. + + **What changed.** The trigger built both roots from the engine's own write result, which keeps the stored row whole for privileged in-process callers. A credential's stored value and an internal field's value therefore reached the flow, and from there its variables map, a paused run's persisted state and the read doors over that state. The trigger now projects both roots through `omitInternalFieldsFromWriteResponse` from `@objectstack/core`, the helper every external write response already uses, with the trigger object's definition. Everything downstream inherits the projection: the variables map, a paused run's persisted state and its read doors, and the run a resume rehydrates, in the same process and after a restart. + + **FROM → TO.** + - `{record.}` and `{previous.}` in a record-change flow: FROM the stored value (the plaintext password, or the secret's stored handle) → TO `SECRET_MASK` when set, `null` when unset. + - `{record.}` and `{previous.}`: FROM the stored value → TO absent. + + **If you are affected.** A flow that needs a credential reads it through a privileged binder (the flow credential channel, or a privileged server-side read such as the engine's `resolveSecretField`), never off the trigger record. A start or edge condition that compared such a field with a literal tests whether it is set (`!= null`) instead. A condition that compares `record.` with `previous.` now sees two equal masks whenever the field is set on both sides, so it can no longer detect a change; use a privileged binder to detect a credential change. + + **Runs stored before this release.** The mask applies to trigger records built after the upgrade. Paused runs, and terminal runs that keep a restorable snapshot, created before it still hold the clear values in `variables_json`, `context_json` and `steps_json`. After upgrading, resume, cancel or purge those runs. + + **Unchanged.** + - Every ordinary field of the trigger record keeps its value, and every other flow variable is untouched. + - The engine's own write result, the stored row and the privileged read paths (`resolveSecret`, `resolveSecretField`) are unchanged. + - Records a flow reads later through its data nodes already came through the generic read path, which masks them. + +### Patch Changes + +- 6091136: MCP stdio, email, knowledge, queue, SMS, storage and record-trigger refusals, warnings and template descriptions no longer cite tracker numbers; each one states the decision behind it in words + + Clause-②: no + + Some strings these seven packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + + - `@objectstack/connector-mcp`: the declarative stdio refusals say a stdio transport launches a local process, so stack metadata may only name a command the host's own code allows, and that an http transport is not gated by this policy. + - `@objectstack/plugin-email`: the built-in change-email notice template's description, in all four locales, says the notice goes to the previous address so a hijacked session cannot move the account identity unannounced; the internal-headers refusal says a missing header does not announce itself, so the send would succeed while silently deviating from what was authored; the over-limit attachments line says the storage capability holds large content outside the row while the row keeps a reference and the attachment's audit metadata. + - `@objectstack/service-knowledge`: the no-identity retrieval warning says a missing identity is not a grant of authority, so retrieval fails closed rather than searching the whole corpus unscoped; the predicate-write warning says the lifecycle reap guard de-indexes retention-swept rows before they are deleted. + - `@objectstack/service-queue`: the missing-retention refusal says the one platform reaper sweeps completed rows by that declaration, so the adapter does not sweep the table itself; the rejected-floor error says the floor is what makes the lifecycle service refuse an override below the idempotency window. + - `@objectstack/service-sms`: the unreadable-counter warning says a quota the platform cannot count must not refuse the one-time codes users sign in with; the counter store's lines name the daily SMS send quota without a number. + - `@objectstack/service-storage`: the reclamation-gate line says deleting bytes cannot be undone, so it waits for a verified migration with no deviation on record, while reversible work carries on. + - `@objectstack/trigger-record-change`: the array-trigger warning says multi-event arrays are deferred until two independent projects need a combination other than created-or-updated. + + Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 8a74d9e2e4e..c949f625626 100644 --- a/packages/triggers/trigger-record-change/package.json +++ b/packages/triggers/trigger-record-change/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-record-change", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Record-change flow trigger for ObjectStack — auto-launches flows on object insert/update/delete via ObjectQL lifecycle hooks (ADR-0018)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index 71b9ced647c..5e3bf460d9a 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,173 @@ # @objectstack/plugin-trigger-schedule +## 17.7.0 + +### Minor Changes + +- 748b240: feat(types,automation): a host's per-kernel scheduled-work OFF reports the host's own reason (#21110) + + Clause-②: yes (widening) + + `ScheduledWorkPolicy` (`@objectstack/types`) gains an optional + `hostDisabledReason`: the host's own sentence for why scheduled work is off on + this kernel, such as a plan that does not include scheduled flows. A new + export, `scheduledWorkDisabledReason(policy)`, gives the one answer for why + scheduled work is not armed under a policy. It returns the host's reason when + the policy carries one, and `SCHEDULED_WORK_DISABLED_REASON` otherwise. + + Every refusal site now reports that answer, read from the same policy reading + that refused: + + - the automation engine's bind log; + - the reason it records for `getTriggerBindingAudit()` and for the + `FlowRuntimeState.reason` that `GET /automation/_status` serves; + - the refusal of `ScheduleTrigger` and `TimeRelativeTrigger` when a host drives + them directly. + + Before this, a kernel that a host turned off through `scheduledWorkPolicy` + was reported with the deployment sentence. That sentence tells the reader to + set `OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true`, even on a process where the + variable is already set, and to a tenant who cannot set it. + + Nothing changes without the new field. A policy with no `hostDisabledReason`, + and the zero-argument deployment resolver `resolveScheduledWorkPolicy()`, which + never sets it, report `SCHEDULED_WORK_DISABLED_REASON` byte for byte. The field + is read only when `enabled` is `false`. + + To use it, a host that turns one kernel off for its own reason sets + `hostDisabledReason` on the `enabled: false` policy it already hands to that + kernel's `AutomationServicePlugin`, `ScheduleTriggerPlugin` and + `TimeRelativeTriggerPlugin`. Give the same policy to all three, as before, and + make the reason a whole sentence that names the cause and the remedy. It is + shown verbatim. + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [a6a7547] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [a0176ef] +- Updated dependencies [e1790fd] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [e6dc7a2] +- Updated dependencies [d16b9fb] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index 6849de52717..30182b6137d 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack \u2014 auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)", "main": "dist/index.js", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 716ff4598f9..3b488d8d6a9 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,188 @@ # @objectstack/types +## 17.7.0 + +### Minor Changes + +- 748b240: feat(types,automation): a host's per-kernel scheduled-work OFF reports the host's own reason (#21110) + + Clause-②: yes (widening) + + `ScheduledWorkPolicy` (`@objectstack/types`) gains an optional + `hostDisabledReason`: the host's own sentence for why scheduled work is off on + this kernel, such as a plan that does not include scheduled flows. A new + export, `scheduledWorkDisabledReason(policy)`, gives the one answer for why + scheduled work is not armed under a policy. It returns the host's reason when + the policy carries one, and `SCHEDULED_WORK_DISABLED_REASON` otherwise. + + Every refusal site now reports that answer, read from the same policy reading + that refused: + + - the automation engine's bind log; + - the reason it records for `getTriggerBindingAudit()` and for the + `FlowRuntimeState.reason` that `GET /automation/_status` serves; + - the refusal of `ScheduleTrigger` and `TimeRelativeTrigger` when a host drives + them directly. + + Before this, a kernel that a host turned off through `scheduledWorkPolicy` + was reported with the deployment sentence. That sentence tells the reader to + set `OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true`, even on a process where the + variable is already set, and to a tenant who cannot set it. + + Nothing changes without the new field. A policy with no `hostDisabledReason`, + and the zero-argument deployment resolver `resolveScheduledWorkPolicy()`, which + never sets it, report `SCHEDULED_WORK_DISABLED_REASON` byte for byte. The field + is read only when `enabled` is `false`. + + To use it, a host that turns one kernel off for its own reason sets + `hostDisabledReason` on the `enabled: false` policy it already hands to that + kernel's `AutomationServicePlugin`, `ScheduleTriggerPlugin` and + `TimeRelativeTriggerPlugin`. Give the same policy to all three, as before, and + make the reason a whole sentence that names the cause and the remedy. It is + shown verbatim. +- 6d728b8: feat(types): the driver-fault redaction is exported from types, so a driver's own log lines take the same cut the engine applies + + Clause-②: no + + - **New exports.** `redactBoundStatement`, `redactStatementFromMessage`, `redactPropagatedDriverFault` and the `DriverFaultOrigin` type are exported from `@objectstack/types`, by name. They moved here from `@objectstack/objectql`, which never exported them from its entries. The cut is unchanged by the move: the same split, the same structural cut at the separator, the same value templates and the same property rules. + - **Why here.** `@objectstack/driver-sql`, `@objectstack/objectql` and `@objectstack/core` all depend on this package, and `operatorFacingErrorText` lives in it, so this is the lowest package all of them can import the cut from. The module imports only this package's own leak predicate, which is unchanged. + - **One widening, on the log face.** `redactStatementFromMessage` takes an optional second argument, `{ statementSent: true }`. With it the cut runs without asking the shared leak predicate, as `redactPropagatedDriverFault` already did with the same flag. Without it the function answers exactly as before. + - **Why minor.** The package gains four exported names, and `redactStatementFromMessage` gains the optional parameter above. No existing export of `@objectstack/types` changes. + +### Patch Changes + +- 85e29b8: fix(types): `operatorFacingErrorText` answers through the driver-fault redaction, so an operator-facing record carries no statement and no bound value + + Clause-②: no + + - **What changed.** `operatorFacingErrorText` passes every text it returns through `redactStatementFromMessage`, the one driver-fault redaction in this package. Text it reads off a raw-statement fault's `cause` is cut with `{ statementSent: true }`, which is the cut `@objectstack/driver-sql` applies to its own log line for the same fault. Every other text asks the shared leak predicate, as the engine's own log line does. + - **What an operator reads now.** The records this helper fills, in `os db clean` and in the metadata migrations and probes, keep the dialect's own diagnostic: the missing column, the failed constraint or the locked database. The value slots the redaction's dialect templates own are cut from it, and the redaction's marker stands where the statement was removed. The records no longer carry the statement or the values bound into it. + - **What does not change.** Text that is not a driver dump comes back exactly as before, empty text included. The thrown error is not touched: its `code`, `status`, class and `cause` reach every other reader as the driver composed them. The function's signature and the package's exports are unchanged. +- 149153c: Membership under the `auto` policy is settled when the user is created, per ADR-0093 D7. + + Clause-②: yes (widening) + + - **At creation.** A user created under `auto` is bound to the default organization at creation, and the first session of that creating request carries it. Membership is not decided again when the user signs in later. + - **One-time backfill.** The ADR-0093 D6 backfill of pre-existing users runs once per deployment, and once per process even if its record cannot be written. Its verdict is recorded in the `sys_migration` ledger with id `adr-0093-membership-backfill`. A pass on a deployment with no organization at all records nothing, and the backfill runs again once the default organization is created. If the ledger is missing or cannot be read, the pass does not run and logs a warning. If the record cannot be written, that is logged as an error. `OS_SKIP_MEMBERSHIP_BACKFILL=1` still disables the pass. + - **Default organization owner.** The platform admin is bound as owner of the default organization once, by the bootstrap that first decides it, in both the single-org and the walled organizations wiring. The decision is recorded in the same ledger with id `adr-0093-default-org-owner-bind` and held for the rest of the process even if the record cannot be written. After that, a missing default organization is recreated without binding anyone. To recover, an administrator re-adds members, including themselves, through member management. On a kernel without the ledger, the owner is bound only when the bootstrap creates the default organization. If the ledger exists but cannot be read, that call binds nobody and the next trigger decides. + - **Full scan.** The backfill reads the user and membership tables page by page with no row cap. A scan that cannot read either table in full binds nobody and records nothing. With organizations present but no default target, as in multi-organization deployments, the refusal is recorded. + - **Upgrade.** The first boot of an upgraded deployment runs the backfill once. + - **Unchanged.** `invite-only` binds nobody. Multi-organization deployments get no automatic binding. Users created through sign-up, admin create-user, import or SSO are bound under `auto` as before. + - **Narrowed.** A `sys_user` row inserted straight through the data engine never passes through user creation. Once the backfill is recorded, a later `app:seeded` pass leaves it unbound. That includes users written by a seed that finishes after its inline budget. Code that inserts users this way must write their membership itself; the showcase approval-demo personas now do. + - **`keysetWalk` (`@objectstack/types`).** The walk now decides that a page did not advance only when it gets back the same cursor key or the same page again. It no longer compares keys in JavaScript string order, which disagrees with database collations and could report a healthy walk as truncated. + - **New public surface of `@objectstack/plugin-auth` (additive).** + - `createEnsureDefaultOrganizationOnce` and `EnsureDefaultOrganizationOnceOptions` are the gated bootstrap both wirings call. + - `ObjectQLAdapterFactoryOptions` adds `onRecordCreated`, passed as the new optional second argument of `createObjectQLAdapterFactory`. + - `EnsureDefaultOrganizationOptions` gains `bindOnlyOnCreate` and `bindOwner`. + - `EnsureDefaultOrganizationResult.reason` gains `'owner_bind_decided'`. + - `BackfillMembershipsResult.reason` gains `'scan-incomplete'`. + - Code that switches exhaustively over those reasons sees one more member. + - **`backfillMemberships` (exported) changed behaviour.** Its `limit` option used to cap the rows scanned (default 5000); it is now the page size of a full scan with no cap. The function now needs a reader that can page by `id`; a reader that cannot gets `scan-incomplete` and binds nobody, where it used to bind. A direct call is not gated by the one-time ledger and decides membership again on every call; call it through the one-time pass instead. + - **Policy switch.** Once a pass under `invite-only` is recorded, switching the policy to `auto` later does not backfill the users who existed then; they get membership through invitation or member management. + - **Deprecated, not removed.** The ungated `ensureDefaultOrganization`, both plugin-auth's helper and the `@objectstack/organizations` wrapper, is `@deprecated` in favour of `createEnsureDefaultOrganizationOnce`. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [9b7a0ef] +- Updated dependencies [5a9292e] +- Updated dependencies [1c52a5e] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [16eefc6] +- Updated dependencies [6e33b67] +- Updated dependencies [57cc695] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [9f13c94] +- Updated dependencies [6d67ad5] +- Updated dependencies [ca0dfb6] +- Updated dependencies [45efcfa] +- Updated dependencies [c9c555a] +- Updated dependencies [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [6ec54f0] +- Updated dependencies [98eb3b9] +- Updated dependencies [a2aadab] +- Updated dependencies [fe10172] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [e83c9f6] +- Updated dependencies [045f764] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [607463d] +- Updated dependencies [cab6396] +- Updated dependencies [e864db5] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + ## 17.6.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index e1cfb8241ac..0e073172c85 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.6.0", + "version": "17.7.0", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "main": "dist/index.js", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index a9a0516c73a..cfdde042f8d 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,254 @@ # @objectstack/verify +## 17.7.0 + +### Patch Changes + +- 2df621a: `bootStack` (and so `os verify`) no longer creates a data key file in the key home, and no longer seals its fixtures under a key the host already holds (#21499) + + Clause-②: no + + The harness composed the settings service with no crypto provider and bound the engine to a default `LocalCryptoProvider`. `bootStack` forces a development posture. In that posture, with no `OS_SECRET_KEY`, no `OS_DEV_CRYPTO_KEY` and no key file, both providers wrote a new key file into the key home. So `os verify`, a one-shot command over an in-memory database, left key material behind, and the next development-posture process on that host adopted it. On a host that already had a key, the harness sealed its throwaway fixtures under that real key. + + - **What the harness uses now.** One `LocalCryptoProvider` over a random key held in this process's memory only. It never reads `OS_SECRET_KEY`, `OS_DEV_CRYPTO_KEY` or the key file, and it never writes anywhere. The settings service and the engine get the same instance, so `secret` fields and encrypted settings still seal and open on a host with no key at all. + - **One key per process, not per boot.** Two `bootStack` calls over one `databaseFile` in the same process (the harness's restart) still open each other's secrets. + - **Unchanged.** `BootOptions` and the rest of the public API, and `os verify`'s stdout and `--json` report. The one stderr line announcing the minted key file is gone. +- bee8d1c: `os verify` writes each derived sample in the shape the engine stores it: a `select` declared `multiple: true` is written as a list and compared as a set + + Clause-②: no + + - The CRUD round-trip derivation now asks `@objectstack/spec`'s `isMultiValueField` whether a field is multi-valued, the same predicate the engine stores by. Before, the `select` / `radio` sample was one scalar option code compared `equal` whatever the field declared, so a multi-valued `select` read back as a one-element list and was reported as a fidelity gap the engine does not have. The shipped `examples/app-todo` (`todo_task.tags`) failed `os verify` with exit 1 on exactly that, and now passes. + - A single-valued `select` or `radio` keeps its scalar sample and its `equal` comparison. `multiselect` and `checkboxes` are unchanged. + - A relational field's `multiple` is answered by the same predicate. A `lookup` declared `multiple: true` still receives a list of ids. A `master_detail` or `tree` field carrying `multiple: true` now receives one id, which is how the engine stores those types. The spec already refuses `multiple` on those types at parse, so only an unparsed config could reach this. + - No export, type or accept-set change. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [909229e] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [aead296] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [36e4647] +- Updated dependencies [93a54b8] +- Updated dependencies [f623e2f] +- Updated dependencies [f9bcd08] +- Updated dependencies [cc07862] +- Updated dependencies [e3ad492] +- Updated dependencies [4916168] +- Updated dependencies [f9f9f91] +- Updated dependencies [44072fc] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- Updated dependencies [99589f9] +- Updated dependencies [36ad321] +- Updated dependencies [dcc5ef4] +- Updated dependencies [748b240] +- Updated dependencies [9b7a0ef] +- Updated dependencies [50e1c65] +- Updated dependencies [713b0fa] +- Updated dependencies [5a9292e] +- Updated dependencies [30af17e] +- Updated dependencies [1878ef9] +- Updated dependencies [7aab759] +- Updated dependencies [1c52a5e] +- Updated dependencies [97239c3] +- Updated dependencies [c2cd651] +- Updated dependencies [99e1912] +- Updated dependencies [7ebb543] +- Updated dependencies [3911901] +- Updated dependencies [222ecc2] +- Updated dependencies [1caa603] +- Updated dependencies [04f0cc4] +- Updated dependencies [1fd5664] +- Updated dependencies [3937ad2] +- Updated dependencies [3a6d92f] +- Updated dependencies [7526058] +- Updated dependencies [53fd35e] +- Updated dependencies [23365ea] +- Updated dependencies [32d5769] +- Updated dependencies [ceb4a93] +- Updated dependencies [16eefc6] +- Updated dependencies [fbe2deb] +- Updated dependencies [ee75aae] +- Updated dependencies [6e33b67] +- Updated dependencies [1d0600b] +- Updated dependencies [ab52182] +- Updated dependencies [57cc695] +- Updated dependencies [0557c2f] +- Updated dependencies [db3fee3] +- Updated dependencies [4c8363f] +- Updated dependencies [49524f6] +- Updated dependencies [9f13c94] +- Updated dependencies [9f13c94] +- Updated dependencies [d956910] +- Updated dependencies [6d67ad5] +- Updated dependencies [d7d5b4f] +- Updated dependencies [ca0dfb6] +- Updated dependencies [8b123c0] +- Updated dependencies [45efcfa] +- Updated dependencies [45efcfa] +- Updated dependencies [6d728b8] +- Updated dependencies [6d728b8] +- Updated dependencies [c9c555a] +- Updated dependencies [b206403] +- Updated dependencies [68c5ab7] +- Updated dependencies [520f66f] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [81e69ca] +- Updated dependencies [85e29b8] +- Updated dependencies [086ad0a] +- Updated dependencies [0b82391] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [2f837a5] +- Updated dependencies [abe8f28] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [1289925] +- Updated dependencies [958cfe2] +- Updated dependencies [ced3e1a] +- Updated dependencies [7d674df] +- Updated dependencies [3f1bc81] +- Updated dependencies [72f3c74] +- Updated dependencies [529d971] +- Updated dependencies [16d241a] +- Updated dependencies [4331a6b] +- Updated dependencies [83b3d32] +- Updated dependencies [a7ab047] +- Updated dependencies [f9a8eb8] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [1ca1eb0] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [a4f0cb0] +- Updated dependencies [bd70706] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [aa0d4b9] +- Updated dependencies [5d0e4e2] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [045b946] +- Updated dependencies [6ec54f0] +- Updated dependencies [316be32] +- Updated dependencies [5d095a0] +- Updated dependencies [a1ca156] +- Updated dependencies [6946f2f] +- Updated dependencies [98eb3b9] +- Updated dependencies [5b5e83f] +- Updated dependencies [96b0e31] +- Updated dependencies [f40bb32] +- Updated dependencies [5ac2ba1] +- Updated dependencies [1968d5e] +- Updated dependencies [be55fd2] +- Updated dependencies [31e3e00] +- Updated dependencies [a2aadab] +- Updated dependencies [8843505] +- Updated dependencies [234d1d8] +- Updated dependencies [fe10172] +- Updated dependencies [83e2fee] +- Updated dependencies [5259a35] +- Updated dependencies [ed15448] +- Updated dependencies [9d91f58] +- Updated dependencies [9059082] +- Updated dependencies [309224d] +- Updated dependencies [73b2246] +- Updated dependencies [c7a60e1] +- Updated dependencies [e83c9f6] +- Updated dependencies [3eb38ae] +- Updated dependencies [1c3a4d9] +- Updated dependencies [025008a] +- Updated dependencies [045f764] +- Updated dependencies [75ddcd1] +- Updated dependencies [2df3d13] +- Updated dependencies [07bf21f] +- Updated dependencies [6fb7115] +- Updated dependencies [53021e3] +- Updated dependencies [26d710e] +- Updated dependencies [a0176ef] +- Updated dependencies [07e933b] +- Updated dependencies [c9be1f1] +- Updated dependencies [149153c] +- Updated dependencies [ba57588] +- Updated dependencies [a43d90a] +- Updated dependencies [833d57c] +- Updated dependencies [607463d] +- Updated dependencies [607463d] +- Updated dependencies [088428f] +- Updated dependencies [cab6396] +- Updated dependencies [f5b8e29] +- Updated dependencies [e864db5] +- Updated dependencies [25eb7de] +- Updated dependencies [41a1135] +- Updated dependencies [54fb60a] +- Updated dependencies [7665c54] +- Updated dependencies [866683f] +- Updated dependencies [88a39c0] +- Updated dependencies [5e0b489] +- Updated dependencies [07c842d] +- Updated dependencies [8e35895] +- Updated dependencies [1f04696] +- Updated dependencies [dcb11c2] +- Updated dependencies [bc7747c] +- Updated dependencies [0728cbf] +- Updated dependencies [e6dc7a2] +- Updated dependencies [faf8dce] +- Updated dependencies [f243a29] +- Updated dependencies [d16b9fb] +- Updated dependencies [131b937] +- Updated dependencies [76fec88] +- Updated dependencies [13a22d0] +- Updated dependencies [753e7a1] +- Updated dependencies [80f9f7e] +- Updated dependencies [bab7685] +- Updated dependencies [fb69825] +- Updated dependencies [0d8ea5e] +- Updated dependencies [f76c622] +- Updated dependencies [48eb9c1] +- Updated dependencies [8832655] +- Updated dependencies [100f68b] +- Updated dependencies [8963dbf] +- Updated dependencies [1354e7b] +- Updated dependencies [1cbe165] +- Updated dependencies [3c7785d] +- Updated dependencies [6dd99b8] +- Updated dependencies [568dc0b] +- Updated dependencies [15fe567] +- Updated dependencies [0bddffd] +- Updated dependencies [7e0066a] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/runtime@17.7.0 + - @objectstack/service-automation@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/plugin-sharing@17.7.0 + - @objectstack/service-analytics@17.7.0 + - @objectstack/objectql@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/plugin-auth@17.7.0 + - @objectstack/service-settings@17.7.0 + - @objectstack/rest@17.7.0 + - @objectstack/plugin-hono-server@17.7.0 + ## 17.6.0 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index f0e77c004b0..f6383e1aa7e 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.6.0", + "version": "17.7.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",