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/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/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-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-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-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/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/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/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/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-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-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-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/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/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/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/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/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/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-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/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/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/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/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..ce256201c74 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-03). - [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..2dc8320718b 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/example-crm +## 4.0.99 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..9445a608efe 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/example-multi-package +## 0.0.6 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..96596b2eee8 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,95 @@ # @objectstack/example-showcase +## 0.3.21 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [13a24ec] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [6091136] +- Updated dependencies [f9bcd08] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [901e7cf] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/driver-sql@17.7.0 + - @objectstack/connector-mcp@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/runtime@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..017d860c2ac 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,101 @@ # @objectstack/example-todo +## 4.0.99 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [8598614] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [6091136] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/service-knowledge@17.7.0 + - @objectstack/runtime@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..06b1249fc3d 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/example-embed-objectql +## 0.0.39 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [db0cf22] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..1b0b8cb880a 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,27 @@ # @objectstack/hono +## 17.7.0 + +### Patch Changes + +- 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] + - @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..6a25804598d 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/account +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..2c8ce12cd34 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/setup +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..85b01f9e812 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,72 @@ # @objectstack/studio +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..ed5d0c9da9f 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,582 @@ # @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. + +### Patch Changes + +- 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [13a24ec] +- Updated dependencies [fd5a1cd] +- 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 [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 [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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [bd70706] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [aa0d4b9] +- Updated dependencies [6cf1154] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [901e7cf] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@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-sql@17.7.0 + - @objectstack/driver-turso@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/service-automation@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/runtime@17.7.0 + - @objectstack/formula@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/console@17.7.0 + - @objectstack/account@17.7.0 + - @objectstack/setup@17.7.0 + - @objectstack/client@17.7.0 + - @objectstack/driver-mongodb@17.7.0 + - @objectstack/driver-sqlite-wasm@17.7.0 + - @objectstack/observability@17.7.0 + - @objectstack/plugin-hono-server@17.7.0 + - @objectstack/plugin-webhooks@17.7.0 + - @objectstack/service-cache@17.7.0 + - @objectstack/service-job@17.7.0 + - @objectstack/service-messaging@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..73acd21bcec 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,75 @@ # @objectstack/client-react +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..9c433930a2a 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/client +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..4a61f7be4be 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,159 @@ # @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. + +### 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/runtime@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 475bab1c952..2e4ac9d5c50 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..08a5de5cbba 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,89 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..b9bf207670a 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/connector-openapi +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..a4279fb1714 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/connector-rest +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..953dfe17b8b 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/connector-slack +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..eb0e24eaa20 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,128 @@ # @objectstack/console +## 17.7.0 + +### Minor Changes + +- 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` + ## 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..10a87222c6f 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,132 @@ # @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`. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..afb33e99ddc 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,88 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..ed0ff22cdbb 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/driver-mongodb +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..3a714bb60da 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,128 @@ # @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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..50aef700c94 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/driver-sqlite-wasm +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [13a24ec] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..5a2ec96a373 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,114 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [13a24ec] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [35dfb81] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..9663dd28807 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,114 @@ # @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 + +- 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..54131b7c529 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,217 @@ # @objectstack/lint +## 17.7.0 + +### Minor 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..5ad5784a0d6 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,106 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..9c80f5f247b 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,97 @@ # @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. +- 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..744254851eb 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,14 @@ # @objectstack/metadata-fs +## 17.7.0 + +### Patch Changes + +- Updated dependencies [c98a72d] +- Updated dependencies [83b3d32] +- 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..0291728fa43 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,309 @@ # @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. + +### 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. +- 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- 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 [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/lint@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/formula@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..4be7e64b4e4 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,113 @@ # @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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/types@17.7.0 + - @objectstack/metadata-fs@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..d80ee419514 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,418 @@ # @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) + + + +### 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/metadata-protocol@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/formula@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 7f169fa2ef3..b9317ff6871 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..3785e6bd8ac 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/observability +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..5fd10613fc5 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,321 @@ # @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. + +### 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. +- 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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..2e83513264e 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/embedder-openai +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..1af64cbe98d 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,76 @@ # @objectstack/knowledge-memory +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [6091136] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..cf816355613 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,76 @@ # @objectstack/knowledge-ragflow +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [6091136] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..50e1a08fc67 100644 --- a/packages/plugins/organizations/CHANGELOG.md +++ b/packages/plugins/organizations/CHANGELOG.md @@ -1,5 +1,80 @@ # @objectstack/organizations +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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 90d2b75d498..f70bcd6991f 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..3a4f870949c 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,221 @@ # @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. + +### 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/formula@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..b25814cb64c 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,223 @@ # @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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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..28f33fa939c 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,141 @@ # 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. + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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..fe4d705df4d 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,124 @@ # @objectstack/plugin-dev +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [6091136] +- Updated dependencies [e3ad492] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@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/runtime@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/account@17.7.0 + - @objectstack/setup@17.7.0 + - @objectstack/plugin-hono-server@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..a810cdcd1c5 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,95 @@ # @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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..ad3c8336850 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/plugin-hono-server +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..7a863273fa4 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,36 @@ # @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] + - @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..ba3dfdc45e9 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,200 @@ # @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). + +### 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/formula@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..af166c44d8b 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,186 @@ # @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`. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/formula@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 e6f145a1bd3..e91eb5370a7 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..bb8ee108362 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/plugin-webhooks +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..79da0496efd 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,151 @@ # @objectstack/dogfood +## 0.0.47 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- 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 [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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@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/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/formula@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/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-webhooks@17.7.0 + - @objectstack/service-messaging@17.7.0 + ## 0.0.46 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index ea378969329..08dff8aea50 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..4120a2624c1 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/downstream-contract +## 0.0.45 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..6af1fae0e1e 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,16 @@ # @objectstack/http-conformance +## 0.1.7 + +### Patch Changes + +- Updated dependencies [c205b6c] +- Updated dependencies [30af17e] +- Updated dependencies [eb9ef79] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] + - @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..f531f42b9b0 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,107 @@ # @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 +- 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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..b544aafcb11 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,358 @@ # @objectstack/runtime +## 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. + + +- 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. + +### 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [13a24ec] +- Updated dependencies [fd5a1cd] +- Updated dependencies [c98a72d] +- Updated dependencies [8598614] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [f9bcd08] +- Updated dependencies [e3ad492] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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/metadata-core@17.7.0 + - @objectstack/metadata@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/formula@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..c88b9af49ab 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,39 @@ # @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`. + ## 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..d1331cba37d 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,373 @@ # 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..3a41c040570 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,156 @@ # @objectstack/service-automation +## 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. + + +- 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 + +- 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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/metadata-core@17.7.0 + - @objectstack/formula@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 5eea5f1c9d6..3801cfc72d6 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..5816799a58f 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,75 @@ # @objectstack/service-cache +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..fcc56f701c8 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,69 @@ # @objectstack/service-cluster-redis +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..3f26756e772 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,74 @@ # @objectstack/service-cluster +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..c4edda1add1 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,148 @@ # @objectstack/service-external-datasource +## 17.7.0 + +### 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. + + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [db0cf22] +- Updated dependencies [13a24ec] +- Updated dependencies [fd5a1cd] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @objectstack/spec@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/types@17.7.0 + - @objectstack/driver-mongodb@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..d22bed9c7a8 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/service-i18n +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..d50791c745d 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/service-job +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..fb24071e729 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,89 @@ # @objectstack/service-knowledge +## 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..c2177dc5387 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,82 @@ # @objectstack/service-messaging +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..cd055ae17eb 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,85 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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..3c85f57a191 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,93 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..76a85d1ac43 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/service-realtime +## 17.7.0 + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..7bd789f5192 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,209 @@ # @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. + +### Patch Changes + +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..ab6b0653757 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,91 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..8676edae52f 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,98 @@ # @objectstack/service-storage +## 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..feec6354b58 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,1447 @@ # @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) + + +- 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) + + +- 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. +- 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. +- 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. +- 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. +- 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. +- 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. +- 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. +- 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. +- 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. +- 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. +- 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. + ## 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..e7df4e9f3b7 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,96 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..5507bee9885 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,89 @@ # @objectstack/plugin-trigger-record-change +## 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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..289f03ba4f4 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,121 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [c98a72d] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [85e29b8] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @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..d65403267bf 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,122 @@ # @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. +- Updated dependencies [ecb6ca0] +- Updated dependencies [135daaa] +- Updated dependencies [22c2d6f] +- Updated dependencies [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- Updated dependencies [96a9719] +- Updated dependencies [41a3c8d] +- Updated dependencies [c52c49d] +- Updated dependencies [cfa4d74] +- Updated dependencies [99589f9] +- 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 [68c5ab7] +- Updated dependencies [b793010] +- Updated dependencies [5555047] +- Updated dependencies [aa46322] +- Updated dependencies [100c394] +- Updated dependencies [72217cd] +- Updated dependencies [72af58c] +- Updated dependencies [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- Updated dependencies [6c5697d] +- Updated dependencies [9a4182a] +- Updated dependencies [41b1333] +- Updated dependencies [f1e4ae5] +- Updated dependencies [eb9ef79] +- Updated dependencies [f83d066] +- Updated dependencies [9e9d693] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [0bddffd] + - @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..d2f2c7d3ac8 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,155 @@ # @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 [0721848] +- Updated dependencies [bdd3654] +- Updated dependencies [c205b6c] +- Updated dependencies [48fa7a3] +- Updated dependencies [ad7c351] +- Updated dependencies [e901c27] +- Updated dependencies [a387354] +- Updated dependencies [f6b7520] +- 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 [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 [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 [958cfe2] +- Updated dependencies [7d674df] +- Updated dependencies [529d971] +- 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 [bd70706] +- Updated dependencies [1ac7308] +- Updated dependencies [10454b3] +- Updated dependencies [aa0d4b9] +- Updated dependencies [9e9d693] +- Updated dependencies [5c9138b] +- Updated dependencies [48eb9c1] +- Updated dependencies [8963dbf] +- Updated dependencies [6dd99b8] +- Updated dependencies [0bddffd] + - @objectstack/spec@17.7.0 + - @objectstack/platform-objects@17.7.0 + - @objectstack/core@17.7.0 + - @objectstack/service-datasource@17.7.0 + - @objectstack/service-automation@17.7.0 + - @objectstack/plugin-security@17.7.0 + - @objectstack/plugin-sharing@17.7.0 + - @objectstack/service-analytics@17.7.0 + - @objectstack/runtime@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",