diff --git a/.changeset/add-member-team-id-no-active-team-fallback.md b/.changeset/add-member-team-id-no-active-team-fallback.md deleted file mode 100644 index c1b7ff49eb..0000000000 --- a/.changeset/add-member-team-id-no-active-team-fallback.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@objectstack/platform-objects": patch -"@objectstack/plugin-auth": patch ---- - -Correct a false vendor claim in the `organization/add-member` source comments: -`teamId` has **no** active-team fallback (#10532). Two comments — the -`sys_member` `add_member` action metadata (the origin) and the -`organization-add-member.ts` module header that cited it as authority — stated -that "organizationId/teamId default to the caller's active org/team when -omitted". Measured on the installed better-auth 1.7.1 -(`dist/plugins/organization/routes/crud-members.mjs`, inside `addMember`), only -the organization half is true: - -```js -const orgId = ctx.body.organizationId || session?.session.activeOrganizationId; -const teamId = "teamId" in ctx.body ? ctx.body.teamId : void 0; -``` - -`activeOrganizationId` is read 8 times in that module; `activeTeamId`, never. An -omitted `teamId` therefore stays `undefined` and the member joins no team — every -`if (teamId)` branch (team lookup, `TEAM_NOT_FOUND`, per-team limit) is skipped. - -No runtime behaviour changes, and no deployment was ever misled: the `add_member` -action's `params` list carries no `teamId`, so the toolbar never sent one and the -claim was never exercised. What the comment did mislead was the next reader of -the mount, which cited it as the justification for forwarding request headers — -forwarding buys the organization default only. Forwarding `teamId` itself remains -correct: pass it and it works. - -The asymmetry the docs now publish is held by a new pin, -`organization-add-member-team-fallback.test.ts`, which reads the fact out of the -installed vendor artifact (not out of our own comments) so that a future -better-auth bump *adding* an active-team fallback reddens instead of silently -putting the docs out of date. diff --git a/.changeset/aggregate-per-aggregation-filter.md b/.changeset/aggregate-per-aggregation-filter.md deleted file mode 100644 index 1710444432..0000000000 --- a/.changeset/aggregate-per-aggregation-filter.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/objectql": minor -"@objectstack/driver-sql": patch -"@objectstack/driver-turso": patch -"@objectstack/driver-mongodb": patch -"@objectstack/driver-memory": patch ---- - -`engine.aggregate` honours a per-aggregation `filter` (#10576, the contract -half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but -marked experimental and enforced by nothing — is now live with SQL -`FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one -aggregation reads while sibling aggregations in the same call keep seeing -every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) -can finally reach the engine instead of being silently dropped (the #10413 -wrong-numbers defect on the ObjectQL analytics path). The -`StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) -gains the same optional `filter` on its aggregation entries so analytics -strategies can lower measure filters into it (#10413 phase 2 consumes this -seam next). - -Execution is the correct-first two-tier shape date bucketing and HAVING use: -the engine lowers filtered aggregations in memory for every driver (unknown -operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation -position; a group emptied by its filter answers the ruled empty-group values -— count/sum 0, avg/min/max null). No driver compiles conditional aggregation -natively today, so each native aggregate face (driver-sql — inherited by -driver-sqlite-wasm and Turso local —, the Turso remote transport, -driver-mongodb's pipeline builder, driver-memory's `performAggregation`) -refuses a directly-delivered per-aggregation filter with -`NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. -Aggregations without a `filter` are byte-identically unchanged, including -their native pushdown path. diff --git a/.changeset/ai-chat-not-agent-resolved.md b/.changeset/ai-chat-not-agent-resolved.md deleted file mode 100644 index f27a2ced9d..0000000000 --- a/.changeset/ai-chat-not-agent-resolved.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/spec": patch -"@objectstack/client": patch ---- - -Stop documenting bare `POST /api/v1/ai/chat` as agent-resolved (#10510). Two -shipped docblocks described a resolution step the route does not perform: -`client.ai.agents` claimed `/ai/chat` "talks to the environment's default -agent", and `App.defaultAgent` claimed that endpoint auto-resolves the app's -agent from `context.appName`. The bare route loads no agent and never reads -`context.appName`; the default-agent chain (explicit > `defaultAgent` of the -named app > first active) is driven by the assistant chat endpoint, -`POST /api/v1/ai/assistant/chat`, and `client.ai.agents.chat()` is the only SDK -method that reaches an agent at all. - -Both sites read as a security-relevant scoping guarantee — an agent-resolved -endpoint would have its tool offer scoped by that agent's skills (ADR-0063 -§1/§5) — so a reader auditing "which endpoints are surface-scoped?" from these -declarations got the wrong answer at both. Documentation text only: no schema -key, no parse behaviour and no runtime path changes. diff --git a/.changeset/approval-row-organization-id.md b/.changeset/approval-row-organization-id.md deleted file mode 100644 index 415dc8bc92..0000000000 --- a/.changeset/approval-row-organization-id.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -Declare `organization_id?: string | null` on `ApprovalRequestRow` and -`ApprovalActionRow` (#10331). The approval service has always stamped the -tenancy placement on the rows it inserts — and returns it on request-row -reads — but the published contract types omitted the field, so consumers had -to cast past the contract to reach it. Type-only widening: one declared -optional field per row, no runtime change. diff --git a/.changeset/archiver-honours-declared-ttl.md b/.changeset/archiver-honours-declared-ttl.md deleted file mode 100644 index 6baaaaef88..0000000000 --- a/.changeset/archiver-honours-declared-ttl.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -**Behaviour change:** a `lifecycle` that declares both `ttl` and `archive` now -has its **`ttl` enforced** — the Archiver selects the rows it moves by the -declared TTL cutoff (`ttl.field` past `ttl.expireAfter`) instead of by -`created_at` age (#10347). - -That pair has always parsed — ADR-0057 §3.5 is satisfied because `ttl` is a -bounding policy, and the `archive.after === retention.maxAge` refine only fires -when `retention` is present — but it did nothing: `LifecycleService.reapObject` -returns into `archiveObject` before its `ttl` branch is reachable, so no reap on -`ttl.field` ever ran and the Archiver copied and hot-deleted by `created_at` age -alone. Declared, not enforced. What the author wrote is now what executes; they -no longer have to discover that the two keys cannot usefully be written -together. - -**Lifecycles that declare `archive` without `ttl` are unaffected** — they keep -selecting rows by `created_at` past `archive.after`, unchanged. Every -archive-declaring object shipped with the platform (`sys_audit_log`, -`sys_metadata_audit`) is that shape, so no bundled object changes behaviour. - -Two details of the new selection, both deliberate: - -- A row whose `ttl.field` is **null or absent is retained, not archived**. `$lt` - is a positive comparison and a value that is not there satisfies none of them - (the platform-wide null answer settled in #5298/#5299), which is also the - right reading: a row with no expiry stamp has not been given one, and treating - "absent" as "expired at the epoch" would archive exactly the rows whose expiry - the author has not yet decided. -- The cold-side `archive.keep` prune is unchanged. It bounds how long **archived** - rows survive in cold storage, not which hot rows are due, and it still measures - from `created_at` under either policy. - -If you declare `retention` beside `ttl` and `archive`, the TTL cutoff is what -selects: the age window no longer separately bounds the hot store for that -triple. Whether the Archiver should honour both windows is a separate open -question, filed as #10527 rather than decided here. diff --git a/.changeset/attachment-before-update-guard.md b/.changeset/attachment-before-update-guard.md deleted file mode 100644 index 5ac0c9df7b..0000000000 --- a/.changeset/attachment-before-update-guard.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/service-storage": patch ---- - -**Behaviour change (tightening):** updates of `sys_attachment` rows are now authorization-gated, where they previously ran with **no record-level check at all** (#10091). - -`installAttachmentAccessHooks` gated insert (parent-edit access, `uploaded_by` server-stamped) and delete (uploader-or-parent-editor), but registered **no `beforeUpdate` hook** — so under the default member permission sets (wildcard CRUD, no row scoping) any member could rewrite any attachment row: re-point `parent_id` at a record they cannot read, or rewrite `uploaded_by` and then walk through the delete gate's uploader shortcut. The `sys_comment` kit — explicitly derived from this one — has gated update with the same rule since #4630; the source kit was missing the limb its derivative copied. - -The new `beforeUpdate` gate narrows the accept set as follows; if a currently-working update starts failing, the caller lacked rights the other two verbs already required: - -- **Row rule:** the caller must be the attachment's uploader OR hold edit on its parent record (`ISharingService.canEdit`; degrades to caller-scoped parent READ visibility when no sharing service is present). A multi-row update requires EVERY matched row to pass. Refusals are HTTP 403 with the **standard catalog code `RECORD_NOT_ACCESSIBLE`** (ADR-0112: generic permission conditions take the catalog — the same envelope the comment kit's update gate emits; the insert/delete gates keep their grandfathered `ATTACHMENT_*` codes). -- **Re-point rule:** an update that changes `parent_object`/`parent_id` must additionally satisfy the attach rule on the NEW parent (edit access, read visibility in degraded mode) — 403 `ATTACHMENT_PARENT_ACCESS` otherwise, and a re-point half that names no record (`null`/empty) is refused rather than left to validation. -- **Unscoped shape:** an unscoped `multi: true` update (no `where` at all) is refused outright via the `dispatchUnscopedMultiWrite` whole-operation dispatch (#9974), mirroring the delete verb's #4757 refusal. The explicit match-all `where: {}` is still accepted and authorized per row. - -System-context operations and context-less programmatic calls on bare kernels bypass the gate exactly as the insert/delete gates do. `uploaded_by` is deliberately not re-stamped on update: the caller is already verified as uploader or parent editor before the write proceeds, so the rewrite-then-uploader-delete escalation is closed by the row rule itself. diff --git a/.changeset/attachment-lifecycle-update-leg.md b/.changeset/attachment-lifecycle-update-leg.md deleted file mode 100644 index d4e3f087f0..0000000000 --- a/.changeset/attachment-lifecycle-update-leg.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/service-storage": patch ---- - -**Bug fix (retention leak):** an UPDATE that re-points a `sys_attachment` row's `file_id` now detaches the PRIOR file the same way deleting that row would — tombstoning it when the re-pointed row was its last reference (#10171). - -`installAttachmentLifecycleHooks` registered only delete-side and insert-side handlers, so a `file_id` re-point left the old `sys_file` sitting at `status='committed'` with zero join rows and no `deleted_at`. That is not the module's "fail toward retention" bias, which buys a **later** look: `sys_file`'s declared lifecycle nominates a row for the sweep only through `ttl { field: 'deleted_at' }` or `retention { onlyWhen: { status: 'pending' } }`, and a silently detached file matches neither — so the reap guard is never asked about it and the storage bytes are stranded permanently, with no later re-examination. - -The new `afterUpdate` handler fires only when the payload actually carries `file_id` and the value actually changes, then runs the existing orphan rule (zero remaining join rows, attachments-scope, committed) on the prior id. It is best-effort like its siblings and never blocks the user's write; with no pre-image available it tombstones nothing, keeping the file. - -The departed id comes from the engine-bound pre-image `ctx.previous`, **not** from a `beforeUpdate` stash mirroring the delete pair. Since #5574 (ADR-0058 Addendum II D1/D2) a predicate write dispatches one fresh context per matched row in each phase, so a stash written in `beforeUpdate` reaches `afterUpdate` on the by-id path and is lost on the predicate path — a stash-based twin would have been silently half-dead on exactly the multi-row updates that orphan the most files. Reading `previous` also adds no driver round trip: the prior-row read is memoized per operation and already demanded on this object. - -**No revival leg was added**, deliberately. Re-pointing a row ONTO a grace-window tombstone is already handled by the reap guard's sweep-time re-verification, which resolves current references, un-tombstones the file and vetoes the reap rather than reclaiming bytes. A second revival mechanism here would be a duplicate answer to a question that already has one. diff --git a/.changeset/audit-plugin-boot-path-reachability.md b/.changeset/audit-plugin-boot-path-reachability.md deleted file mode 100644 index bb2813c6e4..0000000000 --- a/.changeset/audit-plugin-boot-path-reachability.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/plugin-audit": patch ---- - -**Docs (published README) + ruling:** record-view auditing now documents how to turn it on under `objectstack serve`, and the answer to "should `os serve` grow an `appAuditPluginOptions(config)` helper?" is **no** (#9863). - -The README and `content/docs/permissions/record-view-auditing.mdx` both said the audited set is configured "where you compose the kernel", and the docs page went further: *"The CLI's `os serve` registers `AuditPlugin` with no options, so a stack served that way has record-view auditing off and no knob to turn it on."* That last clause stopped being true when #9864 declared and pinned the duplicate-registration contract. The knob is the stack's `plugins` array — a configured `new AuditPlugin({ readAudit: { objects: [...] } })` there supersedes the CLI's option-less instance by name, last-one-wins, on both kernels, with the displaced instance never reaching `init()`. Both pages now spell that path, and name the `Plugin superseded: 'com.objectstack.audit'` boot line as the opt-in working rather than a misconfiguration. - -**No new configuration surface was added, deliberately.** A `config.audit` key read by an `appAuditPluginOptions(config)` helper would reproduce, in `objectstack.config.ts`, exactly the failure #8992's ruling refused for the object-metadata spelling: a declaration that survives in a deployment which never installs this package, reading as coverage while recording nothing. It would also be a *second* configuration surface that silently loses to the first, since an app's own `plugins` entry supersedes whatever the CLI constructed. The `#7001` symmetry argument does not carry it either — `@objectstack/verify`'s `bootStack` constructs no `AuditPlugin` and does not depend on this package, so there is no second boot path to disagree with. - -No runtime behaviour changed. `packages/cli` gains only the reasoning at its registration site and `serve-audit-registration.contract.test.ts`, which pins the three facts the ruling rests on — including the load-bearing ordering (`AuditPlugin` registered above the stack `plugins` loop) that until now was asserted by a comment and nothing else. diff --git a/.changeset/automation-write-manage-metadata-gate.md b/.changeset/automation-write-manage-metadata-gate.md deleted file mode 100644 index 055657e8aa..0000000000 --- a/.changeset/automation-write-manage-metadata-gate.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/runtime": patch ---- - -**Behaviour change (security tightening):** the `/api/v1/automation` **definition writes** now require the `manage_metadata` capability (#10145). - -`POST /api/v1/automation`, `PUT /api/v1/automation/:name` and `DELETE /api/v1/automation/:name` — `automation.create` / `automation.update` / `automation.delete` on the SDK — were reachable by **any authenticated caller**. They now answer **403 `PERMISSION_DENIED`** unless the caller holds `manage_metadata` (ADR-0066 D1's authoring capability), the same key the sibling `PUT /api/v1/meta/:type/:name` and every state-changing `/api/v1/packages/*` route already demand. Engine self-invocation (`isSystem`) bypasses, as on every other capability gate. - -**Existing credentialed callers that author flows over HTTP will start getting 403** and must be granted `manage_metadata`. A flow is authored metadata: this closes the last write door onto the metadata plane that did not ask the metadata plane's question. - -What was measured on a walled multi-organization deployment (`OS_TENANCY_POSTURE=isolated`): a plain tenant org owner holding `organization_admin` — the same session answered 403 by `PUT /meta/:type/:name`, `POST /ai/tools/:tool/execute` and `POST /packages/*` — created, modified and deleted flows through this door, all 200. Flow definitions are registered at **environment** scope, not organization scope, so the write crossed the tenant wall: a shipped flow deleted by one tenant read 404 for the actor, for an unrelated tenant **and** for the platform admin, and an injected flow read 200 for all three. - -**Deliberately unchanged — execution is not authoring:** - -- `POST /automation/:name/trigger` and the legacy `POST /automation/trigger/:name` **run** a flow. They keep their existing posture (authenticated, plus the flow's own `runAs` authorization envelope). -- `POST /automation/:name/runs/:runId/resume` is already fail-closed through the suspended node's `resumeAuthority`; a metadata capability in front of it would refuse the very user the flow paused for. -- `POST /automation/:name/toggle` mutates engine enablement rather than a definition, and is filed separately rather than folded into a security fix. -- The reads (`GET /automation`, `GET /automation/:name`, the run surfaces) are untouched; run-state reads keep their `sys_automation_run` grant. - -The gate sits ahead of the service probe and ahead of body validation, so a refused caller neither writes anything nor learns from a 501-vs-403 whether the deployment mounts automation at all. diff --git a/.changeset/blank-template-console-disclosure.md b/.changeset/blank-template-console-disclosure.md deleted file mode 100644 index 34ba53975a..0000000000 --- a/.changeset/blank-template-console-disclosure.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"create-objectstack": patch ---- - -Tell a newcomer that the `blank` starter ships no app, so an empty Console -reads as the intended starting point rather than a broken install (#10317). - -Measured on a real scaffold-and-boot (`create-objectstack my-app -t blank`, -published 17.1.0 packages, `objectstack dev --ui`): `GET /api/v1/meta/app` -returns the two platform apps (Setup, Account) and nothing of the project's -own, while `GET /api/v1/data/my_app_note` serves the scaffolded object the -whole time. The template ships `src/objects/` only — deliberately, as every -scaffolder template in this repo does — but nothing the newcomer could reach -said so, and `pnpm dev` advertises the Console URL on every boot. - -Documentation only: a new "The Console" section in the generated `README.md` -naming the Console path, the consequence, and `src/apps/*.app.ts` as the -remedy. No change to what the scaffolder writes into `src/`. diff --git a/.changeset/blueprint-sharing-model.md b/.changeset/blueprint-sharing-model.md deleted file mode 100644 index c8db108721..0000000000 --- a/.changeset/blueprint-sharing-model.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -Add an optional `sharingModel` slot (enum `private | public_read | public_read_write | controlled_by_parent`) to `BlueprintObjectSchema` and, as a required-but-nullable key, to the OpenAI-strict structured-output mirror (`SolutionBlueprintStrictSchema`). The propose-stage LLM can now author a deliberate Org-Wide Default (OWD) choice — e.g. `private` for an object the user described as personal/sensitive — instead of having the platform's deterministic default silently override the intent expressed at propose time. Omitting the key (or emitting `null` in the strict mirror) still defers to the platform default (business object → `public_read_write`, master-detail child → `controlled_by_parent`). diff --git a/.changeset/button-metric-icon-liveness.md b/.changeset/button-metric-icon-liveness.md deleted file mode 100644 index a9c3ae2a70..0000000000 --- a/.changeset/button-metric-icon-liveness.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -docs(spec): record the live read points of `element:button.icon` and `object-metric.icon` — the last two icon slots whose describes stated only the vocabulary (#10053) - -Both keys parsed and rendered while saying only what alphabet their value is -drawn from: `Icon name (Lucide icon)` and `Icon name (Lucide)`. That sentence is -equally true of the `page:header` `icon` retired in #6946 — refused *precisely -because no render path reads it* — so the prose could not separate a live key -from a dead one. It is the same absence that sent #9397 through a full dispatch -cycle re-deriving the accordion read point from scratch before the retirement -candidate was closed premise-overtaken. #9881 and #9972 recorded the accordion -and tab items; these two close the set for `component.zod.ts`. - -**Both are live**, re-measured rather than transcribed from the card. Note the -pin: the earlier records cite `82a94170c`, but `.objectui-sha` moved to -`9a3daf8d3` in #10137, and these were measured there. - -- `element:button.icon` — `packages/components/src/renderers/form/button.tsx:44-47` - resolves `schema.icon`, and `:69` / `:71` draw it either side of the label per - `iconPosition`, both suppressed while `loading`. -- `object-metric.icon` — `plugin-dashboard/src/index.tsx:161` publishes it as a - designer input; `ObjectMetricWidget.tsx:142` destructures it and forwards it at - `:474` to `MetricWidget`, which resolves it at `MetricWidget.tsx:312-321` and - draws it at `:373-382` in the `colorVariant`-tinted square. - -**The button is the one authorable icon on this surface that does not go through -`LazyIcon`**, and the docblock now says so, because the two paths are not -interchangeable: - -- button: `toPascalCase` (splits on `-` only) → a one-entry rename map - (`Home` → `House`) → `icons[name]` from `lucide-react`. An unknown name - resolves to `undefined` and the button renders with **no icon and no - diagnostic**. -- `LazyIcon` / `getLazyIcon` (`components/src/lib/lazy-icon.tsx:66-92`, the slot - the metric tile and every container icon use): normalises to kebab-case, - validates against Lucide's own name list, and degrades an unknown name to the - `Database` glyph. - -So a spelling that draws an icon in a tab trigger can draw nothing on a button — -previously discoverable only by reading two objectui files. - -**Nothing about what parses changes.** Both keys were already declared and -already optional; no key is widened, narrowed, retired or renamed. What is added -is the prose that makes each liveness verdict readable from the spec side alone, -and the accept-pins that keep it readable: per key, an accept carried through to -the parsed output, an undeclared-sibling refusal so the accept is not vacuous, -and an assertion that the `.describe()` still names its consumer. diff --git a/.changeset/canonical-docs-host-in-published-links.md b/.changeset/canonical-docs-host-in-published-links.md deleted file mode 100644 index fcaf9ca034..0000000000 --- a/.changeset/canonical-docs-host-in-published-links.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"create-objectstack": patch -"@objectstack/knowledge-ragflow": patch -"@objectstack/plugin-audit": patch -"@objectstack/service-analytics": patch -"@objectstack/service-automation": patch -"@objectstack/service-cache": patch -"@objectstack/service-i18n": patch -"@objectstack/service-job": patch -"@objectstack/service-knowledge": patch ---- - -Point every documentation link in these packages' published READMEs — and in -the project `create-objectstack` scaffolds — at the canonical docs origin -`https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. - -Both spellings reach the same pages (the alias redirects to the apex, -path-preserving), so no link was broken. The reason it needs a release rather -than an in-repo fix alone: a README ships inside the npm tarball, so the -version already on npm keeps showing the old host to every reader of the -package page until a new one is published. diff --git a/.changeset/capability-gate-update-verb.md b/.changeset/capability-gate-update-verb.md deleted file mode 100644 index 60b92f8500..0000000000 --- a/.changeset/capability-gate-update-verb.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/plugin-audit": patch ---- - -**Behaviour change (tightening):** `enable.files` / `enable.feeds` are now enforced on the **update** verb, not only on insert (#10170). - -Both capability gates in `audit-writers.ts` registered on `beforeInsert` only. `enable.files` says whether `sys_attachment` rows may **target** an object and `enable.feeds` whether `sys_comment` rows may target it — properties of the target object, not of the verb that got a row there — so a re-point via update landed rows the declaration refuses: a caller who could not *create* an attachment on an object without `enable.files: true` could *move* an existing one onto it, and a comment could be re-threaded into a `feeds: false` object's thread. The access kits authorize the re-point (`comment-access-hooks.ts` since #4630, `attachment-access-hooks.ts` since #10091), but those are **access** checks — the capability half was never asked on update. - -What an operator will now observe: - -- An update of `sys_attachment` whose payload sets `parent_object` to an object that does not declare `enable: { files: true }` is refused with **403 `FILES_DISABLED`** — the same envelope the insert path has emitted since #2727 (ADR-0112: `code` + `status`). Fail-closed as on insert: an absent `enable` block, an absent flag, and an unknown parent object all reject. -- An update of `sys_comment` whose payload sets `thread_id` to a thread on an object declaring `enable: { feeds: false }` is refused with **403 `FEEDS_DISABLED`**. Opt-out semantics as on insert: only an explicit `false` rejects, and a missing or free-form `thread_id` is still allowed through — this is capability gating, not access control. -- Both apply on **both dispatch shapes**: a by-id update (`dispatch.mode` `record`) and a predicate `multi: true` update, which is evaluated per matched row (#5574 / ADR-0058 Addendum II). An unscoped predicate update is refused on its first matched row. - -**No existing row is newly refused, and no update that is not a re-point changes.** The gates read the payload: an update that never names `parent_object` / `thread_id` returns on the gate's first line, so renames, body edits, reaction writes and other column updates on a row whose parent object has since had the capability flipped off keep working exactly as before. Only a write that makes a row *newly target* a walled object is refused. - -**Blast radius.** A structural sweep of the 4 660 in-tree source files found **no** caller — none in `packages/` source, `examples/`, or the dogfood apps — that issues an update whose payload names `parent_object`, and none that re-points `thread_id`; in the console the only `sys_attachment` write is a create, and the only `sys_comment` update writes `reactions`. If you have your own "move this attachment" or "move this comment" flow, point it at a target object that declares the capability, or declare it on the target. - -No new error code: both codes are existing standard-catalog members already registered in `packages/spec/src/api/error-code-ledger.zod.ts` and already mapped to 403 by `packages/rest/src/error-response.ts`. diff --git a/.changeset/clean-first-install-peer-warnings.md b/.changeset/clean-first-install-peer-warnings.md deleted file mode 100644 index c3ebe6f919..0000000000 --- a/.changeset/clean-first-install-peer-warnings.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"create-objectstack": patch -"@objectstack/cli": patch ---- - -**First-run polish:** a brand-new scaffold's very first `pnpm install` no longer reports two unmet peer dependencies (#10326). - -Reproduced on a clean scaffold from published `create-objectstack@17.1.0` — no lockfile, `node_modules` removed, nothing configured by the user — and again on the second scaffold path, `objectstack init`. Both printed the same two: - -``` -✕ unmet peer better-call - Installed: 1.4.0 - Wanted: - 1.3.7: - @better-auth/scim@1.7.0-rc.1 - -✕ unmet peer better-sqlite3 - Installed: 13.0.3 - Wanted: - ^12.0.0: - better-auth@1.7.1 -``` - -Nothing was broken — but it is the first screen a newcomer sees, and there is nothing they did to cause it or can do about it. - -**`better-sqlite3`: the pin is right and the upstream range is stale — so it is widened, not corrected.** better-auth 1.7.1 declares `better-sqlite3` as an **optional** peer at `^12.0.0`, and it governs exactly one configuration: a raw better-sqlite3 `Database` handed to better-auth's `database` option, which its Kysely dialect then drives. ObjectStack never takes that path — `AuthManager.createDatabaseConfig()` returns `createObjectQLAdapterFactory(dataEngine)`, and every `better-sqlite3` use under `plugin-auth` is knex's `client: 'better-sqlite3'` beneath ObjectQL. Measured anyway on the configuration the range *does* govern: better-auth 1.7.1 with `database: new Database(':memory:')`, running `getMigrations().runMigrations()`, `signUpEmail`, `signInEmail` and adapter `findOne`/`update`/`delete`, is green on **better-sqlite3 13.0.3** and byte-for-byte equivalent on **12.11.1**. The same probe with `Database.prototype.prepare` neutered fails, so that green is the driver's and not an unexercised path. Pinning our own `^13.0.3` declarations back to `^12` would downgrade a native module across the platform to satisfy a range measurement shows is simply behind. - -**`@better-auth/scim`: the rc pin stays, and one `better-call` copy is the correct tree.** `npm view @better-auth/scim dist-tags` reads `latest: '1.7.1'`, but stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is a separate migration rather than a version bump; the exact `1.7.0-rc.1` pin is deliberate. The rc peers an exact `better-call@1.3.7` while better-auth 1.7.1 depends on `1.4.0` — and a better-auth plugin has to share the **host's** better-call instance, so the single 1.4.0 copy every install already resolves is right, not a skew to repair. This declaration retires together with the rc pin. - -**What changed, and what deliberately did not.** Both remedies are pnpm `peerDependencyRules.allowedVersions` entries, scoped `>` so each widens exactly one declaration. They ship *inside* the scaffold — the bundled `pnpm-workspace.yaml` template and the one `objectstack init` renders — because a block in this repo's own workspace file does not travel with published packages. `allowedVersions` changes what pnpm **reports**, never what it resolves: measured on both scaffold paths, the lockfile is byte-identical with and without it (0 lines of diff), and no dependency version, range or resolution moved anywhere. This repo's own resolutions are untouched. diff --git a/.changeset/cli-invocation-loudness.md b/.changeset/cli-invocation-loudness.md deleted file mode 100644 index 9d303eb061..0000000000 --- a/.changeset/cli-invocation-loudness.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -Two ways to invoke the CLI wrong used to present as a crashed boot. Both now say what they are. - -`node packages/cli/dist/index.js` — the package `main`, which is a re-export barrel — ran to completion, printed nothing and exited 0. Backgrounded, that is indistinguishable from a server that came up and died. It now writes two lines to stderr, the first saying that running this file starts nothing and the second naming `bin/run.js` as the CLI entry point, and exits 1. - -A rejected invocation such as `objectstack dev --no-ui` answered with oclif's error line followed by a full usage dump, and in a background log the dump is what the eye lands on. One line now goes to stderr ahead of it: - -``` -objectstack: INVOCATION ERROR — Nonexistent flag: --no-ui. The command never ran: nothing was started and nothing is listening. Invoked as: objectstack dev --no-ui -``` - -No flag surface changed: `dev` still rejects `--no-ui` (only `serve` declares `ui` with `allowNo`). What changed is what the CLI says when it rejects an invocation. diff --git a/.changeset/cli-retire-abandoned-tsup-config.md b/.changeset/cli-retire-abandoned-tsup-config.md deleted file mode 100644 index 10a4cbad73..0000000000 --- a/.changeset/cli-retire-abandoned-tsup-config.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -Remove the abandoned tsup build path from `packages/cli` (#10185): the -`tsup.config.ts`, the orphaned `src/bin.ts` it was the only referrer of, and -the now-unused `tsup` devDependency. - -The package has built with `tsc -p tsconfig.build.json` since the oclif -migration, which also introduced `oclif.commands.target: "./dist/commands"` -and moved the `bin` field onto `bin/run.js`. The tsup config was left behind -by that commit and never invoked again — but it was not inert. It declared -`clean: true` with only `src/bin.ts` and `src/index.ts` as entries, so anyone -running the obvious `tsup` next to a `tsup.config.ts` would wipe `dist/` and -emit no `dist/commands/**` at all, leaving a CLI that resolves zero commands. -Deleting it removes the trap rather than documenting it. - -No published behaviour changes: the resolved oclif command surface is -identical before and after (60 commands, 68 topics). The only build-output -difference is that `dist/bin.js` — a re-export of `execute` from -`@oclif/core` that nothing imported — is no longer emitted. diff --git a/.changeset/cli-serve-host-anchored-cluster-import.md b/.changeset/cli-serve-host-anchored-cluster-import.md deleted file mode 100644 index 9778ee09b5..0000000000 --- a/.changeset/cli-serve-host-anchored-cluster-import.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -Fix `os serve` failing to boot with `OS_CLUSTER_DRIVER=redis` when the app -declares `@objectstack/service-cluster` (#10645). The cluster gate and its -driver were reached through a bare dynamic `import()`, which Node ESM resolves -against the CLI's own realpath — inside the framework workspace — so packages -installed under the host app were invisible to it and boot died with -`Cannot find package '@objectstack/service-cluster'`. Both loads now go through -the host-anchored importer `serve` already uses for its other optional and -enterprise packages, so any package the app declares resolves the way the app -declares it. The host importer is now defined at the top of the boot sequence -rather than partway down, which is what made these two loads fall back to bare -resolution in the first place. No change to what `serve` accepts or refuses: -an undeclared package is still refused by the same declaration gate. diff --git a/.changeset/companion-source-never-primary-key.md b/.changeset/companion-source-never-primary-key.md deleted file mode 100644 index 674704b49b..0000000000 --- a/.changeset/companion-source-never-primary-key.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -The `__search` companion is no longer provisioned or backfilled on objects whose only companion source is the primary key (#10290) - -`resolveSearchCompanionSources` resolves the companion's source through -ADR-0079's `resolveDisplayField`. That derivation ends at "first title-eligible -field by declaration order", and on a table whose only text column IS its -primary key — system tables, junction tables, append-only logs — it lands on -`id`. `id` is `type: 'text'`, not hidden and carries no `requiredPermissions`, -so it passed the eligibility gate: `provisionSearchCompanion` declared a -`__search` column on those objects and `plugin-pinyin-search`'s backfill walked -them at every boot. - -That work is doomed by construction rather than merely unlikely. Both writers — -the `beforeInsert`/`beforeUpdate` stamp and the boot backfill — gate on -`containsCJK(row[source])`, and a platform-generated primary key is ASCII by -construction, so the predicate can never be true. Measured on a real -`bootStack` of `examples/app-showcase`: **20 of the 66 objects** the backfill -enumerated were in this state, walking whole platform tables to compute nothing -— `sys_secret`, `sys_oauth_access_token` and `sys_jwks` among them. - -`resolveSearchCompanionSources` now returns `[]` when the resolved display -field is the record's primary key, and `isPrimaryKeyField` is exported as the -named judgement behind it. - -**Keyed on the field's ROLE, not on "resolved by fallback".** The registry's -materialization seam runs `provisionPrimary(schema, { synthesize: false })` -before this module — a contractual order — and that pass writes `nameField: -'id'` onto the document, so by the time provisioning asks, a derived fallback -and an author's explicit pointer are byte-identical. The role is readable from -the name because that is where the platform keeps it: the driver provisions -`id` on every physical table unconditionally and there is no per-field -`primaryKey` marker in the spec, which is why `isPreservableUnderAudit` already -keys on `SystemFieldName.ID` for the same reason. `_id` is refused as the -alternate spelling of the same address. - -**This interprets ADR-0079, it does not amend it.** The title contract is -untouched: `resolveDisplayField` still resolves `id`, `provisionPrimary` still -designates it, and `resolveRecordDisplayName` still renders the `Record #` -floor. Only the search normalizer declines to take its input from there — the -same distinction #4483 drew one seam over on the READ path, where the display -field's job in the `$search` auto-default is to ORDER the set and never to -ADMIT a field the exclusions already rejected (`SEARCH_AUTO_EXCLUDED_FIELDS` -names `id` and `_id`). - -**What does not change.** Existing permanently-NULL `__search` columns on -already-migrated tables stay: ADR-0045 migrations are additive and dropping a -physical column is a separate decision. Those deployments still stop walking — -the backfill skips an object whose sources resolve empty even when its schema -still declares the column. Objects with a real name/title field are unaffected: -provisioning, write-time stamping and the query-time `$or` clause all behave -exactly as before, including when the object also declares an `id` field and -when its display field is a plain text column that is not named `name`/`title`. diff --git a/.changeset/count-opt-out-and-permission-set-memo.md b/.changeset/count-opt-out-and-permission-set-memo.md deleted file mode 100644 index fd7b13e7eb..0000000000 --- a/.changeset/count-opt-out-and-permission-set-memo.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -"@objectstack/metadata-protocol": minor -"@objectstack/plugin-security": patch ---- - -Stop issuing two DB queries for questions already answered earlier in the same -request (#10757). One authenticated `GET /data/:object?$top=1` measured **24 DB -queries before, 23 after** — **22** when the caller opts out of the count. -Measured with `X-OS-Debug-Timing: json` on `pnpm dev:crm`, whose `Server-Timing` -carries `db;dur=…;desc="N queries"`. - -**`$count=false` now skips the COUNT query** (`@objectstack/metadata-protocol`). -The parameter has been declared (`ODataQuerySchema.$count`), aliased on the wire -(`$count` → `count`), reserved out of the implicit-field-filter bucket, -arity-checked and boolean-coerced for a long time — and then deleted unread, so -every paginated list ran `engine.count()` whether or not the caller wanted a -total. It is honoured now: - -``` -GET /data/task?$top=25 → { records, total, hasMore } (unchanged) -GET /data/task?$top=25&$count=true → { records, total, hasMore } (unchanged) -GET /data/task?$top=25&$count=false → { records, hasMore } (no COUNT query) -``` - -Read the shape of that carefully before adopting it: - -- **Only an explicit `false` opts out.** An ABSENT `$count` still counts and - still reports `total`. OData reads absent as "omit the count", and taking that - reading here would silently strip `total` from every existing caller — none of - them send the parameter, all of them read the number. The asymmetry is - deliberate and pinned by tests. -- **`total` is OMITTED, never estimated.** `FindDataResponse.total` is declared - optional ("if requested"), so absent is the declared shape for "not - requested". A caller that opted out and then reads `total` gets `undefined`, - not a plausible-looking guess — guard the read (`total ?? undefined`) or do - not send `$count=false`. -- **`hasMore` is still answered**, from the page alone: a full page means there - may be more. Same page-local rule the `$search` path already uses. - -**A find and its COUNT resolve permission sets once, not twice** -(`@objectstack/plugin-security`). `findData` answers a paginated list with two -engine operations, and the security middleware runs on both; each pass re-read -`sys_permission_set` for the same context with identical bindings. The -resolution is now memoized per execution context — a `WeakMap` keyed on the -context object, which is built once per request and collected with it, so -nothing outlives the caller it was resolved for — and **retired by any write**: -a process-wide epoch is bumped on every `insert`/`update`/`delete` the engine -middleware sees, ahead of the `isSystem` bypass so a seeder, a package publish -or an auto-org-admin grant invalidates too. A context whose grants are rewritten -in place re-resolves as well (the memo key covers `positions`, `permissions`, -`principalKind` and the presence of `userId`). No authorization answer is reused -across a write, across a context, or across a request. - -Not a fix for the whole cost: the remaining ~22 queries per authenticated -request are session resolution, grant resolution, localization and metadata -reads that repeat on every request. Removing those needs cross-request caching -with an invalidation design, which is deliberately not in this change. diff --git a/.changeset/datasource-cli-envelope-unwrap.md b/.changeset/datasource-cli-envelope-unwrap.md deleted file mode 100644 index d0a82a3018..0000000000 --- a/.changeset/datasource-cli-envelope-unwrap.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -**Fix:** `os datasource list-tables`, `os datasource introspect` and -`os datasource validate` now read the response envelope the server actually -emits, so all three work against a live server for the first time (#10675). - -The three commands read the pre-#3843 **flat** shape — `body.tables`, -`body.draft`, `body.results`, and `body.error` as a string — while every REST -body the platform sends is the declared envelope written by `sendOk` / -`sendError`: `{ success: true, data: { … } }` or -`{ success: false, error: { code, message } }`. Nothing failed loudly, because -each payload simply read `undefined` and every command reported that as an -ordinary empty result: - -- `list-tables` printed `No remote tables found.` while the server was - returning two tables. -- `introspect` printed `Failed to generate draft` for drafts the server had - generated. -- `validate` printed `No federated objects to validate.` and exited **0** - against drift the server had flagged `missing_column … severity:error` — a - schema gate green-lighting a CI-breaking condition it had never read. -- An unknown datasource crashed with `TypeError: first argument must be a - string or instance of Error`, because the error **object** was handed to - oclif's `this.error()` instead of `error.message`. - -`validate`'s exit code is the behaviour change to note: a datasource whose -federated objects have drifted now exits **1** where it previously exited 0. If -you have a pipeline that treats this command as advisory, it starts failing on -drift that was always there. - -A body that is **not** the declared envelope is now a loud failure rather than -an empty payload. That distinction is the point: "nothing found" is reachable -only from a server that really said so, never from a response the CLI could not -read. The legacy flat shape is deliberately *not* also accepted — a -consumer-side fallback would re-create the divergence as a second de-facto -contract. diff --git a/.changeset/delivery-dispatcher-sweep-tenant-classification.md b/.changeset/delivery-dispatcher-sweep-tenant-classification.md deleted file mode 100644 index 5f988c1867..0000000000 --- a/.changeset/delivery-dispatcher-sweep-tenant-classification.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/service-messaging": patch ---- - -Classify the delivery dispatchers' predicate writes on `sys_http_delivery` and -`sys_notification_delivery` as global environment sweeps (#10673). On a walled -deployment (`OS_TENANCY_POSTURE=isolated|group`) the SQL driver's tenant-audit -gate reported every `updateMany` these outboxes issue from the claim path as an -un-isolated write. The audit was right to ask: both objects are tenant-scoped -via `organization_id`. The answer is that these six writes — the -visibility-timeout reap and the atomic claim in `SqlHttpOutbox.claim`, -`SqlNotificationOutbox.claim` and `SqlNotificationOutbox.claimDigest` — are -issued by a `setInterval` dispatcher tick under a cluster lock, with no request -context and no tenant anywhere in the `ClaimOptions` contract, and they must -cross organizations: one outbox drains the whole environment's queue, so a -per-organization predicate would strand every other organization's deliveries. -They now pass `bypassTenantAudit` through a single documented helper that -carries that warrant. Diagnostics only — per its spec the flag never changes -what a write touches, and the row-level `ack` / `redeliver` writes are -unaffected. diff --git a/.changeset/diagnostics-request-arm-400.md b/.changeset/diagnostics-request-arm-400.md deleted file mode 100644 index e953ee6c3c..0000000000 --- a/.changeset/diagnostics-request-arm-400.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch ---- - -fix(metadata-protocol): `getMetaDiagnostics` refuses an unrecognised `type` spelling with the producer's 400 instead of answering "scanned 1 type, 0 problems" (#8924) - - - -This is a **narrowing that makes an already-classified 400 reach the caller**. -`GET /api/v1/meta/diagnostics?type=` -(and the SDK method `client.meta.getDiagnostics({ type })`) used to answer -`200 {"entries":[],"total":0,"scannedTypes":1,"scannedItems":0,"stats":{}}` — -"scanned 1 type, no issues" — for a spelling every sibling `/meta` door -refuses with a 400 that names both accepted spellings. The producer had -already classified the mistake (`status: 400`, `code: 'INVALID_REQUEST'`, -raised by `canonicalizeMetaRequestType` inside `getMetaItems`); the -diagnostics sweep's per-type `catch` swallowed the verdict into a benign -skip, and `scannedTypes: 1` then published a sweep that scanned nothing as -coverage. Maintainer ruling 2026-08-20: rethrow the 400 the same way #8855's -fix rethrows the 503. - -**Measured on a booted kernel (real HTTP), before → after:** - -``` -GET /api/v1/meta/diagnostics?type=fieldes 200 {"scannedTypes":1,"stats":{}} → 400 [invalid_request] "… Address it as 'field' or 'fields'." -GET /api/v1/meta/diagnostics?type=fields 200 (recognised plural) → 200 unchanged -GET /api/v1/meta/fieldes 400 → 400 unchanged -``` - -What is unchanged: recognised plurals (`fields`, `views`, …) still fold and -answer; a name that is a plural of nothing (`fieldz`) still answers an honest -`count: 0` entry; a genuine, unclassified listing failure still skips that -one type instead of failing the sweep; the whole-corpus sweep (no `?type=`) -cannot produce the refusal at all — its target set comes canonical out of the -registry. A caller that treated the old `200`-with-empty-stats answer as -"clean" now hears the refusal that names the accepted spellings. diff --git a/.changeset/docs-drop-standalone-output.md b/.changeset/docs-drop-standalone-output.md deleted file mode 100644 index 648743f83d..0000000000 --- a/.changeset/docs-drop-standalone-output.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@objectstack/docs': patch ---- - -docs site: drop `output: 'standalone'` so the production build stops failing - -The production build of the docs site died at the end of `next build` with -`ENOENT: no such file or directory, open '.../apps/docs/.next/next-server.js.nft.json'`, -so nothing merged to `main` reached the site. - -That file is opened by the standalone packer (`writeStandaloneDirectory` -> -`copyTracedFiles`), which Next calls **only** when `output === 'standalone'`. -Nothing in this repo consumes `.next/standalone` — no Dockerfile, workflow, -script or config references it, and `docker/Dockerfile` does not build -`apps/docs` at all — and Vercel does its own serverless packaging. The setting -served no consumer and was the sole reason that read happened, so removing it -removes the only code path that can raise this error. diff --git a/.changeset/doctor-withholds-checks-over-unexamined-tree.md b/.changeset/doctor-withholds-checks-over-unexamined-tree.md deleted file mode 100644 index a774efc685..0000000000 --- a/.changeset/doctor-withholds-checks-over-unexamined-tree.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os doctor` no longer prints `✓ Test coverage` / `✓ Deprecations` about a tree it -never examined, and no longer warns `@objectstack/spec Not built` about a -workspace that is not part of the tree (#10679). - -`findMissingTests()` and `findDeprecatedUsages()` both walk -`/packages/spec/src` — a path that exists in this monorepo and in no -application built with the framework. Both answered "that directory is not here" -with the same value they return for "I walked it and found nothing wrong" (an -empty array), so in a stock `create-objectstack -t blank` scaffold every run -printed, verbatim: - -``` - ✓ Test coverage All *.zod.ts files have matching tests - ✓ Deprecations No @deprecated tags found -``` - -about files doctor never opened. The command exits 0 either way, so "no problems -found" and "I never looked" were byte-identical to every downstream reader. - -Doctor already refuses to do this one screen down: the ADR-0120 D5e advisory's -`✓ Unique scope` is withheld unless `ledgerReadingIsComplete()` says the ledger -half was read in full. These two checks escaped that discipline; this restores -it, in the same shape #5413 used for the ledger — whether the tree was examined -is now a fact in the return type rather than an absence, so the print site -cannot reach the `✓` from the unexamined arm. Where the tree is absent doctor -prints an informational, named-reason skip instead: - -``` - ℹ Test coverage Skipped — no packages/spec/src in this directory (monorepo-only check) - ℹ Deprecations Skipped — no packages/spec/src in this directory (monorepo-only check) -``` - -`--verbose` adds the resolved directory it looked for. The skip is deliberately -not a warning: nothing is wrong in an application that has no -`packages/spec/src`, and withholding a false `✓` must not manufacture a false -`⚠`. - -The adjacent `⚠ @objectstack/spec Not built` probe read `/packages/spec/dist` -with no check that the workspace it names exists, so in an application it warned -about an absent package and prescribed `pnpm --filter @objectstack/spec build`, a -command that cannot succeed there. It is now gated on `packages/spec/package.json` -being present. Inside the monorepo the row is unchanged; outside it there is no -row, and an application's spec dependency stays covered by the `Dependencies` -check and by the spec-version-gap advisory. - -Exit codes are untouched — 1 exactly when an error row exists, warnings never -flip it. One visible consequence: a stock scaffold with no other findings now -ends on `✅ Environment is healthy and ready for development!` instead of -`⚠️ Environment is functional but has some warnings`, because the warning it -used to carry was about a workspace that was never there. diff --git a/.changeset/driver-sql-pg-introspection-search-path.md b/.changeset/driver-sql-pg-introspection-search-path.md deleted file mode 100644 index 89030fa8ab..0000000000 --- a/.changeset/driver-sql-pg-introspection-search-path.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/driver-sql": patch ---- - -**Bug fix:** on Postgres, index and schema introspection now resolve tables the way the session does, instead of assuming the `public` schema (#9350). - -`introspectIndexes` pinned `n.nspname = 'public'` and `introspectSchema` pinned `table_schema = 'public'`. For a driver whose connection carries a `searchPath` pointing anywhere else, both returned **empty** — not an error, an empty result. Measured on a live Postgres 16: for a table carrying a primary key *and* a declared unique index, `introspectIndexes` returned `[]` and `introspectSchema` listed no tables at all. - -Empty does not read as "I could not see" downstream; it reads as "there are no indexes". `assertConflictTargetHonoured` turns that into a refusal, so an `upsert` against a perfectly well-indexed table would be rejected with *no PRIMARY KEY or UNIQUE index backs them* — and index-drift detection would propose creating indexes that already exist. - -- `introspectIndexes` now resolves the table with `to_regclass(?)` and reads `pg_index` by OID. That is the same resolution every other statement in the session performs — first match along `search_path` — and it removes an ambiguity a schema list would introduce, since two schemas on the path can hold the same table name and only one of them is the one a query reaches. -- `introspectSchema` now lists `table_schema = ANY (current_schemas(false))`. - -**No change for a default deployment.** With the default `search_path`, `current_schemas(false)` is exactly `{public}` and `to_regclass` resolves into `public`, so both queries return what they returned before. The behaviour only differs where the old queries returned nothing. diff --git a/.changeset/durability-summary-reports-error-less-sink.md b/.changeset/durability-summary-reports-error-less-sink.md deleted file mode 100644 index 9b401bae80..0000000000 --- a/.changeset/durability-summary-reports-error-less-sink.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/plugin-email": patch -"@objectstack/plugin-security": patch ---- - -**Durability fix:** the two boot-time **summary** reports now reach a logger sink that has no `error` method, instead of printing nothing at all (#9748). - -`SweepLogger.error` and `ProjectionLogger.error` are both declared **optional**, and both summaries were spelled `logger?.error?.(…)` — an optional call that emits **nothing** when the method is absent. #9657 repaired the six per-row reports of this shape; it could not see these two, because `check:durability-log-level` only judges a call inside a `catch`, and a summary sits after the loop. Against a `{ info, warn }` sink the result was that the repair made the split **worse**: the per-row detail arrived at `warn` while the count of failures vanished, so the detail and the total reported through different channels. - -- `sweepStrandedOutbox()` — *"N stranded `sys_email` row(s) could NOT be delivered"*. Mail the platform **accepted** and never delivered, previously summarised to nobody. -- `reconcilePermissionSetProjection()` — *"N FAILED backfill(s)"*. Worse than a plain omission here: the `else` branch carrying the `info` "reconciled" line is skipped too, so such a sink heard **neither** — the reassuring half-truth this rule exists to remove, arrived at from the other side. - -Both now reach for `error` and fall back to `warn`, never to silence — the same repair shape #9657 applied to the per-row lines. A sink that **does** have `error` is unaffected and still gets the summary at `error`; a downgraded level is a degradation of the channel, never of the message, so the consequence and the fix survive the fallback intact. - -Also enforced from now on: `check:durability-log-level` grew a **summary limb** that judges a report keyed on the counter a durability-critical `catch` accumulated into, so this class cannot regress silently. The limb never second-guesses a chosen log **level** — it only checks that a call that reaches for `error` can actually print. diff --git a/.changeset/external-validate-read-capability.md b/.changeset/external-validate-read-capability.md deleted file mode 100644 index c1b5b84cfb..0000000000 --- a/.changeset/external-validate-read-capability.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -**Behaviour change (tightening) — `POST /datasources/:name/external/validate` now requires `manage_platform_settings`** (#10255, completing the #9901 federation-family gate). This was the one route of the external-datasource federation family still admitting **any authenticated caller**; it now requires the same capability as the family's two read routes. Maintainer ruling, 2026-08-20 (verbatim: 「同意你的意见。」, accepting option A on #10255). - -**This is published SDK surface.** `datasources.external.validate` on `ObjectStackClient` and the CLI's `os datasource validate` reach exactly this route. An existing integration that presents a valid credential — a better-auth session or a `sys_api_key` — and does not hold `manage_platform_settings` was served before and is **refused now**: `403` with the standard catalog code `PERMISSION_DENIED` (ADR-0112), the message naming the missing capability so the caller knows which grant to request. The anonymous floor is unchanged: no identity is still `401 UNAUTHENTICATED`. - -**Why the read capability.** `validateAll` drives the same live remote-schema introspection the family's gated read routes expose (`introspect` per datasource), and its report — schema diffs naming remote columns and types, driver error strings for unreachable remotes — is a read of the same federation surface. An unentitled caller refused at `GET /:name/external/tables` could previously still trigger live remote introspection through this route and read what it found. One family, one door-type: reads on `manage_platform_settings`, writes on `manage_metadata`. - -**Migration.** Grant the calling credential's permission set `manage_platform_settings` — the same grant the family's read routes have required since #10254, so an integration already migrated for those is covered. The platform's `admin_full_access` set carries it; a purpose-built operator set is the case to check. diff --git a/.changeset/federated-sweep-phantom-columns.md b/.changeset/federated-sweep-phantom-columns.md deleted file mode 100644 index ff113f9888..0000000000 --- a/.changeset/federated-sweep-phantom-columns.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -**Waste removed:** the lifecycle dangling-reference audit no longer asks a federated (ADR-0015 `external`) remote for platform anchor columns that were never provisioned on it (#8414). - -`applySystemFields` injects `organization_id`, `owner_id`, `owning_business_unit_id` and the audit `*_by` lookups into every registered object, federated ones included — that is deliberate (#7865, direction B). `Engine.syncObjectSchema` then issues no DDL for a federated object, because the remote database owns its schema. So those five reference columns existed in the registered schema and nowhere else, and `auditDanglingReferences` — which enumerated reference fields off `fields` alone — projected all of them onto the remote table. Measured on a real boot of `examples/app-showcase`, against a `customers` table whose real columns are `id, name, email, region, lifetime_value`: - -``` -select `id`, `organization_id`, `created_by`, `updated_by`, `owner_id`, `owning_business_unit_id` from `customers` limit ? -select * from `customers` limit ? -``` - -The first statement cannot compile (`no such column` — a backtick-quoted identifier does not take SQLite's double-quote literal fallback, and Postgres/MySQL raise their own error); `SqlDriver.find`'s unknown-column recovery caught it and retried `select *`, fetching up to 500 whole rows to audit columns that cannot exist — once per federated object, every lifecycle sweep interval, each pass also emitting a #4363 non-deterministic-paging warning. **No answer was ever wrong**; the pass was pure waste, and it was being absorbed by a safety net rather than by a design. - -The enumerator now consults `unprovisionedInjectedColumns` (`@objectstack/spec/data`, the #7865 provenance derivation) and skips columns that are the platform's own injected anchor on an object the platform provisions no storage for. - -**This reads provenance, not `external != null`.** A federated object that declares a real remote `organization_id` — or any other anchor name — keeps its audit on that column: the author's definition is not byte-identical to the shipped one, so provenance answers `'author'` and nothing is withheld. Objects the platform provisions storage for are untouched: the derivation returns an empty set for them, so an ordinary object is still swept with its full column set. - -Two consequences worth knowing: - -- A federated object left with **no real reference column** is no longer read at all, and is deliberately not filed in `unscannedObjects` — a column that was never provisioned stores no reference, so its absence from `dangling` is proven, not assumed. A federated object that declares a real reference column is still opened and audited on it. -- `AuditableObject` now carries an index signature. The port was already being handed the whole registered document (the engine passes `SchemaRegistry.getAllObjects()` straight through); the type now says so, because the provenance derivation reads the injection plan's inputs off it. Hand-written doubles carrying only `name`/`fields` still satisfy the type and behave exactly as before. - -The card also named `backfillSearchCompanion` (`@objectstack/plugin-pinyin-search`) for `select `id`, `name`, `__search` from `customers``. **That statement is already gone and this release changes no code for it:** #9469 stopped `provisionSearchCompanion` from declaring `__search` on a federated object, so the backfill's existing `if (!schema.fields[SEARCH_COMPANION_FIELD]) continue` early-out drops those objects before enumerating anything. A second federation-aware guard inside the backfill would have been redundant, and — spelled as "skip external objects" — would have wrongly withheld the companion from a federated object whose author declares a real remote `__search`. The precondition is now pinned on a real boot instead. diff --git a/.changeset/federation-family-capability-gate.md b/.changeset/federation-family-capability-gate.md deleted file mode 100644 index 37fc9f011d..0000000000 --- a/.changeset/federation-family-capability-gate.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -**Behaviour change (tightening) — a capability is now required on the external-datasource federation family** (`/api/v1/datasources/:name/external/*`, #9901). These routes previously admitted **any authenticated caller**; four of the five now also require a platform capability. Maintainer ruling, 2026-08-20 (verbatim: 「其他接受你的建议。」). - -**This is published SDK surface.** `datasources.external.*` on `ObjectStackClient` reaches exactly these routes, and the CLI's `datasource` commands go through them. An existing integration that presents a valid credential — a better-auth session or a `sys_api_key` — and holds neither capability was served before and is **refused now**. Nothing about the credential itself changed; what changed is what the credential must carry. - -| route | SDK call | now requires | -| --- | --- | --- | -| `GET /:name/external/tables` | `datasources.external.listTables` | `manage_platform_settings` | -| `POST /:name/external/tables/:remote/draft` | `datasources.external.draft` | `manage_platform_settings` | -| `POST /:name/external/tables/:remote/import` | `datasources.external.import` | `manage_metadata` | -| `POST /:name/external/refresh-catalog` | `datasources.external.refreshCatalog` | `manage_metadata` | -| `POST /:name/external/validate` | `datasources.external.validate` | *(unchanged — authentication only)* | - -A refusal is **`403` with the standard catalog code `PERMISSION_DENIED`** (ADR-0112; deliberately not the grandfathered `FORBIDDEN` synonym), and the message names the missing capability so the caller knows which grant to request. The anonymous floor is unchanged: no identity is still `401 UNAUTHENTICATED`. - -**Why these two capabilities.** The first two routes are the declared twins of `GET /:name/remote-tables` and `POST /:name/object-draft` on the datasource-admin spelling, which has required `manage_platform_settings` since #9593 — the same operation was reachable through two mounted routes with two different admission policies, so an agent or integration refused at one spelling was served at the other. The two write routes have no twin and create live metadata (the import mounts a runtime-origin federated object; the refresh rewrites the cached catalog snapshot), so they take `manage_metadata`, this package's existing gate for metadata creation. - -**Migration.** Grant the caller's permission set the capability its routes need — `manage_platform_settings` for remote-schema introspection, `manage_metadata` for import/refresh. The platform's `admin_full_access` set already carries both, so admin-credentialed integrations are unaffected; a purpose-built operator set is the case to check. diff --git a/.changeset/formula-scale-at-producer.md b/.changeset/formula-scale-at-producer.md deleted file mode 100644 index 7611aa63f6..0000000000 --- a/.changeset/formula-scale-at-producer.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -Apply a `formula` field's declared `scale` when the formula is evaluated -(#10280). `Field.formula({ scale: 2 })` was accepted and then ignored: a -percentage formula such as `(record.num_responses * 100.0) / record.num_sent` -**returned** `41.666666666666664`, so the API response — and the record page -rendered from it — carried all fifteen digits despite the declaration. - -The value is now rounded where it is produced, in the engine's formula -evaluation, so all three surfaces that materialize a formula inherit it: list -reads, single-record reads, and the record a write responds with. - -- **Rounding is `Number(v.toFixed(scale))`** — round-half-away-from-zero, the - same arithmetic the console's client-side computed columns use. Negatives - round away from zero: `-1.5` at `scale: 0` is `-2`, not `-1`. -- **A formula declaring no `scale` is unchanged** and keeps full precision. -- **Non-numeric results are untouched** — a formula returning a string, - boolean or `null` is returned as-is. -- A formula value is **returned, never stored** — it is virtual and has no - column. Rounding it at the producer is what makes an app's own copy of that - result writable into a stored `DECIMAL(10, 2)`-style field, which previously - failed that field's decimal validation. - -Unchanged: `scale` on a **caller-supplied** number (`Field.number`, -`Field.currency`, …) is still enforced by **rejection** (`max_scale`), never by -rounding. A value someone sent has an author to refuse; a platform-computed -formula result does not. diff --git a/.changeset/hono-server-readme-kernel-bootstrap.md b/.changeset/hono-server-readme-kernel-bootstrap.md deleted file mode 100644 index 1ba759b8c5..0000000000 --- a/.changeset/hono-server-readme-kernel-bootstrap.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@objectstack/plugin-hono-server": patch ---- - -docs(plugin-hono-server): boot the kernel with the method it actually ships (#9870) - -`packages/plugins/plugin-hono-server/README.md` is in the package's `files` array -with `private` unset, so it is the page npm renders. Its Usage block ended: - -```ts -const kernel = new ObjectKernel(); -kernel.use(new HonoServerPlugin({ port: 3000, /* … */ })); -await kernel.start(); -``` - -Measured against the built type surface: `ObjectKernel` (re-exported by -`@objectstack/runtime` from `@objectstack/core`) declares `bootstrap()` and -`shutdown()` and has **no** `start` member. A reader copying the block gets a -compile error on its last line. - -The line reads plausibly because the `IKernel` *interface* in -`@objectstack/types` does declare `start()` — but the concrete class the fence -constructs does not implement that name, and eight sibling READMEs -(`objectql`, `rest`, `runtime`, `service-cache`, `service-job`, -`service-automation`, `service-package`, `service-cluster-redis`) all spell the -same step `await kernel.bootstrap()`. Fixed to match. - -Found by the call-site widening in the same PR, not by hand: the receiver is -never import-bound, so before that widening this call site was one of the 262 -`check:published-readme-exports` could not read. diff --git a/.changeset/http-request-errors-total-retired.md b/.changeset/http-request-errors-total-retired.md deleted file mode 100644 index 210fcaf471..0000000000 --- a/.changeset/http-request-errors-total-retired.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -"@objectstack/observability": minor -"@objectstack/runtime": minor -"@objectstack/spec": minor ---- - -fix(observability): **BREAKING** — `http_request_errors_total` is retired (ADR-0049 enforce-or-remove, #9834) - -**⛔ If you have a Grafana panel, an alert rule or a recording rule keyed on -`http_request_errors_total`, it will read a FLAT ZERO after this upgrade.** That -zero is the removal, not a healthy server, and it is the one way this change can -hurt you — nothing throws, nothing warns, the series simply stops receiving -samples. Rewrite the query before you deploy. - -Maintainer ruling 2026-08-20: **RETIRE**. The name was declared in `SEMCONV` as -part of a stable namespace *"so hosts can wire alerts/dashboards against it"*, -but the only emitter was `@objectstack/runtime`'s `instrumentRouteHandler`, -applied only by the dispatcher's own route Proxy — so the series never saw -auth's `getRawApp()` mount, the REST data API via `RouteManager`, or any other -inbound surface. Its two siblings in the same HTTP family moved to the -`IHttpServer.afterResponse` transport seam (`http_requests_total`, #9650/#9835; -`http_request_duration_ms`, #9834/#10004) and this one could not follow: -`HttpResponseObservation` carries `{method, routePattern, status, elapsedMs}` -and **no throw signal of any kind**, so every transport-side shape would have -counted a *different* population rather than the same one more widely. - -Migration (FROM → TO): - -| Wrote | Write instead | -|---|---| -| `rate(http_request_errors_total[5m])` in a panel or alert | `rate(http_requests_total{status=~"5.."}[5m])` — emitted by the transport, so it covers every inbound surface instead of the dispatcher's routes only | -| `sum by (route) (http_request_errors_total)` | `sum by (route) (http_requests_total{status=~"5.."})` | -| `SEMCONV.httpRequestErrorsTotal` / `RUNTIME_METRICS.httpRequestErrorsTotal` in host code | Delete the read. Both members are gone; `tsc` reports the missing property at the read site. | - -One-line fix: replace the metric name with `http_requests_total{status=~"5.."}`. - - - -**The replacement is wider, not merely different.** The retired counter was -divergent from a 5xx rate in *both* directions, measured: the dispatcher answers -its own errors through `errorResponseBase`, which sets a status and does **not** -re-throw — so the counter **missed** those — while its `catch` incremented -unconditionally, so a **thrown 4xx WAS counted** as an error. And -`http_requests_total` already carries a `status` label, so a status-class error -counter was fully derivable from data the transport already publishes. Prove the -new query wider rather than merely non-empty: make an auth route or a REST -data-API route answer 5xx and confirm it moves, where the retired counter would -not have moved at all. - -**If what you were actually alerting on was "a handler threw rather than -returning an error envelope"** — the one signal this counter uniquely carried — -that is the `errorReporter`, not a metric. Wire an `ErrorReporter` adapter -(Sentry / Datadog / your own); it still fires on every 5xx throw and is -untouched by this change. - -What is NOT removed: `http_requests_total`, `http_request_duration_ms`, -request-id propagation, the 5xx error reporter, and the -`res.__obsRecordedError` side channel that carries a swallowed error to it. The -dispatcher still instruments every route it mounts; it just no longer publishes -a fourth series whose name promised more coverage than it had. diff --git a/.changeset/i18n-inline-map-retired-spellings-by-name.md b/.changeset/i18n-inline-map-retired-spellings-by-name.md deleted file mode 100644 index 75e61e5a57..0000000000 --- a/.changeset/i18n-inline-map-retired-spellings-by-name.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -fix(spec): reject the retired `key`/`defaultValue` spellings in inline locale maps BY NAME, in any combination — and stop claiming the retired form "resolves to nothing" (#10492) - -Two legs, both on `InlineLocaleMapSchema` in `packages/spec/src/ui/i18n.zod.ts`: - -1. **Message accuracy.** The `INLINE_LOCALE_KEY` rejection message said the - retired key-reference form (#5055) "resolves to nothing". Measured false: - both resolvers — `resolveI18nLabel` here and objectui's `pickLocalized`, - parity-pinned — fall through to their last resort (first string value, in - key insertion order) and return the raw dotted key, which renders as the - visible label. The message now states the measured behaviour. - -2. **Enforcement hole closed.** `key` is three letters — syntactically a valid - BCP-47 primary subtag — so `{ key: 'common.save' }` alone parsed as a - "language `key` inline locale map" and painted `common.save` on screen; the - pair form was rejected only because `defaultValue` fails the tag grammar. - The key pattern now refuses the two retired spellings by name, in any - combination, matching the emitted type's `{ key?: never; defaultValue?: - never }` narrowing (#9925, maintainer ruling 2026-08-19, option B). This is - an enforcement gap of the #5055 retirement, not a new contract: nothing else - is denied — real 2–3 letter subtags (`deu`, `fra`, `yue`) still parse. - -FROM → TO: a label authored as `{ key: '' }` (or any inline map -carrying a `key`/`defaultValue` entry) is now refused at parse time with the -named message; write the inline locale map form `{ en: '…', 'zh-CN': '…' }`, -or a plain string resolved through a translation bundle. This is the same -prescription the #5055 retirement and the #9925 type narrowing already carry — -the runtime now enforces what the type already refused. - - diff --git a/.changeset/impersonate-user-platform-admin.md b/.changeset/impersonate-user-platform-admin.md deleted file mode 100644 index 6169aa62cf..0000000000 --- a/.changeset/impersonate-user-platform-admin.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/plugin-auth": patch ---- - -**Fix:** `POST /api/v1/auth/admin/impersonate-user` now admits ObjectStack **platform admins**. It previously refused every one of them with `403 YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS` — byte-identical to the refusal a plain member received — so the `sys_user` "Impersonate User" button was dead on every deployment (#9968). - -better-auth's `admin` plugin authorizes on the legacy `user.role === 'admin'` scalar that ADR-0068 D2 stopped synthesizing. ObjectStack's platform admin is a `sys_user_permission_set` row pointing at `admin_full_access` with `organization_id = null`, which the vendor cannot be pointed at, and re-synthesizing the scalar is permanently vetoed. - -**What an operator will now observe.** A platform admin who could not impersonate anyone can now impersonate a non-admin user, and the impersonation takes effect for cookie and bearer clients alike. Refusals are unchanged for everyone else: a signed-in non-platform-admin (including an organization owner or admin, who is **not** a platform admin under ADR-0068) still gets `403 YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS`, and an anonymous caller still gets `401` from better-auth's own `adminMiddleware`. - -**One refusal is newly reachable.** The vendor refuses to impersonate an admin-grade *target* by reading that same `role` scalar against `adminRoles: ['admin']` — a column nothing writes after ADR-0068 D2, so the guard was inert. It is now asked through the ADR-0068 predicate, so impersonating a **platform-admin target** is refused with `403 YOU_CANNOT_IMPERSONATE_ADMINS` where it previously succeeded. - -Implemented as a better-auth **plugin endpoint**, replacing the vendor endpoint in place on the `admin` plugin's own `endpoints` record — not a raw Hono mount. That keeps the signed-cookie contract with `/admin/stop-impersonating` and keeps the `/admin/impersonate-user` path-keyed rotation hook attached, so bearer-client impersonation does not regress to a silent 200 no-op. - -Every other better-auth-native `/admin/*` route still gates on the legacy scalar and still refuses platform admins — unchanged here. diff --git a/.changeset/init-scaffold-pnpm11-allow-builds.md b/.changeset/init-scaffold-pnpm11-allow-builds.md deleted file mode 100644 index 0241e5ea2a..0000000000 --- a/.changeset/init-scaffold-pnpm11-allow-builds.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`objectstack init` now writes both build-approval keys into the scaffolded -`pnpm-workspace.yaml`, so a brand-new project's first `pnpm install` succeeds -on pnpm 11 (#10405). - -The renderer emitted only `onlyBuiltDependencies`. pnpm 11 does not read that -key at all, and it turned an unapproved dependency build script from a warning -into a hard error — so `objectstack init my-app && cd my-app && pnpm install` -exited 1 with `ERR_PNPM_IGNORED_BUILDS`, on the very first command after -scaffolding. The rendered file now also carries `allowBuilds`, built from the -same source list, which is the only key pnpm 11 reads. Measured one clean -install per pnpm version, each with its own store: pnpm 10.0.0-10.25.0 read -`onlyBuiltDependencies`, 10.26.0-10.34.x read either key, and 11.x reads -`allowBuilds` only — so both keys are load-bearing and neither is redundant. - -Build permission is still granted to exactly the two packages that need it and -nothing else: `esbuild` (a `postinstall` that installs its platform binary, -used to compile `objectstack.config.ts`) and `better-sqlite3` (ships a -`binding.gyp`, which pnpm treats as a native build; without it `objectstack -serve` can fail with "Could not locate the bindings file"). No wildcard. - -Existing scaffolds are unaffected — `init` never overwrites a -`pnpm-workspace.yaml` that is already there. To fix a project scaffolded by an -earlier CLI, add to its `pnpm-workspace.yaml`: - -```yaml -allowBuilds: - better-sqlite3: true - esbuild: true -``` diff --git a/.changeset/kernel-timeout-guard-reclaim.md b/.changeset/kernel-timeout-guard-reclaim.md deleted file mode 100644 index 561d0c8c1d..0000000000 --- a/.changeset/kernel-timeout-guard-reclaim.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -"@objectstack/core": patch ---- - -The kernel's two `Promise.race` timeout guards — the startup guard around each -plugin's `init`/`start`, and the shutdown guard around `performShutdown()` — -now reclaim **both** halves of the guard when the race settles: the timer is -cleared *and* the losing promise is settled (#10604). - -Neither site settled its loser, so the timeout promise and the reaction -`Promise.race` held on it were retained for the life of the process — four -leaking promises per showcase test run under `vitest --detectAsyncLeaks`, now -zero. The two hand-rolled copies had also drifted into doing opposite halves of -the same cleanup: the startup site cleared its timer and never `unref`'d, the -shutdown site `unref`'d and never cleared. Both now go through one internal -`TimeoutGuard`, so they cannot drift apart again. No exported API changes. - -**Behaviour change, at the shutdown guard:** the shutdown timer is no longer -`unref()`d. Two consequences for an embedding host (CLI, auth-proxy, test -runner): - -- After a **successful** shutdown, no timer is left armed. Previously the guard - survived its own race and stayed scheduled to fire against a kernel already - `'stopped'`. That late rejection was *handled* — `Promise.race` had attached a - rejection handler to it — so this was never an unhandled-rejection risk; it - was retained work and a wakeup after teardown. -- When teardown **hangs**, the guard now actually fires. An unref'd timer does - not keep the event loop alive, so a process with nothing else to run could - exit silently — status 0, teardown incomplete — before `shutdownTimeout` - elapsed, leaving `Shutdown timed out — forcing exit` and its `exit(1)` - unreachable in exactly the case they exist for. Reclaiming on settle keeps the - guard ref'd exactly as long as the race is undecided, which is the guarantee - the startup guard already had (#4813). - -If your host relied on a hung `shutdown()` letting the process fall out of the -event loop on its own, it will now wait up to `shutdownTimeout` (default 60s) -and then hard-exit with status 1. Lower `shutdownTimeout` in the kernel config -to shorten that window. diff --git a/.changeset/lifecycle-prose-family-10526-10336.md b/.changeset/lifecycle-prose-family-10526-10336.md deleted file mode 100644 index 7b3934d4c6..0000000000 --- a/.changeset/lifecycle-prose-family-10526-10336.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Correct two stale author-facing contract statements in `Object.enable` / `Object.lifecycle` — text only, no change to what parses. - -- `lifecycle.ttl.onlyWhen` × `archive` (#10526): the refusal's rejection message no longer says "the Archiver moves rows by age alone". Since #10347 the Archiver selects candidates by the declared ttl cutoff, so that reason had gone stale; the reason it states now is the one that holds — the ttl **window** carries over to the Archiver, the `onlyWhen` **filter** does not, so the filtered-out rows would still be archived. The refusal itself is unchanged. -- `enable.files` / `enable.feeds` (#10336): the two `.describe()` strings said the flags reject *creation*. Since #10170 both capability gates are registered on `beforeUpdate` as well, so they refuse any write that makes a row **target** the walled object — a create and an update that re-points/re-threads an existing row alike (403 `FILES_DISABLED` / `FEEDS_DISABLED`). The strings now state that, matching the docblocks above them. `enable.activities` is unaffected and untouched. diff --git a/.changeset/lifecycle-triple-alignment-refine.md b/.changeset/lifecycle-triple-alignment-refine.md deleted file mode 100644 index a564fd0fdf..0000000000 --- a/.changeset/lifecycle-triple-alignment-refine.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -`LifecycleSchema` now refuses the `retention` + `ttl` + `archive` triple at -parse time unless the ttl restates the age bound exactly — `ttl.field: -'created_at'` with `ttl.expireAfter` equal to `retention.maxAge` (#10527). - -Since #10347 the Archiver selects the rows it moves by the declared ttl cutoff -(`ttl.field` older than `ttl.expireAfter`) whenever `ttl` is declared, and by -`created_at`/`archive.after` only when it is not. On a diverging triple that -leaves `retention.maxAge` (pinned equal to `archive.after` by the existing -alignment refine) declared but enforced by nothing — a row whose `ttl.field` -sits in the future stays hot past `retention.maxAge`, silently. A declared -bound nothing enforces is the class this block already refuses loudly, so the -divergence is now rejected at authoring time with a named message instead of -being resolved by whichever column the sweep happens to read. - -No shipped or example object declares the triple (censused in #10527: -`sys_audit_log` and `sys_metadata_audit` are the only archive-declaring -objects, both `retention` + `archive` pairs) — so no bundled object changes -behaviour, and the ruled-legal shapes are unchanged: `retention` + `archive` -aligned pairs and `ttl` + `archive` pairs parse exactly as before. diff --git a/.changeset/lint-checked-parse-findings.md b/.changeset/lint-checked-parse-findings.md deleted file mode 100644 index f3d47733a7..0000000000 --- a/.changeset/lint-checked-parse-findings.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -Report an unparseable source instead of scoring it CLEAN (#10653). - -Four validators parsed authored source with `ts.createSourceFile` and never read -`parseDiagnostics`. That call **cannot throw**, so a source with syntax errors -came back as a tree built by error recovery, got walked like any other, and -produced no findings — a source the validator could not read, reported as a -source with nothing to report. Two of the sites carried a `try/catch` around the -parse that never once ran. - -Each now reports what it could not read, as a finding the author receives rather -than as an exit — a publish-time validator is handed metadata by someone else, -so ending the process on their input is not its call. Four new advisory -(`warning`) rule ids, all additive: every finding these rules produce today they -still produce, including from a partially recovered tree. - -- `react-page-source-unparseable` — `kind:'react'` page source - (`validateReactPageProps`) -- `startup-source-unparseable` — plugin source (`findStartupRegistryVerdicts`) -- `hook-body-source-unparseable` — L2 hook body (`validateHookBodyWrites`) -- `action-body-source-unparseable` — L2 action body (`validateActionBodyWrites`) - -New exports: the four rule-id constants, plus `describeParseFailure`, -`PARSE_FAILURE_HINT` and the `SourceParseFailure` / `CheckedParse` / -`CheckedParseOptions` types. `ExtractedHookBodyWriteSet` gains an optional -`parseFailure`, so a consumer of the extractor can tell "wrote nothing" from -"could not be read" — the distinction that was missing. - -Nothing is removed or renamed, and no source that parses gains a finding. A -stack whose authored sources all parse lints exactly as before; one carrying a -source with a syntax error gains a warning that names the file, line and column -instead of silently skipping the checks. diff --git a/.changeset/localization-context-ttl-cache.md b/.changeset/localization-context-ttl-cache.md deleted file mode 100644 index ff3c4fd46d..0000000000 --- a/.changeset/localization-context-ttl-cache.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/core": patch ---- - -`resolveLocalizationContext` now memoizes a FAILED read's fallback per `(ql, tenantId, userId)` for 30s (#10221). - -On a fresh environment whose `sys_setting` table hasn't been created/migrated yet, every authenticated request re-ran the same `sys_setting` localization read, and every one of those reads failed the same way ("no such table"). The `#2409` batching had already collapsed the three per-key reads a single request used to issue into one query, but that one query still repeated on every subsequent request, and `driver-sql`'s `backendStatementFault` logs a `[sql-driver] DATABASE_ERROR` warning on every failed read — so the identical warning printed once per request and buried real errors in between. - -Only the case where the underlying read genuinely fails (a backend fault, e.g. the missing table) is cached; a successful read — including a legitimate "nothing configured yet" empty result — is never cached and always re-reads on the next call, so a settings write takes effect immediately. (An earlier version of this fix cached every outcome, mirroring `packages/plugins/plugin-audit/src/audit-writers.ts`'s existing TTL cache of this same read — safe there because audit-trail enrichment is best-effort, but not safe for `@objectstack/rest`'s use of this function: analytics date-bucketing reads the org timezone on every query and `packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts` — the #1982/#2018 golden regression — asserts the very next read reflects a just-written timezone.) The `UTC` / `en-US` fallback behavior itself is unchanged; this only stops the failing query — and its log line — from re-running every request. The cache is keyed on the `ql` engine instance first, so two environments/tenants sharing one process never share a cached outcome, and self-heals within one TTL window once `sys_setting` exists. diff --git a/.changeset/manager-approver-org-screen.md b/.changeset/manager-approver-org-screen.md deleted file mode 100644 index 26301d1362..0000000000 --- a/.changeset/manager-approver-org-screen.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -"@objectstack/plugin-approvals": minor ---- - -fix(approvals): screen the `manager` approver to the request's organization (#10153) - -`expandApprovers` hands the directory organization to every graph-shaped -approver expansion — `department`, `position`, `org_membership_level`. The -`manager` branch did not: `lookupManager` read `sys_user.manager_id` under a -system context and took no organization argument at all. `sys_user` is a global -identity table with no `organization_id`, so nothing else on that path supplied -the tenancy fact either. A `manager_id` crossing an organization boundary -therefore routed the submission to an approver **in another organization** — an -out-of-tenant person granted approval authority over the record. - -The same column has been screened on the hierarchy side since cloud#1195. This -brings the approvals consumer into line for the `manager` branch. - -## What the screen is - -`lookupManager(userId, organizationId)` now resolves the manager and then asks -whether he is **provably outside** the request's organization: - -| membership rows for the manager | result | -|---|---| -| some exist, none in the request's org | **screened out** — the slot falls through to the `manager:` literal | -| one is in the request's org | resolves, unchanged | -| none exist at all | resolves, unchanged — the tenancy fact is absent, not negative | -| the `sys_member` read failed | resolves, unchanged | -| the request carries no organization | resolves, unchanged — and no read is performed | - -The fail-open half is this file's ruled posture on addressing paths, stated -twice already: `filterApproversWhoCanRead` refuses to empty a live slate on an -infrastructure hiccup, and `expandPositionUsers` carries "a step routing to -nobody is worse than one routing to a lapsed holder". A drop is logged with the -manager's id, his organizations and the request's, so the fix ("repair the link" -/ "grant the membership" / "retarget the step") is legible without a debugger. - -## ⚠️ This moves one input from accepted to refused - -A node whose **sole** approver is a cross-org `manager` and which is authored -with the **non-default** `onEmptyApprovers: 'fail'` used to open successfully; -it now throws `NO_APPROVERS`. Nothing new is thrown — a screened-out manager -leaves only a `type:value` literal, which the pre-existing empty-slate test -already classifies as empty, and `'fail'` already throws on empty. Every -screened sibling has reached that same bucket since it was written. - -**The default policy is unaffected**: `admin_rescue` still opens the request -(decidable by a privileged admin) and warns, and `auto_approve` still -auto-approves. Both directions and both policies are pinned in -`manager-approver-org-screen.test.ts`. - -## What this does NOT decide - -- **#7497** (does approver routing imply record read visibility?) stays open. - The screen reads `sys_member`, which looks like the D2 read filter beside it, - and the code says at length why it is the *sibling* treatment instead: two of - the three org-scoped expansions already screen on `sys_member.organization_id`, - and `sys_user` offers no other tenancy fact. No reads are granted and no read - screen is applied to any type that lacked one. -- **`team`** is still unscreened — it is a sibling graph expansion that is not - org-scoped either, tracked as #10230, and it touches this same file. -- `APPROVER_ORG_SCOPED` is untouched. It answers ADR-0105 D9 *retargetability* - (may an author write `organization:` on this type?), not screening, and - `manager: false` remains correct. diff --git a/.changeset/messaging-dispatchers-stop-on-shutdown.md b/.changeset/messaging-dispatchers-stop-on-shutdown.md deleted file mode 100644 index c519aa3878..0000000000 --- a/.changeset/messaging-dispatchers-stop-on-shutdown.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/service-messaging": patch ---- - -**Fix:** `MessagingServicePlugin` now releases its delivery dispatchers on `kernel.shutdown()`. Previously they kept running after shutdown had resolved (#9371). - -The plugin starts two `setInterval` dispatchers at `kernel:ready` — `NotificationDispatcher` over `sys_notification_delivery` and `HttpDispatcher` over `sys_http_delivery` — and released them from a method named `stop()`. The kernel's plugin teardown hook is `destroy()` (`Plugin.destroy?()` in `@objectstack/core`; the only teardown `ObjectKernel.performShutdown()` and `LiteKernel.destroy()` invoke), and `stop()` is not on that interface, so **nothing ever called it**. Both dispatchers went on claiming and updating delivery rows after `await kernel.shutdown()` returned. Measured on the new pin: 48 further delivery reads/writes in the 80 ms following a resolved shutdown. - -The teardown body now lives on `destroy()`. `stop()` is **retained as an alias** — it is public API of an exported class, and an embedder may well have learned to call it directly precisely because the kernel never did. No call site has to change, and no accept/reject behaviour of any contract moves. - -**Why it was invisible in production, and where the bill landed.** `start()` `unref()`s both timers, so a long-lived host process still exits and the leak is silent. Under vitest the worker process is alive throughout teardown, so a tick fires *after* a test file is over, reads a delivery table through a driver the suite already disconnected, and `SqlDriver`'s console fallback warns. `console.*` inside a vitest worker is an RPC to the main process (`onUserConsoleLog`); one issued after `rpcDone()` has snapshotted the pending set is rejected by `$rejectPendingCalls` as `EnvironmentTeardownError: [vitest-worker]: Closing rpc while "onUserConsoleLog" was pending`. Nothing awaits that promise, so it lands as an unhandled rejection and fails a run in which every test passed — twice measured on `examples/app-showcase` (334/334 and 337/337 green, exit 1, a merge-queue eviction each time). The width of the window is the duration of `rpcDone()`, which is why it only ever fired on a loaded queue runner and never on the PR-side run of the identical diff. - -Suites that boot a kernel with this plugin get quieter and finish cleaner as a result: over 48 loaded runs of the affected showcase file, console output emitted after the file's own `afterAll` went 3 → 0, and console RPC round-trips per run roughly halved (6574 → 3456 in aggregate). diff --git a/.changeset/meta-bind-theme-analytics-cube.md b/.changeset/meta-bind-theme-analytics-cube.md deleted file mode 100644 index e72bf5682f..0000000000 --- a/.changeset/meta-bind-theme-analytics-cube.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -fix(spec): `theme` / `analytics_cube` are validated at the `/meta` write door (#10194) - -The two doors #6245 left open, closed the same way. Both are declared, -authorable stack collections with real `.strict()` schemas — -`defineStack({ themes })` validates with `ThemeSchema`, -`defineStack({ analyticsCubes })` with `CubeSchema` — yet neither was bound in -`UNREGISTERED_KIND_SCHEMAS`, so `getMetadataTypeSchema()` answered `undefined` -and `saveMetaItem` took its documented "unregistered type → store without -validation" branch: a body the stack door strictly refuses was stored, -unvalidated and badged `success: true`, through the metadata door. For `theme` -that is the console's own styling surface — a malformed one failed at render -rather than at write, with nothing at the write point to say so. - -**FROM** `PUT /meta/theme/:name` / `PUT /meta/analytics_cube/:name` with any -JSON → `200 { success: true }`, stored unvalidated. -**TO** a malformed body → `422 INVALID_METADATA` with structured `issues[]`, -the same envelope every other kind already returned. A well-formed body is -accepted exactly as before. - -Each entry binds the **same schema its stack collection is validated against** -(`ThemeSchema` at `stack.zod.ts` `themes:`, `CubeSchema` at `analyticsCubes:`), -and that closing invariant is now pinned by identity for all five map entries. - -**No new capability surface.** Shape validation only: no `MetadataTypeSchema` -member, no `DEFAULT_METADATA_TYPE_REGISTRY` entry, so every authorization -verdict keeps taking the identical "no static entry ⇒ synthesised -`allowRuntimeCreate: true`" branch. The write *door* is unchanged; only the -422 is new. #2657's B/C decision on whether these should become kinds is -untouched and unprejudged. `rag_pipeline` is deliberately not bound — it has -no stack collection to take a schema from (#6242 row 2). - -Graded **minor**, following #6245's landed precedent for the identical change -(itself following #5271): a write that previously returned 200 can now return -422. Nothing well-formed changes behaviour, but a caller relying on the API -accepting malformed bodies will see the difference. - -**One schema change rides along per kind, and it is load-bearing.** -`Theme` and `Cube` now declare the ADR-0010 protection envelope (`_lock`, -`_lockReason`, `_lockSource`, `_lockDocsUrl`, `_packageId`, `_packageVersion`, -`_provenance`) — the sharing_rule precedent from #6245: both metadata load -paths call `applyProtection` on **every** type, and these shapes are -`.strict()`, so binding the door without the spread would have aimed the new -422 at the runtime's own stamp instead of at malformed author input. Additive -and internal-only — no authored field changes. diff --git a/.changeset/meta-org-scope-folded-not-raw.md b/.changeset/meta-org-scope-folded-not-raw.md deleted file mode 100644 index 7b04188578..0000000000 --- a/.changeset/meta-org-scope-folded-not-raw.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/rest": patch -"@objectstack/metadata-core": patch ---- - -**Fix:** the REST `/meta` doors now decide **organization scope on the folded type**, never on the raw URL spelling (#10340). - -Storage folds `/meta/:type` through `META_URL_TO_SINGULAR` — the complete spelling map — while the doors' scope predicate (`declaresOrgOverride`) tolerates only the manifest-collection spellings. For the two registry-derived spellings, `translations` and `email_templates`, the doors therefore read and wrote **env-wide** where the singular twin was org-scoped: an org-active author's `PUT /meta/translations/:name` landed an env-wide row their own org-scoped read then shadowed (persisted, receipted as live, served by nothing), and `GET` under one spelling answered a different partition than the other — one item, two namespaces, addressed by spelling (#4432 / #7894's defect one layer down). - -- All nine `/meta` org-scope call sites (list, single read, layers view, compound read, save, compound save, delete, publish, rollback) fold the segment through `canonicalMetaUrlType` **before** calling `organizationIdForMetaRead` / `organizationIdForMetaWrite`, exactly as `metadata-url-spelling.ts` mandates: folding happens at the boundary and only there. -- The `GET /meta/:type/:name/published` code-store fallback folds too — the smaller second site of the same class: it reads a registry keyed by canonical types, so a recognised plural of a code-published item answered 404 while the singular answered 200. -- **Deliberately unchanged:** `GET /meta/_drafts` still applies no fold (it filters by the draft row's *stored* type, which is canonical because the protocol folds on save), the request `type` handed to the protocol stays the raw segment (the protocol owns its own fold), and `declaresOrgOverride` does **not** absorb the URL map — a predicate below the boundary consuming the URL spelling contract is the repair #7894 forbids. `@objectstack/metadata-core` changes are documentation and pins only: the predicate's header no longer claims parity with the protocol's normalization (measured false), and new tests pin both the composed fold→predicate contract and the predicate's deliberate limit. - -No stored rows move: rows previously minted env-wide through a plural spelling stay env-wide and keep serving org-less callers (and org-active callers until an org overlay exists), which is the same layering the singular spelling always had. diff --git a/.changeset/meta-publish-route-package-binding.md b/.changeset/meta-publish-route-package-binding.md deleted file mode 100644 index a09580dca8..0000000000 --- a/.changeset/meta-publish-route-package-binding.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -**Additive:** `POST /meta/:type/:name/publish` now accepts `?package=`, so a single-item draft→active promotion can state the package it belongs to (#10063). - -#9612 taught the runtime publish gate to narrow `objects` to the written item's package closure, but only for callers that can NAME a package. Of the three write doors that reach the gate, `saveMetaItem` (`?package=` on the `PUT` door) and `publishPackageDrafts` (the batch names it) both did; the single-item promotion door named nothing — so every HTTP-driven promotion, which is exactly Studio's designer save→publish loop on every edit, handed the gate the whole tenant. The protocol half already existed and was waiting: `promoteDraftForPublish` declares `packageId?: string | null` and threads it into both the gate and `repo.promoteDraft`. Only the REST caller was mute. - -- **Wire spelling:** `?package=`, deliberately the same parameter name and the same normalisation the `PUT` door states it with — `all` and the empty value mean "env-local overlay, no package", not a package literally named `all`. One value, one spelling across both steps of the save→publish loop. -- **Multiplicity:** a repeated `?package=a&package=b` is refused `400 VALIDATION_ERROR` in the ADR-0112 nested envelope, via the shared `refuseRepeatedQueryParams` rule the sibling doors already carry; a single occurrence encoded as a one-element array is unwrapped and accepted. Previously the parameter was ignored outright on this route, so no caller relying on a documented behaviour changes. -- **Ordering:** the read sits AFTER the `manage_metadata` capability gate, so an uncapable caller still gets `403` rather than a `400` that would let it probe the shape of the surface. -- **Absent behaviour is unchanged, deliberately down to key presence.** The key is omitted from the `publishMetaItem` request when no package is stated, rather than passed as `undefined`. `promoteDraftForPublish` forwards to `repo.promoteDraft` on `'packageId' in request` — the KEY, not the value — because `null` there is a meaningful scope (pin the lookup to the unbound row) while an absent key means "match any package". A present-and-`undefined` key would therefore coerce to `null` downstream and stop package-bound drafts from being found, answering `no_draft` on a path this change was not supposed to touch. - -⚠️ **The acceptance criterion is that the narrowing is now REACHABLE from HTTP, not that publishing got faster.** Package-closure narrowing has a second, independent gate this change does not touch: `narrowObjectsToPackageClosure` keeps any object carrying no `_packageId` provenance, unconditionally, and a tenant-authored overlay corpus carries none. On such a corpus supplying the package still narrows nothing. On a provenance-stamped corpus the shipped deriver measures 421 objects → 45. Both gates must hold; this closes the caller-side one. diff --git a/.changeset/meta-state-route-singular.md b/.changeset/meta-state-route-singular.md deleted file mode 100644 index 24eaac429a..0000000000 --- a/.changeset/meta-state-route-singular.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@objectstack/client": minor -"@objectstack/rest": minor -"@objectstack/runtime": minor ---- - -The `/meta` FSM state route is singular: `meta.getLegalNextStates` moves, the plural registration is retired (#10077) - -Step 2 of the #9180 ruling — the `/meta` type segment is always singular, no -exception and no tolerated plural alias. Maintainer re-weigh, 2026-08-17, -verbatim: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」 - -- `client.meta.getLegalNextStates(object, field, from?)` now requests - `GET /api/v1/meta/object/:name/state/:field`. Same method, same arguments, - same response body — only the path segment changes. -- `GET /api/v1/meta/objects/:name/state/:field` is **no longer registered**. - The singular twin has been mounted alongside it since #7526, so the - migration for a hand-rolled HTTP caller is to drop the `s`. A request to the - retired spelling now gets the transport 404, which is the loud answer; the - one shape that changes hands rather than 404ing is a field literally named - `published`, which the compound `/:type/:section/:name/published` route - picks up. -- The two route ledgers follow what is mounted and what the SDK calls: the - plural row is deleted from `rest-route-ledger.ts` and the dispatcher ledger's - mirror row is respelled. - -**What this does not change.** The boundary fold `META_URL_TO_SINGULAR` is -untouched, so no `/meta/:type/...` spelling that is accepted today becomes -refused: the retired route matched a **literal** path segment and never -consulted the fold. The 2026-08-17 re-weigh (item 3) defers that break with no -scheduled window. The legacy dispatcher branch in `runtime/src/domains/meta.ts` -also still matches both literals; narrowing it is not part of this step. diff --git a/.changeset/metric-filters-retired.md b/.changeset/metric-filters-retired.md deleted file mode 100644 index ffda2ad085..0000000000 --- a/.changeset/metric-filters-retired.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): retire `MetricSchema.filters` — the per-metric raw-SQL filter nothing read (#10414, ADR-0049) - -**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep -launch-window convention ships it as `minor`; the migration prescription is -registered under protocol major 18, where `os migrate meta` users will look). - -`filters` on a cube metric (`filters: [{ sql: string }]`) was a real authoring -surface — `defineCube()` parses an author literal and -`defineStack({ analyticsCubes })` carries every cube through `StackSchema.parse` -— with ZERO consumers, measured with a positive control: no `.filters` read in -`service-analytics` or any driver's non-test code, while the neighbouring -`format` key IS read. `NativeSQLStrategy.resolveMeasureSql` and -`ObjectQLStrategy.resolveMeasureAggregation` both wrap the metric's `sql` in -the aggregate and never look at `filters` — so a hand-authored -`filters: [{ sql: "stage = 'closed_won'" }]` parsed, registered, and silently -returned the UNFILTERED aggregate under the author's metric name. That is the -#10298 dataset-measure failure for a hand-authored cube; the dataset half was -repaired through its own structured channel (#10411), which left this key inert -with the fix built around it. The raw-SQL fragment also ran against the -platform's structured-`FilterCondition` direction: it cannot be parameterized, -re-targeted per driver dialect, or walked by the lint filter rules -(`packages/lint/src/filter-walk.ts` deliberately never enumerated it). - -**What is refused:** `filters` on a metric. `MetricSchema` is `strictObject`, -so the key is deleted from the shape and the unknown-key rejection carries the -retirement prescription via the schema's `guidance` entry (fully-qualified key, -why it was inert, the replacement channels, the `os migrate meta` pointer). -The nested `strictObject` the key carried (closed by #4001 batch D) is gone -with it. - -**What stays accepted:** every other metric key (`name`, `label`, -`description`, `type`, `sql`, `format`) parses byte-identically. Filtering -that actually works is unchanged: the query's `where` (canonical Query DSL -`FilterCondition`), the condition folded into the metric's own `sql` -expression, or an ADR-0021 dataset measure's structured `filter`. - -The retirement kit: - -- strict deletion + `guidance` prescription at the schema - (`packages/spec/src/data/analytics.zod.ts`); the `AnalyticsQuerySchema` - `filters` guidance no longer points authors at the removed key -- ADR-0087 registration: retired-key entry `data/Metric:filters` and the D2 - conversion `metric-filters-removed` (protocol 18), wired into the step-18 - chain — `os migrate meta --from 17` strips the key from every metric in - `analyticsCubes[].measures` (pure lossless delete; it never had an effect to - lose) -- pin tests (`analytics.test.ts` — the old parse-survival pin flips to a - refusal pin asserting the prescription; `analytics-strictness-batchd.test.ts` - records the nested batch-D surface as superseded) -- generated baselines/docs follow the schema (`authorable-surface/`, - spec-changes, upgrade guide, reference docs) - -## FROM → TO - -```ts -// before — parsed green; both SQL strategies ignored it and the query -// returned the unfiltered aggregate -defineCube({ - name: 'orders', - sql: 'orders', - measures: { - closed_won_revenue: { - name: 'closed_won_revenue', label: 'Closed-Won Revenue', - type: 'sum', sql: 'amount', - filters: [{ sql: "stage = 'closed_won'" }], - }, - }, - dimensions: {}, -}); - -// after — delete the key; express the condition where something reads it: -// query time: { where: { stage: 'closed_won' } } -// in the metric: { type: 'sum', sql: "CASE WHEN stage = 'closed_won' THEN amount END" } -// dataset measure: a structured `filter` (ADR-0021, the #10411 channel) -``` - - diff --git a/.changeset/name-keyed-issue-paths.md b/.changeset/name-keyed-issue-paths.md deleted file mode 100644 index 0818e8cab3..0000000000 --- a/.changeset/name-keyed-issue-paths.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/lint": minor -"@objectstack/spec": patch ---- - -Runtime publish-gate findings for collection-resident write types (`object` / -`permission` / `book`) now key the top-level collection entry in -`issues[].path` / `advisories[].path` by NAME — -`objects.acme_invoice.sharingModel` — instead of by the gate's private -per-write snapshot index (`objects[417].sharingModel`), which no caller could -resolve: that index numbered an in-memory array a Studio / MCP / REST receiver -has never seen. Single-member write types keep their trivially-stable -positional form (`flows[0].nodes[1]…`), and nested positions inside one named -item (`objects.acme_invoice.indexes[1]`) stay positional — they index the -author's own document. An entry with no splice-safe name falls back to the -positional spelling. The accepted metadata set is unchanged; only the spelling -of the emitted finding `path` changes, and `RuntimeAuthoringIssueSchema.path`'s -description now states the convention. CLI (`os validate` / `os lint`) output -is unchanged — there the index resolves against the author's own config file. diff --git a/.changeset/nav-run-action-liveness-live.md b/.changeset/nav-run-action-liveness-live.md deleted file mode 100644 index 0a29c14a90..0000000000 --- a/.changeset/nav-run-action-liveness-live.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -**Liveness-ledger verdict:** `app.navigation[].runAction` moves `planned` → `live`, and drops its `authorWarn` (#10068). - -The declared deep-link slot (`ObjectNavItemSchema.runAction`, #4848/#7253) now has a real consumer in a shipped shell, so authoring it changes runtime behaviour. **What changes for authors:** setting `runAction` no longer raises the liveness advisory that told you the auto-run does not fire from this declaration yet. Nothing about the schema, the accept set, or the authoring-time validation changed — `defineStack`'s cross-reference walk and lint's `validate-action-name-refs` nav arm still reject a name that resolves to no defined action, exactly as before. - -The row carries **two** evidence pointers, not one, and the split is the point: - -- **`producer`** — objectui `packages/layout/src/NavigationRenderer.tsx`: defines `NAV_RUN_ACTION_PARAM` (the wire name's one definition) and applies `withRunAction` inside `resolveHref`'s object branch, on the **list landings only** — never the `recordId` branch. It *writes* the deep link and runs nothing. -- **`evidence`** — objectui `packages/app-shell/src/hooks/useNavRunAction.ts`: the single read-once/consume-once consumer, wired generically at `ObjectView.tsx` (every object list) and behind the entitlement gate at `EnvironmentListToolbar.tsx`. - -A renderer-only pointer would have said the slot is live because something *emits* it; what makes the key live is that a shell *consumes* it, and that lives in `app-shell`, not `layout`. Both were read at the `.objectui-sha` pin `9a3daf8`, which postdates the consumer's merge (objectui#5216 via objectui PR #5354). - -⚠️ **Recorded on the row: enforcement is not consumption.** The published `@objectstack/spec@17.0.0` does **not** enforce the `runAction` × `recordId` exclusivity — the `objectNavTargetExclusivity` refinement exists on `main` but is outside the GA build — and it accepts `runAction: ''`. That is the merged-but-unpublished window, not a defect. The consequence worth carrying: objectui's list-surface-only precedence and its empty-string-is-absent handling are **load-bearing rather than defensive**, because the pinned schema refuses neither input for it. Generalising: merged upstream ≠ published ≠ pinned downstream, and unlike a missing key, a missing **refinement fails silent** — the input is let through and the consumer proceeds. diff --git a/.changeset/objectql-dataset-level-filter.md b/.changeset/objectql-dataset-level-filter.md deleted file mode 100644 index d821259f61..0000000000 --- a/.changeset/objectql-dataset-level-filter.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/service-analytics": patch ---- - -Apply a dataset's definition-level `filter` on the ObjectQL analytics path -(#10413, phase 1). `/api/v1/analytics/query` served by a driver that reports -`objectqlAggregate` but not `nativeSql` (MongoDB, the memory driver) reached -`engine.aggregate` with no `filter` key at all: the dataset's own scope — a -`filter: { is_deleted: false }` on the dataset definition — was dropped, so -every measure aggregated the whole table while the dashboard door, on the same -cube and the same measure names, answered the scoped numbers. The scope is now -ANDed into the strategy's whole-call filter (never merged key-by-key, so a -caller's own `where` and the time windows cannot be overwritten by it), and the -representative SQL echo renders it too. - -Per-MEASURE `filter`s on this path are still not applied: an -`engine.aggregate` aggregation is `{ field, method, alias }` and cannot carry a -predicate of its own. Widening that contract is #10576; lowering the measure -filters into it is phase 2 of #10413. The native-SQL path already applies both -(#10298). diff --git a/.changeset/optional-error-sink-contract-requires-warn.md b/.changeset/optional-error-sink-contract-requires-warn.md deleted file mode 100644 index 82f1ca0477..0000000000 --- a/.changeset/optional-error-sink-contract-requires-warn.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/plugin-email": minor -"@objectstack/plugin-security": minor ---- - -`SweepLogger` and `ProjectionLogger` now declare `warn` as a REQUIRED channel, so a sink handed to the boot outbox sweep or to permission-set reconciliation can no longer be one that prints nothing (#9754) - -Both interfaces declared every member optional — `info?`, `warn?`, `error?` — which made `{ info }` a legal sink. Against such a sink both durability reports evaporated: each reaches for `error`, finds none, falls back to `warn`, and finds none of that either. For the sweep that is mail the platform accepted and never delivered, summarised to nobody; for reconciliation it is a permission set that will not survive a re-provision, with the `info` "reconciled" line skipped as well, so the sink heard neither the failure nor the reassurance. - -#9657 and #9748 repaired the call-site spellings. This is the other half, and the half that cannot regress: an optional `error` with no guaranteed alternative is a contract that permits silence, so an author reading the interface can write a report that never prints and be right about the type. Requiring `warn` makes that unrepresentable at the point of authoring rather than catchable one gate-run later. - -`error` deliberately stays optional on both types — hosts do inject reduced sinks, and requiring `error` would foreclose the `{ warn }`-only host the drivers were written for. - -If you pass a logger of your own and it declares no `warn`, add one; the kernel `Logger`, `ctx.logger` and `console` all satisfy the tightened shape unchanged. Consumers reach these types through `@objectstack/plugin-security`'s exported `ProjectionDeps`; `SweepLogger` is internal to `@objectstack/plugin-email`. - -The rule now has a checker of its own: `pnpm check:optional-error-sink` scans every sink type in `packages/**`, reports the population as a census on every run, and carries a shrink-only ledger of the 15 sinks that still permit silence. diff --git a/.changeset/optional-error-sink-paydown.md b/.changeset/optional-error-sink-paydown.md deleted file mode 100644 index c4fcdfbdd7..0000000000 --- a/.changeset/optional-error-sink-paydown.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -"@objectstack/cloud-connection": minor -"@objectstack/metadata-protocol": minor -"@objectstack/plugin-approvals": minor -"@objectstack/plugin-audit": minor -"@objectstack/plugin-auth": minor -"@objectstack/plugin-email": minor -"@objectstack/plugin-reports": minor -"@objectstack/plugin-sharing": minor -"@objectstack/plugin-webhooks": minor -"@objectstack/service-knowledge": minor ---- - -**BREAKING** (compile-time only): twelve logger sink types that declared an -optional `error` now declare a **non-optional** `warn`, so a durability report -always has somewhere to land (#9754, #10556). - -`minor`, not `major`: during the launch window this stack ships breaking changes -as `minor` — every publishable package versions in lockstep, so a `major` would -promote the whole release. `patch` would be wrong in the other direction, because -this *can* break a consumer's build. - -`error` stays optional on every one of these types — hosts legitimately inject -reduced sinks, and requiring `error` was measured and rejected as #9754 option C. -What changes is that its *absence* now has a declared, guaranteed destination. -Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the -type cannot reach, so **no runtime behaviour changes**: nothing that printed -before stops printing, and nothing silent starts printing. - -### Who has to change, and what to do - -Only a caller that hands one of these sinks an object with **no `warn` method** — -for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no -rename, no removal, and no stored value or metadata key to rewrite. Every -construction site inside this repo already supplied one, so the in-repo cost was -zero; the compile error is reserved for the callers that were silently discarding -these reports. - -The affected types, by package: - -- `@objectstack/cloud-connection` — the internal `PluginContext['logger']` -- `@objectstack/metadata-protocol` — `IndexMigrationLogger` -- `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` -- `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` -- `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal - `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` -- `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` -- `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` -- `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, - `rule-hooks` and `record-share-cascade` -- `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` -- `@objectstack/service-knowledge` — `KnowledgeLogger` - -`AuthManagerOptions['logger']` is the one most likely to be reached from outside: -`AuthManager` is public surface, its `logger` option stays optional, and a logger -that *is* supplied must now carry `warn`. The only non-test construction site in -this repo passes the kernel `Logger`, whose `warn` is already required. - -`ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger -field to `{}`. The field is now honestly optional rather than holding an empty -object that declared it could report and discarded everything. Behaviour is -unchanged in both directions. - - diff --git a/.changeset/page-source-styling-primitive-prose.md b/.changeset/page-source-styling-primitive-prose.md deleted file mode 100644 index 3a178a19fa..0000000000 --- a/.changeset/page-source-styling-primitive-prose.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Name the real per-tier styling primitive in `PageSchema`'s `kind` and `source` -descriptions, replacing the "JSX/HTML+Tailwind" framing that ADR-0080's 2026-06-30 -amendment retracted on styling. - -A page's `source` is runtime metadata, so the console's build-time Tailwind never -scans it — authored utility `className`s silently produce no CSS. The descriptions -now say what each tier actually styles with: `kind:'html'` via the registered -components' structured props plus a JSON `style` object with `hsl(var(--token))` -theme colors, `kind:'react'` via inline `style` with the same token colors, and -neither with Tailwind classes. - -Text-only correction, no schema shape or acceptance change — the accepted page set -is unchanged, and every other claim in the two descriptions survives verbatim -(parse-never-execute, the compiler package per tier, `source` authoritative over -`regions`, the ADR-0081 `OS_PAGE_REACT=off` gating). - -- `packages/spec/src/ui/page.zod.ts` — the `kind` and `source` `.describe()` - strings and the `source` TSDoc block, which regenerate - `content/docs/references/ui/page.mdx`. diff --git a/.changeset/pause-ends-retry-segment.md b/.changeset/pause-ends-retry-segment.md deleted file mode 100644 index 1486b04e12..0000000000 --- a/.changeset/pause-ends-retry-segment.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Document the retry/durable-pause boundary on a flow's `errorHandling` block: a durable -pause (`approval`, `screen`, `wait` — ADR-0019) **ends the retry-governed segment**. -`errorHandling.strategy: 'retry'` describes one synchronous dispatch, so a run that pauses -and later resumes gets exactly one attempt for anything that fails after the pause. - -Prose only — no validation change. The accepted flow set is unchanged and every flow that -parsed before parses identically; what changes is that the boundary is now stated where an -author meets it (the `errorHandling` and `strategy` `describe()` text, which is what the -generated reference tables render) instead of having to be inferred from engine behaviour. - -The boundary is deliberate rather than a gap: the retry knobs (`backoffMs`, -`backoffMultiplier`, `jitter`) model an in-process loop, which a pause of arbitrary -duration is not, and the durable continuation carries no attempt counter. To protect the -half of a flow that runs after a pause, give that half its own failure handling in the -flow — a `try_catch` node with its own `retry` around the post-resume work, or a `fault` -edge to a handler node. `content/docs/automation/flows.mdx` carries the recipe. diff --git a/.changeset/per-item-publish-rebind-and-draft-scope.md b/.changeset/per-item-publish-rebind-and-draft-scope.md deleted file mode 100644 index e9d886a4e9..0000000000 --- a/.changeset/per-item-publish-rebind-and-draft-scope.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch -"@objectstack/objectql": patch ---- - -Per-item publish (`POST /api/v1/meta/:type/:name/publish`) now re-binds runtime consumers -and finds drafts authored env-wide — the two things the package-scoped publish door -already did. - -**A metadata publish now announces `metadata:reloaded` on BOTH doors.** The event that -tells boot-cached consumers to re-read had two announcers: the dev-artifact watcher and -the runtime dispatcher after `POST /packages/:id/publish-drafts`. Publishing item by item -— what AI authoring and the item-level Studio doors do — announced nothing, so a flow -published while the server ran stayed `state='active'` and completely inert (no trigger -bound, no execution) until the kernel was rebuilt. `publishMetaItem` now notifies its host -through a new `onMetaItemPublished` seam and `ObjectQLPlugin` turns that into the kernel -announce, so `service-automation`'s flow re-bind, the authored hook/action re-sync, -declarative connectors and authored translations all catch up without a restart. The -announce is awaited, so the publish's own 2xx means the re-bind was attempted; a -subscriber failure is logged and never fails the publish. The batch door is unchanged — -it keeps its single per-publish announce rather than gaining one per promoted draft. - -**A per-item publish now resolves the draft's own org scope.** For the types the registry -declares `allowOrgOverride: true` (`view`, `dashboard`, `report`, `translation`, -`email_template`) the REST seam threads the session's active organization into the -publish, while package/AI authoring writes the draft env-wide — so the strict org lookup -matched nothing and answered `404 [no_draft] … nothing to publish` over a draft the -console's pending-changes banner was listing and the batch button published fine. The -per-item door now discovers the draft's scope the way `publishPackageDrafts` has since -#3115, with the ADR-0005 precedence (an org holding its own draft publishes that one) and -the same `NO_DRAFT` refusal when no scope holds a draft. diff --git a/.changeset/plugin-teardown-reaches-destroy.md b/.changeset/plugin-teardown-reaches-destroy.md deleted file mode 100644 index 8958c81545..0000000000 --- a/.changeset/plugin-teardown-reaches-destroy.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/plugin-reports": patch -"@objectstack/connector-openapi": patch -"@objectstack/connector-rest": patch -"@objectstack/connector-slack": patch -"@objectstack/plugin-approvals": patch -"@objectstack/service-knowledge": patch ---- - -Release these plugins' resources from `destroy()`, the teardown hook the kernel -actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and -`destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and -`LiteKernel.destroy()`, which walk the plugins in reverse calling -`plugin.destroy()`, walked straight past every plugin whose teardown was spelled -`stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still -armed, the REST/OpenAPI/Slack connectors still registered on the automation -engine, the approvals SLA escalation job still scheduled, and the knowledge -event-sync subscription still open. - -Each teardown body now lives in `destroy()`. `stop()` is retained as a -delegating alias with its parameter made optional, so an embedder that learned -to call it directly — precisely because the kernel never did — keeps working -unchanged. No export is removed and the `Plugin` interface is untouched. - -Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as -fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted -from the merge queue. diff --git a/.changeset/position-permissions-column-retired.md b/.changeset/position-permissions-column-retired.md deleted file mode 100644 index a11c7d7be5..0000000000 --- a/.changeset/position-permissions-column-retired.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -'@objectstack/plugin-security': minor -'@objectstack/spec': minor ---- - -fix(security): **BREAKING** — `sys_position` retires the `permissions` column (ADR-0049 enforce-or-remove, #9885) - -Maintainer ruling 2026-08-20: **REMOVE**. The column — a "JSON-serialized array -of permission strings" textarea — was declared on the platform position table -while **no producer ever wrote it and no runtime path ever read it**. The -object-scoped census (every `sys_position`-naming file, with same-object -positive controls resolving `active` / `delegatable` / `is_default` / `name` -to real readers) measured it at zero on both sides: the builtin and declared -position bootstrappers set `label` / `description` / `managed_by` / `active` / -`is_default` only, and position→grant resolution consults -`sys_position_permission_set` rows plus the position `name` — never this -column. Its only reference was the `clone_position` action copying it between -rows (a copy of a value nothing writes), removed in the same stroke. objectui -was searched under the same discipline: no console surface names the column. -A free-text grant catalogue on a security object that no runtime enforces -tells an author — human or AI — that direct position-level permission strings -are a platform capability; they are not. This is an **accept-set narrowing**: -the platform stops declaring, projecting and accepting the column. - -Migration (FROM → TO): - -| Wrote | Write instead | -|---|---| -| `permissions` on a `sys_position` seed row or data-door write | Delete the key. Capability reaches a position **only** through permission-set bindings (`sys_position_permission_set` rows, created in Setup or by an app's kernel:ready binder); prose that was documenting intent belongs in `description`. | - -One-line fix: delete `permissions` from any authored `sys_position` row. - - - -Enforcement after the removal is loud, not silent: the engine's schema -preflight refuses an undeclared field with `400 INVALID_FIELD` before the -driver or any hook runs, and `PositionSchema`'s strict parse now rejects a -declared-position `permissions` key with guidance naming the binding table. -Physical columns on already-deployed databases are untouched (ADR-0045 schema -sync is additive). If position-level direct grants ever become a real need, -the column is re-declared **with a runtime reader in the same PR** — -declare-and-enforce or don't declare. diff --git a/.changeset/publish-batch-closure-carries-pending-drafts.md b/.changeset/publish-batch-closure-carries-pending-drafts.md deleted file mode 100644 index fab2fbdd63..0000000000 --- a/.changeset/publish-batch-closure-carries-pending-drafts.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@objectstack/metadata-protocol': patch ---- - -fix: a package publishes as a self-consistent unit — `publishPackageDrafts` judges each draft against the batch's own pending declarations - -The batch publish door built the author-time validation context from -`engine.registry` alone, i.e. the ALREADY-LIVE universe. A draft is not in that -registry, and the batch's own promotions do not put it there either: the -registry write-through runs in Phase 2, after the Phase-1 transaction that gates -and promotes every draft. So while a batch was being judged, no member of it was -visible to any other member — in any order. - -Measured consequence: a package shipping `dataset/x` together with a `dashboard` -whose widget binds `x` could NEVER publish. `validateWidgetBindings` raises -`widget-dataset-unknown` at `severity: 'error'`, which refuses the promotion, -and the batch being all-or-nothing rolls the whole package back. Renaming the -dataset could not help, and neither could re-ordering the items. - -`publishPackageDrafts` now reads its own pending drafts once, before any -promotion, and folds them into all four context collections the closure carries -(`objects`, `permissions`, `books`, `datasets`) — pending declarations replace a -live one of the same name, never sit beside it. A binding that resolves to -neither the batch nor the live universe is still refused exactly as before. diff --git a/.changeset/publish-drafts-outcome-discriminant.md b/.changeset/publish-drafts-outcome-discriminant.md deleted file mode 100644 index 482250dbae..0000000000 --- a/.changeset/publish-drafts-outcome-discriminant.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/metadata-protocol": minor ---- - -Declare `outcome: 'published' | 'refused' | 'nothing_to_publish'` as a required -key on the `publishPackageDrafts` response (#10462) — the first-class -discriminant for WHICH exit answered, the fact `success` compresses into one -boolean. Before this field, a publish with nothing to promote and a genuine -refusal (pre-flight violation or ADR-0067 D2 rollback) were indistinguishable: -both answer `success: false` with `publishedCount: 0` on a 200, and the no-op -left no trace at all — an AI consumer graded the no-op as "refused and rolled -back" and burned two repair rounds on artifacts that were already correct -(cloud#1488; cloud#1492's patch discriminates on `failed.length > 0`, an -invariant the producer never stated). - -The producer invariants, now stated and pinned in the conformance suites, both -directions of each: `outcome === 'refused'` ⟺ `failed.length > 0`; -`outcome === 'nothing_to_publish'` ⟺ -`published.length === 0 && failed.length === 0`; -`success === (outcome === 'published')`. `success` keeps its exact pre-#10462 -value on every exit — a no-op still answers `success: false` — so consumers -reading only `success` see no change, and cloud#1492's `failed.length` -discrimination stays valid during its convergence onto `outcome`. The no-op -exit additionally logs one `info` line naming the package and both facts -(nothing pending, nothing refused), so that exit is no longer traceless. - -Additive for response consumers. A custom protocol implementation that serves -`publishPackageDrafts` must now emit `outcome` on every return — -`PublishPackageDraftsResponseSchema` declares it required, and the conformance -suites treat a producer return without it as a drifted seam. diff --git a/.changeset/rest-published-501-message.md b/.changeset/rest-published-501-message.md deleted file mode 100644 index 19608b3220..0000000000 --- a/.changeset/rest-published-501-message.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -Reworded the `501 NOT_IMPLEMENTED` message on `GET /meta/:type/:name/published` (and its -compound-name arity) to state its true post-#8278 condition. Since #8278 put the -runtime-published overlay consult ahead of this arm, the 501 no longer means "this kernel -cannot answer `/published`" — it means "nothing is runtime-published for this item, and -this kernel has no code/package store" (i.e. `metadata.getPublished()` is unavailable). -The old message ("metadata.getPublished() is not available in this kernel") overstated -that condition. Status code, `error.code`, and routing order are unchanged — only the -message text changed. diff --git a/.changeset/runtime-readme-unread-call-site-audit.md b/.changeset/runtime-readme-unread-call-site-audit.md deleted file mode 100644 index 73e8b53457..0000000000 --- a/.changeset/runtime-readme-unread-call-site-audit.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -"@objectstack/runtime": patch ---- - -Repair six false API claims in the published `@objectstack/runtime` README -(#10368). The README is in the package's `files` array, so it is the page npm -renders — a reader following it wrote code that could not compile. - -Found by hand-adjudicating every call site in that document that -`check:published-readme-exports` reports under `NOT read:` — receivers built -from free variables, parameters and globals, which neither the gate nor a human -reader can type by looking. 30 sites on 17 receivers were read; the repairs below -are what came out. - -- `engine.update('user', user.id, { name: 'Jane' })` → `engine.update('user', - { id: user.id, name: 'Jane' })`. `IDataEngine.update` is - `(objectName, data, options?)`; there is no `id` parameter. A by-id update is - identified by a truthy scalar `data.id` (or `options.where.id`) — the rule - `resolveEngineUpdateDispatch` in `@objectstack/metadata-core` defines. -- `engine.delete('user', user.id)` → `engine.delete('user', { where: { id: user.id } })`. - `IDataEngine.delete` is `(objectName, options?)`; the id belongs in - `options.where.id` (`assertEngineDeleteDispatch`). Passing it positionally - landed the id in the options bag. -- The **Interface Methods** bullet list restated both wrong signatures, so it is - corrected in the same edit — a repaired example beside a bullet list that still - contradicts it is not a repair. -- `reply.code(429).send({ retryAfterMs })` in the rate-limiting recipe → - `res.status(429).json({ retryAfterMs })`. `reply.code()` is Fastify; this - package's HTTP contract is `IHttpResponse`, which spells the step - `status(code)` and whose `send` takes `string | Uint8Array | ArrayBuffer`, not - an object. The `docs/HARDENING.md` recipe the same section links to already - answers 429 through the framework's own JSON responder. -- `status: res.statusCode` in the middleware example → dropped. - `IHttpResponse` has no `statusCode`; a response's status is observed through - `IHttpServer.afterResponse` (`HttpResponseObservation.status`), not read off - the response inside middleware. -- The `PluginContext` interface block declared `logger: Console` and - `getKernel?(): any`. The real contract (`@objectstack/core`) is - `logger: Logger` and a required `getKernel(): ObjectKernel`. - -Documentation only — no runtime, type or export change. diff --git a/.changeset/scaffold-summary-names-every-written-path.md b/.changeset/scaffold-summary-names-every-written-path.md deleted file mode 100644 index 46b180366d..0000000000 --- a/.changeset/scaffold-summary-names-every-written-path.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"create-objectstack": minor ---- - -`create-objectstack` now closes with a "Created files" summary derived from a -walk of the finished project directory, so it names everything the run wrote — -including the files written after the template copy (#10323). - -The old summary was the template copy's own list, printed before -` install` and before `npx skills add`. Measured against published -`create-objectstack@17.1.0` (`create-objectstack demo-app`, then a full walk of -the result): 12 entries printed, 18,045 paths on disk, **18,033 of them -unreachable from the summary** — `AGENTS.md`, `.github/copilot-instructions.md`, -`pnpm-lock.yaml`, `skills-lock.json`, `node_modules/`, and two ~968 KB trees of -agent instructions at `.agents/skills/` and `agent/skills/`. - -That mattered because the same run ends with the `skills` CLI printing *"Review -skills before use; they run with full agent permissions."* Advice to review -files the run never named, at paths it never showed, is advice a newcomer -cannot act on — the wrong failure direction for a security-flavoured warning. - -The list could not have been correct where it stood: two of the three write -phases belong to other processes, and the `skills` installer's destination set -moves with **its** releases, not ours. Reading the directory afterwards makes -the summary self-correcting instead. Large directories collapse to one line -carrying their path, entry count and size, so the bulk stays reviewable without -18,000 lines of output, and the paths the skills installer created are marked -`⚠ skills` with the permissions warning tied to them. - -Same run, after the change: 20 entries printed, **0 written paths unreachable**. diff --git a/.changeset/schema-free-meta-spelling.md b/.changeset/schema-free-meta-spelling.md deleted file mode 100644 index 742b44a503..0000000000 --- a/.changeset/schema-free-meta-spelling.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -Schema-free `/meta` spelling entry, and the package becomes tree-shakeable (#10096, #10031). - -- New fine-grained export `@objectstack/spec/meta-spelling`: the `/meta/:type` - URL-spelling contract — `META_URL_TO_SINGULAR`, `canonicalMetaUrlType`, - `metaUrlSpellingRefusal`, `unrecognisedMetaTypeRefusal` — importable for a few - hundred bytes instead of the schema graph the same symbols cost through - `/shared` (measured +246.9 KB minified / +69.7 KB gzipped marginal on a graph - already carrying `/ui` + `/kernel`). `/shared` keeps all four symbols - (re-exported from the one declaration); nothing moves or breaks. -- The map is now materialized at build time (`gen:meta-url-spelling`, gated by - `check:meta-url-spelling`). The module-load `assertMetaUrlSpellingsAgree()` - moved into that gate — same assertion, build-time enforcement home. -- `package.json` declares `sideEffects: false` (module-scope evaluation purity - measured per entry), and emitted bundles carry `/* @__PURE__ */` on deferred - schema construction, so consumer bundlers can drop schemas an entry never - reaches instead of retaining a subpath's whole module graph. -- Standing principle recorded in the package docs: a browser-reachable spec - export surface must be schema-free (maintainer ruling 2026-08-20, #10096). diff --git a/.changeset/security-metadata-outage-unresolved-cause.md b/.changeset/security-metadata-outage-unresolved-cause.md deleted file mode 100644 index 5244dce750..0000000000 --- a/.changeset/security-metadata-outage-unresolved-cause.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@objectstack/plugin-security": patch ---- - -Report a metadata-store OUTAGE as an outage, not as an absent declaration -(#10424). When an object's security posture cannot be resolved, the refusal -now consumes the `degraded` verdict `IMetadataService.getDiagnosed` was already -computing and discarding (#5840), so a store that could not answer no longer -wears the sentence written for an object that was never declared — "Check that -the object is declared and published on this runtime" sent operators to -re-check a healthy declaration in the middle of an incident. The refusal now -names the store, says the declaration may well be fine, and the operator log -line carries a grep-able `DEGRADED` / `metadata-store OUTAGE`. - -Explanation and logging only. The deny is unchanged in every case — same -`PermissionDeniedError`, same `PERMISSION_DENIED`, same 403, still fail-closed -per #3545 — and the set of requests that are accepted or rejected does not -move: the resolving read is untouched and `getDiagnosed` is consulted as a -separate best-effort probe on the path that is already refusing. A metadata -service that does not implement the optional `getDiagnosed` reports `unknown` -and keeps the previous wording; it is never reported as an outage. diff --git a/.changeset/seed-tenancy-repair-receipt.md b/.changeset/seed-tenancy-repair-receipt.md deleted file mode 100644 index d7fdfd4301..0000000000 --- a/.changeset/seed-tenancy-repair-receipt.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -"@objectstack/metadata-protocol": patch -"@objectstack/platform-objects": patch ---- - -fix(metadata-protocol): the seed/API tenancy repair now records each applied run in `sys_migration`, so "was my data rewritten, and when" survives the container being replaced (#9451) - -`backfillSeedTenancy` (#8686) is the platform's only row-rewriting repair that -runs unattended: it stamps `organization_id` onto business rows, merges one -autonumber counter and deletes another. It persisted nothing about having done -so. The only evidence was one `logger.info` line, and the healthy path is silent -by design — so once that line had scrolled, a silent boot and a boot that -rewrote data were indistinguishable. The operator most likely to need the record -(a fresh install, repaired during the first admin sign-up, where nobody is -reading server stdout) was the one least likely to have captured it. - -An `applied` run now writes one row into the **existing** `sys_migration` -deployment ledger — the face that already answers "has this deployment run this -data migration", and is already written at boot by the ADR-0104 attestation -path: - -```sql -SELECT last_run_at, advisory, details FROM sys_migration -WHERE id = 'seed-tenancy-backfill'; -``` - -`details` carries the run's status, the objects stamped, the organization -adopted and the identifiers that could not be adopted because they were already -minted on both sides of the split. - -Deliberately narrow: - -- **`applied` only.** `no-split` stays silent — a row per healthy boot would be - a ledger of non-events. -- **`verified_at: null`, `blocking: 0`, always.** This repair runs no self-check - and gates no consumer, so it claims no certificate; the collision count goes - to `advisory`, which never gates. Every reader of this ledger looks a row up - by `id`, so the new id cannot reach another migration's gate. -- **Best-effort, and loud when it fails.** A boot is never failed by - bookkeeping (2026-08-15 ruling), so a failed receipt write is reported at - `error` — naming that the rows *were* rewritten, that the repair is not - retried, and what to do — rather than rethrown. -- **No new schema, no new authoring surface, no new dependency.** The row is - written against the `@objectstack/spec/system` contract that - `metadata-protocol` already depends on. diff --git a/.changeset/serve-auth-base-url-loud.md b/.changeset/serve-auth-base-url-loud.md deleted file mode 100644 index 31b0345d3c..0000000000 --- a/.changeset/serve-auth-base-url-loud.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -**Bug fix (silent failure made loud):** `serve` now prints a boot-time diagnostic when the configured auth base URL cannot be parsed, instead of discarding the failure in an empty `catch` (#10202). - -The base URL was resolved through a `??` chain and parsed inside `try { new URL(baseUrl) } catch { /* ignore malformed baseUrl */ }`. That catch was the only place in the boot that learned the value was unusable, and it threw the knowledge away: the deployment's own origin never reached the `trustedOrigins` allow-list, boot continued normally, and the operator's first news of it was a browser-side `403 INVALID_ORIGIN` that names neither the variable nor the value. - -The shape that reaches it is ordinary env plumbing. `readEnvWithDeprecation` returns the preferred variable whenever it is `!== undefined`, so a **present-but-empty** variable resolves to `''` rather than `undefined`; `??` falls through only on `null`/`undefined`, so `OS_AUTH_URL=` on its own line in an env file (or a Helm/systemd/CI template rendering an absent key) consults neither `OS_BASE_URL` nor the `http://localhost:` default; and `new URL('')` throws. - -Measured on a real `os serve` boot with `NODE_ENV=production`, `OS_AUTH_URL=` set-but-empty and `OS_TRUSTED_ORIGINS` / `OS_ROOT_DOMAIN` / preview mode unset, probing `POST /api/v1/auth/sign-in/email` so a trusted origin answers `401 INVALID_EMAIL_OR_PASSWORD` and an untrusted one `403 INVALID_ORIGIN`: - -| Origin | `OS_AUTH_URL=` (empty) | `OS_AUTH_URL=https://app.example.com` | unset | -| --- | --- | --- | --- | -| `https://app.example.com` | 403 | **401** | 403 | -| `http://localhost:` | **401** | 403 | **401** | -| `http://tenant.localhost:` | **401** | 403 | 403 | -| `/api/v1/health`, `/api/v1/ready` | 200 | 200 | 200 | - -Two corrections to how this was expected to behave, both from that table. The allow-list does **not** come out empty: `serve` passes `trustedOrigins.length ? trustedOrigins : undefined`, and `AuthManager` substitutes a localhost wildcard trio for an absent list — so better-auth receives a non-empty list and localhost origins are trusted. Which makes set-but-empty strictly **more permissive than unset**: `http://tenant.localhost:` is trusted in the empty case and refused in the unset case, so an env template that renders an absent key to the empty string silently widens a production CSRF allow-list. - -**What changed is only what is said, never what is resolved.** The precedence chain, its order, and the `${protocol}//${host}` origin spelling are byte-for-byte identical; a set-but-empty `OS_AUTH_URL` still stops the chain exactly as before. Treating empty as unset inside the shared `readEnvWithDeprecation` would change behaviour for every caller of that helper and remains a separate, deliberate decision. The diagnostic is a warning, not a refusal to boot: a deployment running set-but-empty today keeps starting, and now says why authentication will not work. - -The resolution is exported as a seam — `resolveAuthBaseUrl()` and `formatUnusableAuthBaseUrlDiagnostic()`, alongside this file's sibling helpers — so the behaviour is reachable from tests without booting a server. diff --git a/.changeset/settings-write-before-engine-bind.md b/.changeset/settings-write-before-engine-bind.md deleted file mode 100644 index aca2a1c876..0000000000 --- a/.changeset/settings-write-before-engine-bind.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/service-settings": patch -"@objectstack/spec": patch ---- - -**Behaviour change (tightening, boot-time only):** a settings write issued before `SettingsService`'s data engine is bound is now **refused loudly** instead of resolving successfully while nothing reaches `sys_setting` (#10159). - -`upsertRow` picks its store on `if (this.engine)`, and the engine is bound in exactly one place — `SettingsServicePlugin` registers a `kernel:ready` hook from its `start()` and calls `bindEngine` inside it. `kernel:ready` handlers run in registration order and every plugin's `init()` runs before any plugin's `start()`, so **every `kernel:ready` hook registered from an `init()` fires inside that window**. A `set()` from there landed in the in-process memory fallback, re-resolved off that same array, and handed the caller a fully resolved value; `sys_setting` received nothing, and neither audit ledger recorded anything (both sinks bind on the same `bindEngine` call). Nothing was logged at any level, because the write did not fail — it succeeded against the wrong store. - -**What an operator will now observe.** A write in that window throws `SettingsEngineNotBoundError` — code `SETTINGS_ENGINE_NOT_BOUND`, status **503** — whose message names the window, the reason, and the fix: move the write to `kernel:bootstrapped` (or later), which fires strictly after every `kernel:ready` handler has settled. Previously that same call returned a resolved value and the setting was silently absent after restart. - -**Nothing outside the window changes.** The refusal is armed only by the new opt-in `SettingsServiceOptions.engineBindPending`, which `SettingsServicePlugin` sets in `init()` and clears on both branches of its `kernel:ready` hook — by `bindEngine` when `objectql` is present, or by the new `SettingsService.settleWithoutEngine()` when it is not. So: - -- a `SettingsService` constructed directly (unit tests, bootstrap, control-plane mock) keeps the in-memory fallback exactly as before — it declares no pending bind, and the guard never arms; -- a lean kernel with no `objectql` keeps the plugin's deliberate degradation: once its `kernel:ready` hook has established that no engine is coming, writes resolve into the memory fallback again (now with a `warn` saying those values are lost on restart); -- reads are untouched in every state, so an ordinary boot-time read of a setting still resolves. - -No shipped caller wrote settings inside the window, so no existing startup sequence becomes an error. - -`SETTINGS_ENGINE_NOT_BOUND` is registered in `ERROR_CODE_LEDGER` per ADR-0112. The status is declared on the error class rather than at an HTTP door because no door can reach it: the window closes at `kernel:ready`, and HTTP servers open their socket at `kernel:listening`, strictly after. diff --git a/.changeset/sharing-rule-criteria-org-scope.md b/.changeset/sharing-rule-criteria-org-scope.md deleted file mode 100644 index 19d54f23a8..0000000000 --- a/.changeset/sharing-rule-criteria-org-scope.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/plugin-sharing": patch ---- - -**Behaviour change (narrowing):** an **org-stamped** sharing rule's criteria sweep is now scoped to that rule's own organization, where it previously swept **every** organization's records (#10119). - -`SharingRuleService.findMatchingRecords` (the whole-rule evaluation pass) and `recordMatches` (the per-record write-hook pass) ran the rule's criteria query under a bare system context carrying no tenant, for every rule. The recipient half was already org-aware — `expandRecipient` threads `rule.organization_id` into the team / business-unit / position graph services — so a rule stamped with an `organization_id` expanded recipients inside its own organization and then matched records belonging to all the others. `reconcile` materialized the cross product: `sys_record_share` rows granting one organization's users access to another organization's records. - -Measured on `main` before the change, through a real `ObjectQL` on a real `SqlDriver`: an `org_a`-stamped rule matched **the same four records as a platform-global rule** (`deal_a1`, `deal_b1`, `deal_b2`, `deal_p1`) and materialized a grant on each; the per-record hook pass minted a grant on `org_b`'s record with `grantsCreated: 1`. - -What changes, and for whom: - -- **Org-stamped rules** (`organization_id` non-null — what any org admin mints through `defineRule`) now run their criteria query with `tenantId` set to the rule's organization. The platform's existing chokepoint does the rest: `ObjectQLEngine.buildDriverOptions` threads it to `DriverOptions.tenantId` and `SqlDriver.applyTenantScope` emits `(organization_id = ? OR organization_id IS NULL)`. So such a rule matches its own organization's records **plus** platform-owned null-org records, and no other tenant's. `SharingRuleEvaluationResult.matchedRecords` falls accordingly, and the next reconcile pass **revokes** the cross-org `sys_record_share` rows it previously created, through the existing revoke-the-remainder branch — no migration is needed. -- **Platform-global rules** (`organization_id = null`) are unchanged: they keep the full unscoped sweep, which is their declared behaviour (documented at the `deleteRule` platform-authority guard). Both directions are pinned. -- **No public contract changes.** No schema, route, error code or accept/reject set moves; the system elevation on the criteria read is retained (the evaluator still sees rows no individual recipient could), only the tenant axis is added. - -The cross-org rows this stops creating were **inert** under a walled posture — the Layer-0 tenant wall AND-composes over sharing's Layer-1 widening, so such a grant could not open a read across the wall. The costs were `sys_record_share` bloat (every org-stamped rule scanning the whole table at `limit: 5000`) and a population that is wrong at rest, which any consumer reading `sys_record_share` directly, or any future softening of the wall, would inherit. diff --git a/.changeset/showcase-react-page-adapter-query-contract.md b/.changeset/showcase-react-page-adapter-query-contract.md deleted file mode 100644 index 79288c37e5..0000000000 --- a/.changeset/showcase-react-page-adapter-query-contract.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/example-showcase": patch ---- - -Fix the showcase react pages' `useAdapter()` query contract, and pin it (#10288) - -`renewals-pipeline` passed `top: 500` and `crm-workbench` passed `limit: 200` to -`adapter.find`. Neither is a query option: `QueryParams` declares only `$`-prefixed keys -and `ObjectStackAdapter.convertQueryParams` copies exactly those, so the key reached no -branch and was dropped with no error. The consequence is the opposite of a truncated -read — the GET list route has **no default page size**, so an absent `top` returns the -ENTIRE match set, and the cap the author wrote never happened. - -The same effect then read its rows off `.records`. `find()` resolves to a normalized -`QueryResult` (`data` + `total`), never the REST envelope, so `pr.records` was -`undefined` on every call and the renewals KPI strip sat at `0 / 0 / 0` while the -`` beside it showed the same rows correctly. Measured on a 640-row account with -the real page source driven against a contract-faithful adapter double: before, -`$top` arrives `undefined` and the strip reads `{projects: 0, invoices: 0, openInvoices: 0}`; -after, the cap is applied and it reads `{projects: 640, invoices: 640, openInvoices: 100, -capped: true}`. - -Applying the cap is only half a fix, because `data.length` under a `$top` is exactly the -silently-capped count the card was filed about — so both pages now count the envelope's -`total` (the server's real count over the same `$filter` whenever a limit was applied). -The one number a cap genuinely bounds, "Open AR", is a per-row verdict over the fetched -window; it renders as `100+` rather than passing for a total. - -`test/react-page-adapter-query-contract.test.ts` executes the page's real rollup effect -and then sweeps every `kind:'react'` page in the app for both contracts, with an -extraction control, a census control, and a positive control on the scanners. diff --git a/.changeset/skill-tools-docblock-adr-0109.md b/.changeset/skill-tools-docblock-adr-0109.md deleted file mode 100644 index c733525762..0000000000 --- a/.changeset/skill-tools-docblock-adr-0109.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -**Docs:** the `skill.tools[]` docblock now states ADR-0109's authoring model instead of its rejected alternative (#10356). - -`SkillSchema.tools`' docblock told authors that "Tools should also be registered as first-class metadata (type: 'tool') unless they are dynamically materialised at runtime" — the shape ADR-0109 explicitly **rejected** ("a required tool record per exposed action": a second authoring step, a second namespace to keep consistent, and a second surface for AI authors to hallucinate into, for zero added capability). It also inverted the exemption, treating the materialised path as the exception when ADR-0109 makes it — together with the platform registry — the rule. The sibling docblock over `stack.zod.ts`'s `tools` already said the opposite, so the package shipped two contradictory answers to the same question. - -The text now mirrors the resolution universe `@objectstack/lint`'s `validate-ai-tool-references` actually implements: a `tool` record is never required and the default third-party path declares none; a `skill.tools[]` name resolves against the stack's own `stack.tools[]` names, `PLATFORM_PROVIDED_TOOL_NAMES`, and the `action_` family the runtime materialises from AI-exposed declarative actions (`ai.exposed` + `ai.description` on a headless action type, per ADR-0011). It also records that `stack.tools` is the optional Phase-2 AI-presentation refinement layer with no runtime reader until that phase lands — so a record authored today is inert, which the old sentence recommended authoring without saying. - -Prose only: no schema shape, no `.describe()` text, no runtime behaviour and no authorable-surface change (`check:authorable-surface` and the whole `check:generated` set are unmoved by this diff). It is graded rather than skipped because the text ships to consumers: `@objectstack/spec`'s `files` list publishes `src/**/*.zod.ts`, so this docblock travels in the npm tarball as source. It does **not** reach `dist/*.d.ts` — property-level comments inside the `z.object({ … })` literal are dropped from the emitted declarations, which is measurable in the built chunk (`tools: z.ZodArray;`, no comment). Published source is the surface that matters here anyway: this is the docblock an AI author reads while writing `skill.tools[]`, the exact surface ADR-0109 was written to keep clean. diff --git a/.changeset/sort-axis-provenance-warning.md b/.changeset/sort-axis-provenance-warning.md deleted file mode 100644 index f1ffefcdf8..0000000000 --- a/.changeset/sort-axis-provenance-warning.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -The SORT axis now asks the #8116 provenance question about a name the blanket -`SYSTEM_FIELDS` union told it not to flag — new rule `sort-field-unprovisioned` -(#10474), the twin of `searchable-field-unprovisioned` on the identical index -(#8404). - -`validate-sortable-fields` consulted the union and stopped there, so a list view -ordering by a registry-injected anchor on an ADR-0015 `external` object was -skipped in silence. The #8999 consumer census recorded that gap with the reason -that such an object never reaches the union branch at all — skip (2) was believed -to catch it. **That reason was measured wrong.** `declaredFieldTarget` returns -`null` on exactly one condition (`fields` missing, unreadable, or naming -nothing) and nothing in it tests `external`, so the shipped shape — a federated -object that declares a mapped field map, as `examples/app-showcase`'s -`showcase_ext_customer` does — is indexed like any other object and lands -squarely in the skip. The census ledger entry now carries the correction rather -than the inherited reason. - -Why the authoring gate is the only door available for it: both runtime doors on -this axis judge `formula` alone (`UNMATERIALIZED_SORT_TYPES`) — the REST ingress -`assertSortFieldsExist` (#6994) and the engine's `assertOrderByIsMaterializable` -(#7095). An injected anchor is a `datetime` or `lookup`, it *is* in `gate.known` -because the registry injected it into the served schema, and it is undotted, so -it clears every verdict and reaches the driver. Measured with a real `SqlDriver` -over better-sqlite3, the object declared exactly as the showcase declares it, -against a remote `customers` table carrying `[id, name, email, region, -lifetime_value]` and none of the seven injected anchors: - -``` -orderBy name asc -> [c1,c2,c3] desc -> [c3,c2,c1] (a real column: reverses) -orderBy created_at asc -> [c1,c2,c3] desc -> [c1,c2,c3] asc === desc, 3 rows, no error -orderBy owner_id asc -> [c1,c2,c3] desc -> [c1,c2,c3] asc === desc, 3 rows, no error -``` - -`asc` and `desc` byte-identical while the baseline reverses is what makes it a -dropped sort rather than a coincidence — the same signature this rule already -records for `formula`, reached by a second route, except that a formula sort is -refused at both doors and this one is not. A list view ordered by an anchor with -no storage answers `200` with the rows in the driver's arbitrary order, on the -view's first fetch and every fetch after it, which `limit`/`offset` then slice -into an arbitrary page. - -`warning`, never `error` and never gating (#4330's cost asymmetry, the call every -sibling makes): the remote schema is invisible to this pass, so the remote table -may genuinely carry a `created_at` of its own. Declaring that column — the first -remedy the shared hint prescribes — silences the finding, because -`unprovisionedInjectedColumnsFor` excludes an author-declared column of the same -name (#7859's security direction). The runtime publish gate sorts on severity, so -this lands as an advisory and refuses no write. - -Two deliberate narrowings, both pinned: - -- **Undotted names only** — the one place this axis departs from the SEARCH twin. - `resolveSearchFields` matches by exact string and drops a dotted entry like a - typo, but a dotted SORT name is refused by the ingress gate as its own verdict - (`400 INVALID_SORT`, loudly, on every fetch), so the silent degradation this - finding reports cannot happen there. Answering would give the SORT axis its own - dotted verdict, which is exactly the posture the rule shares with the FILTER - and PROJECTION axes (#4256 / #7532 / #7589) and declines to break. -- **`checkSortDeclaration`'s new anchor-index parameter is optional**, with the - same meaning `checkSearchableFieldList`'s carries: an out-of-repo caller that - never built the index keeps its pre-#10474 answers. Every in-repo caller passes - it. - -Also re-ruled, with fresh eyes and on evidence rather than inheritance: -`validate-translation-references` still correctly asks nothing. It reads the -union at exactly one site (the `fields.` orphan test), and the key it -decides about is derived from the *registered* metadata, into which the registry -injects the anchor on a federated object just as on a local one — so the key -resolves and the label renders. Warning there would flag a translation that -works. The blank-column consequence belongs to the surface that renders the -anchor (`validate-page-field-bindings`, #8340), not to the bundle that names it. diff --git a/.changeset/sso-register-platform-admin-only.md b/.changeset/sso-register-platform-admin-only.md deleted file mode 100644 index f34bf8b56d..0000000000 --- a/.changeset/sso-register-platform-admin-only.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/plugin-auth": patch ---- - -**Behaviour change (tightening):** registering an SSO identity provider through the direct `POST /api/v1/auth/sso/register` endpoint now requires a **platform admin**. An organization **owner or admin** who is not a platform admin can no longer register an identity provider on any surface (#10009). - -Who loses access: an org owner/admin (a `sys_member` row graded owner/admin) with no org-less `admin_full_access` grant. They previously passed the ADR-0024 before-hook on the direct endpoint and now receive `403 SSO_REGISTER_FORBIDDEN`. Platform admins — an org-less `sys_user_permission_set` link to `admin_full_access`, per ADR-0068 D2 — are unaffected, as are anonymous callers, who still fall through to better-auth's `sessionMiddleware` (`401`). - -This closes a posture divergence: the four `/admin/sso/*` bridges the `sys_sso_provider` metadata actions call have gated on the platform-admin judge since #9653, while better-auth's own endpoint kept the wider ADR-0024 admit set — so the same principal was refused at one door and admitted at the other for the same underlying registration, leaving the bridge tightening as labelling rather than a boundary. Per the 2026-08-20 maintainer ruling, ADR-0068 D4 governs: registering an identity provider is a platform-operator action. If org-scoped IdP self-serve is ever wanted, it is a deliberate future decision rather than a vendor default inherited by omission. - -The direct endpoint also gains its first test pins; the now-callerless `isOrgOrPlatformAdmin` predicate was removed rather than left dead. diff --git a/.changeset/stack-themes-carrier-retired.md b/.changeset/stack-themes-carrier-retired.md deleted file mode 100644 index 9d715ee2b1..0000000000 --- a/.changeset/stack-themes-carrier-retired.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): retire the `themes` carrier key and `ThemeSchema` — the authoring surface nothing ever applied (#10485, ADR-0049) - -**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep -launch-window convention ships it as `minor`; the migration prescription is -registered under protocol major 18, where `os migrate meta` users will look). -Maintainer ruling 2026-08-21, recorded verbatim on #10485: 「B:退役授权面 — -收掉 `themes` 载体键与 schema,`app.branding` 留作唯一颜色面;objectui 引擎代码 -与单测保留。」 - -`defineStack({ themes })` was a real authoring surface — parsed strictly at the -authoring gate, ingested and stored by artifact ingest -(`ARTIFACT_FIELD_TO_TYPE`) — with ZERO consumers past that point, measured: -no non-test read of `.themes` or of stored `theme` items anywhere in -core/runtime/rest/services/plugins; `theme` never in `MetadataTypeSchema`, -`DEFAULT_METADATA_TYPE_REGISTRY` or `BUILTIN_METADATA_TYPE_SCHEMAS`; the only -mounted `ThemeProvider` is the app-shell chrome light/dark toggle (unrelated to -`ThemeSchema`); and no stack- or app-level key ever selected an active theme. -An author who wrote a theme shipped it through every green gate and the console -looked exactly the same. - -**What is refused:** the top-level `themes:` key. `ObjectStackDefinitionSchema` -is a `strictObject`, so the key is deleted from the shape and the unknown-key -rejection carries the retirement prescription via the schema's `guidance` entry -(removal citation, why it was inert, and the `app.branding` replacement). -`ThemeSchema`, `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, -`ShadowSchema`, `ThemeModeSchema`, `defineTheme` and the `Theme` / -`ThemeParsed` / `ColorPalette` / `Typography` / `BorderRadius` / `Shadow` / -`ThemeMode` types are removed from `@objectstack/spec` / `@objectstack/spec/ui` -(orphaned value schemas leave with their one consumer, #3950). `PUT -/api/v1/meta/theme/:name` now gets the #8421 unrecognised-type refusal — the -`themes: 'theme'` fold left `PLURAL_TO_SINGULAR` and with it the generated -URL-spelling contract — instead of the pre-#10194 store-anything branch. - -**What stays:** `app.branding.primaryColor` / `accentColor` — the one live -colour surface (objectui's `AppShell` reads it and derives `--primary`, -`--accent` and friends) — plus objectui's `ThemeEngine` / `ThemeContext` engine -code and their unit tests, explicitly retained by the ruling. Legacy stored -`theme` rows are untouched: reads still answer, DELETE still works, and -`applyConversionsToStoredItem` passes them through unchanged. - -The retirement kit: - -- strict deletion + `guidance` prescription at the stack schema - (`packages/spec/src/stack.zod.ts`); `packages/spec/src/ui/theme.zod.ts` - deleted whole -- ADR-0087 registration: retired-def entries `ui/Theme`, `ui/ThemeMode`, - `ui/ColorPalette`, `ui/Typography`, `ui/BorderRadius`, `ui/Shadow` and the - D3 **semantic** entry `stack-themes-carrier-retired` (protocol 18). Semantic - rather than a D2 conversion on the lossless-only scope guard: a stack may - declare N themes and M apps, so which palette entry becomes which app's - `branding.primaryColor` is a judgment the transform cannot make — the entry - prescribes the hand move instead of auto-deleting authored content -- ingest mapping removed (`packages/metadata/src/plugin.ts`), CLI stats row - removed, showcase example re-based on app branding -- pin tests: `stack-top-level-strict.test.ts` (refusal carries `#10485` + - `app.branding` + no rename suggestion; replacement parses green; no theme - export survives on `./ui`) and `protocol.unrecognised-meta-type.test.ts` - (`/meta/theme` refused with the ADR-0112 envelope, nothing stored) -- generated baselines/docs follow the schema (`authorable-surface/`, - `json-schema.manifest/`, api-surface, export-origins, meta-url-spelling, - spec-changes, upgrade guide, reference docs, skill references) - -## FROM → TO - -```ts -// before — parsed green, stored by artifact ingest, applied by NOTHING: -defineStack({ - themes: [{ name: 'corporate', label: 'Corporate', mode: 'light', - colors: { primary: '#7C3AED' } }], -}); - -// after — delete the key; colour the console where something reads it: -defineApp({ - name: 'my_app', - label: 'My App', - branding: { primaryColor: '#7C3AED', accentColor: '#06B6D4' }, -}); -// a custom CSS variable your own stylesheet consumed has no spec slot any -// more — move it into your own CSS. -``` - - diff --git a/.changeset/stdio-mcp-execution-context-converge.md b/.changeset/stdio-mcp-execution-context-converge.md deleted file mode 100644 index 61bdd57a97..0000000000 --- a/.changeset/stdio-mcp-execution-context-converge.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -"@objectstack/mcp": minor ---- - -fix(mcp): the stdio MCP transport assembles its ExecutionContext with the shared assembler, and resolves localization (#7279) - -`resolveStdioExecutionContext` was the last hand-written `ExecutionContext` -assembly on the platform. #6216 converged the dispatcher, REST and share-link -sites onto `assembleExecutionContext`; this face was not in that card's -inventory, so it kept building the envelope field-by-field — and fell behind it -in two ways. - -| field | before | after | -|---|---|---| -| `tabPermissions` | dropped | **carried** | -| `timezone` / `locale` / `currency` | **resolved not at all** | **carried** (workspace values) | -| `accessToken` | absent by omission | **withheld by decision, on the record** | -| `positions` / `permissions` / `systemPermissions` / `userId` / `tenantId` / `email` / `posture` / `org_user_ids` / `accessible_org_ids` | carried | carried, unchanged | - -## ⚠️ This changes output on the stdio surface — it is NOT a no-op - -**Formula fields evaluated during a stdio call move from `UTC` to the -workspace timezone.** The read path threads `ExecutionContext.timezone` into -`ExpressionEngine.evaluate`, which defaults to `UTC` when the context carries -none (`cel-engine.ts`: `ctx.timezone ?? 'UTC'`). Every stdio call previously -carried none. **A date-bucketing formula can therefore return a different -calendar day than it did before this change** — for a workspace whose timezone -is not UTC, that is the point: the same record read over REST and over stdio -now agree, where before they could disagree by a day. - -Two smaller shifts ride along: - -- **Denial messages localize.** A read refused by CRUD/FLS or RLS renders in the - workspace language (`userFacingDenialMessage`, `opCtx.context?.locale`) instead - of English. -- **Date-dependent driver generation on the write doors** (autonumber - `{YYYYMMDD}` tokens) resolves its calendar day from the workspace timezone. - `buildDriverOptions`' `hasTz` gate (`execCtx?.timezone !== undefined`) is one - of the few places where a field's ABSENCE is a meaningful state, and a stdio - call crosses it for the first time. Pinned in both directions by - `packages/objectql/src/engine-timezone-presence-gate.test.ts`. - -If a deployment's workspace timezone is unset, `resolveLocalizationContext` -falls back to `UTC` / `en-US` — the values this face effectively used before — -and nothing changes for it. - -## `accessToken` is withheld, deliberately, and now says so - -The stdio face's credential is a **long-lived `osk_` API key** read from -`OS_MCP_STDIO_API_KEY`, not a session bearer. `ExecutionContext.accessToken` is -a **published hook surface** (`session.accessToken`, `spec/data/hook.zod.ts`), -so handing every `beforeFind`/`afterFind` a credential with far longer life than -the session token that surface was designed around is a product decision nobody -has made. This face passes `accessToken: undefined` with the reason written -down, matching the REST precedent. (It is also unreachable here: the value is -assigned only inside `resolve-authz-context.ts`'s -`if (!userId && typeof input.getSession === 'function')` branch, and this call -passes no `getSession`. The test injects a sentinel token at the seam anyway, so -the *decision* is pinned rather than the accident.) - -## Cost, and where it is paid - -`resolveStdioExecutionContext` still re-resolves the **identity** on every call, -deliberately — ADR-0101 D1, so a revoked key stops working on the next one. -Localization is resolved **once, in `start()`**, and reused: the key's tenant -cannot change mid-session, and up to three settings reads per MCP call on a -long-lived process is not acceptable steady state. diff --git a/.changeset/strict-blueprint-namefield.md b/.changeset/strict-blueprint-namefield.md deleted file mode 100644 index ddac34d0e8..0000000000 --- a/.changeset/strict-blueprint-namefield.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -Add `nameField` to the solution-blueprint strict mirror's object schema (required-but-nullable, matching the strict convention), so the design-stage structured output can author the ADR-0079 record-title choice instead of always deferring to the platform auto-pick. The key-parity pin between the strict mirror and the lenient schema is widened from the field schemas to the object schemas, so the next object-level divergence fails a test. diff --git a/.changeset/sys-session-ttl-spare-tombstones.md b/.changeset/sys-session-ttl-spare-tombstones.md deleted file mode 100644 index 5b38dad571..0000000000 --- a/.changeset/sys-session-ttl-spare-tombstones.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@objectstack/platform-objects": minor ---- - -Declare an ADR-0057 lifecycle policy on `sys_session` (#7826): the object is -now `class: 'transient'` with -`ttl: { field: 'expires_at', expireAfter: '1d', onlyWhen: { revoked_at: { $null: true } } }`. - -**Ordinary expired sessions are now reaped** by the LifecycleService Reaper one -day after `expires_at` passes — the same window `sys_device_code` uses. Until -now nothing swept this table: better-auth's only expiry-driven collector fires -inside `GET /get-session`, so it can never reach a row whose cookie is never -presented again, and an abandoned session was effectively immortal. - -**Revoked tombstones are deliberately spared.** The `onlyWhen` filter (#10165) -is load-bearing, not defensive: the #7732 revocation write backdates -`expires_at` to `now - 1000` and clears nothing, so an ADR-0069 D4 audit -tombstone looks *maximally* expired — a TTL on `expires_at` without the filter -would reap the audit trail first and hardest. - -Deliberate, known consequence: because tombstones are spared entirely, -`sys_session` still grows without bound on the revoked arm. How long a -revoked-session tombstone should be retained is compliance / audit-trail -policy and is not settled here. diff --git a/.changeset/team-approver-org-screen.md b/.changeset/team-approver-org-screen.md deleted file mode 100644 index 0f61ff3455..0000000000 --- a/.changeset/team-approver-org-screen.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@objectstack/plugin-approvals": patch ---- - -**Who loses access:** members of a team belonging to a *different* organization -than the record being approved. Concretely — a request raised in `org_a` routed -to a `team` approver whose `sys_team.organization_id` is `org_b` used to place -every `sys_team_member` of that team into `pending_approvers`, giving them the -approve/reject buttons on a record they are not a tenant of. They no longer -enter the slate, and the step falls back to the dead `team:` literal with -the existing `#3807` "expanded to nobody" warning — the same shape a cross-org -`position` approver has always produced (#10230). - -`team` was the last approver expansion that resolved people without asking -which organization was asking; `department`, `position`, `org_membership_level` -and (since #10153) `manager` all do. The screen reads the team's own -`organization_id`, so it costs one row and a team that fails it never fans out. - -**Who does not lose access**, deliberately: a team stamped with the request's -own organization; a team stamped with **no** organization (`organization_id: -null` on a platform object means "owned by no organization" — what a seed -writes, since a seed cannot know the id the runtime mints at boot); a team id -with no `sys_team` row at all; and any request that carries no organization — -all four leave routing exactly as it was, because the tenancy fact is absent -rather than negative. - -⚠️ One externally observable accept→reject change beyond the routing itself: -under the non-default `onEmptyApprovers: 'fail'` policy, a node whose *sole* -approver was a cross-org team used to open a request and now throws -`NO_APPROVERS`. Under the default (`admin_rescue`) the node still opens. diff --git a/.changeset/tidy-cubes-count-their-columns.md b/.changeset/tidy-cubes-count-their-columns.md deleted file mode 100644 index f419deb259..0000000000 --- a/.changeset/tidy-cubes-count-their-columns.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/service-analytics": patch ---- - -Analytics measures are now compiled from everything they declare — `aggregate`, `field` **and** `filter` — on both the dashboard path and `POST /api/v1/analytics/query`. - -**Reported figures change, and the new ones are the declared ones.** Two corrections, both of which move numbers a dashboard or an API consumer is already reading: - -- A measure written `{ aggregate: 'count', field: 'some_column' }` used to compile to `COUNT(*)` and count **rows**. It now compiles to `COUNT("some_column")` and counts **non-null values**. Any such measure will report the same number as before or a **smaller** one, and a rate built on top of it (a numerator over a total) will drop accordingly — a "100%" tile whose column was mostly empty was reading its own denominator. -- `POST /api/v1/analytics/query` used to drop every per-measure `filter`, and the dataset's definition-level `filter` with it, returning unfiltered aggregates under the author's measure names. It now applies both, so the endpoint answers what the dashboard already answered for the same cube. Figures pulled through the API — agent tools, exports, downstream reports — will move to the filtered values; a measure declaring `filter: { stage: 'closed_won' }` stops counting every row. - -Measures that declare no `field` still compile to `COUNT(*)`, and a cube that is not a compiled dataset (an inferred or manifest cube) emits byte-for-byte the statement it did before. Measure filters lower to portable `CASE WHEN` conditional aggregates rather than `FILTER (WHERE …)`, which MySQL does not have. - -If a saved figure or a screenshot disagrees with what the platform now reports, the new number is the one the metadata declares. diff --git a/.changeset/time-relative-dispatch-ledger.md b/.changeset/time-relative-dispatch-ledger.md deleted file mode 100644 index b8e6008c89..0000000000 --- a/.changeset/time-relative-dispatch-ledger.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/service-automation': minor -'@objectstack/trigger-schedule': minor -'@objectstack/spec': patch ---- - -Time-relative sweeps are now idempotent per matched window (#10220). Previously the sweep -held no cross-tick memory, so every re-scan of the same window re-dispatched the same -records — a 5s-interval flow minted 15 duplicate reminders in ~70s, and even under a daily -cron a kernel rebuild re-dispatched the day's window. - -- `@objectstack/service-automation` — new platform object `sys_flow_dispatch`: a persisted - dispatch-claim ledger (ADR-0057 telemetry retention, 30 days), registered alongside - `sys_automation_run` and exposed as `AutomationEngine.claim(key): Promise` on - the automation service surface (check-and-record; a concurrent duplicate insert re-reads - and reports the key as already claimed). When no ObjectQL engine / registration is - available the engine degrades to in-process dedup and logs the weakened guarantee once; - when the ledger errors, the claim falls back to the in-process check for that key so a - store outage never blocks a dispatch (availability over strict-once). -- `@objectstack/trigger-schedule` — the time-relative sweep computes a dispatch key from - the MATCHED WINDOW's identity and claims it before launching: offset mode keys on - `(flowName, recordId, windowDay, offset)` — so a dateField edit that moves the window - legitimately re-fires — and range mode keys on `(flowName, recordId, sweepDay, - rangeSpec)`, preserving the documented `withinDays` semantic ("fires every day the - record stays in range") while never firing twice in one day. The trigger resolves the - claim surface structurally from the automation service; without one it dedups - in-process and warns once. -- `@objectstack/spec` — `sys_flow_dispatch` added to `PLATFORM_OBJECTS_BY_PACKAGE` under - `service-automation` (registry conformance). diff --git a/.changeset/ttl-onlywhen-null-predicate.md b/.changeset/ttl-onlywhen-null-predicate.md deleted file mode 100644 index 400ffc7014..0000000000 --- a/.changeset/ttl-onlywhen-null-predicate.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@objectstack/spec': minor -'@objectstack/objectql': minor ---- - -`lifecycle.ttl` now accepts an `onlyWhen` row filter mirroring `retention.onlyWhen`, and the shared `onlyWhen` value union gains the platform's canonical null predicate `{$null: boolean}` (on both blocks). A `transient` object that interleaves live rows with terminal audit tombstones can now spare rows defined by a value's absence — e.g. `ttl: { field: 'expires_at', expireAfter: '1d', onlyWhen: { revoked_at: { $null: true } } }` — instead of the TTL reaping backdated tombstones first. The LifecycleService Reaper passes `ttl.onlyWhen` into the same reap scope `retention.onlyWhen` already rides; declaring `ttl.onlyWhen` together with rotation storage or archive is refused at parse time, mirroring retention's guards. diff --git a/.changeset/unpublished-object-deny-message.md b/.changeset/unpublished-object-deny-message.md deleted file mode 100644 index 0df05a452c..0000000000 --- a/.changeset/unpublished-object-deny-message.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@objectstack/plugin-security": patch ---- - -**Message change (no behaviour change):** a data-plane read against an object that exists only as an **unpublished draft** now says so, instead of reporting an internal security step (#10401). - -The refusal itself is unchanged and stays fail-closed (#3545): same `PermissionDeniedError`, same `PERMISSION_DENIED` code, same HTTP 403, same `[Security] Access denied` prefix — which is a **matcher** the transports read as "this is a 403", not house style. Nothing here widens access, and no access decision branches on the new information. - -What changed is what the refusal *says*. One sentence — "the security posture of object 'X' could not be resolved for operation 'find'" — covered two conditions with two different remedies, and described neither: because it named a *security* step, every reader took it for a permissions problem and went looking for a sharing rule to change. Measured downstream (objectstack-ai/cloud#1481): an end-user AI turn asked "how many customers do I have?" against a draft-only object, spent seven tool calls oscillating between a metadata plane that said the object existed and this refusal, then told the user the object was "missing its sharing/visibility setting" — confident, professional, and wrong. On a free plan that one turn also exhausted the daily allowance. - -The two conditions are now separated: - -- **The object has a `sys_metadata` draft and no published row** → *"object 'X' is not published — a draft declaration exists but no published one … Publish the object to make it queryable. This is NOT a permissions problem …"*. -- **The declaration genuinely cannot be read** (never declared, or a metadata-store outage) → the pre-existing clause **verbatim**, so any surface matching `the security posture of object 'X' could not be resolved for operation 'Y'` keeps matching, followed by the remedy and the same explicit statement that permissions are not the lever. - -Both sentences, and the operator log line beside them, are derived from one module (`unresolved-posture.ts`) shared with the explain engine's `object_crud` layer detail. Enforcement and explanation stating one refusal in two drifting wordings is the defect shape this closes, so the wording is a single source rather than two literals. - -The discriminator comes from a **best-effort** `sys_metadata` probe that runs only on the path already refusing, reads under a system context (so it cannot re-enter the middleware), and fails safe in one direction only: any failure — no `sys_metadata` in the deployment, an unprovisioned store, a driver error — reports the both-conditions wording rather than a claim. A posture that resolves never probes at all. diff --git a/.changeset/v11-to-v16-session-alias-datings.md b/.changeset/v11-to-v16-session-alias-datings.md deleted file mode 100644 index 895e163d63..0000000000 --- a/.changeset/v11-to-v16-session-alias-datings.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@objectstack/spec': patch ---- - -Retarget four `roles` → `positions` action-session provenance strings from "v11" to -"v16" — the release that actually shipped the `#3280` deprecate → `#3290` remove -session-alias precedent they cite (`content/docs/releases/v16.mdx` is the only release -page citing `#3290`). - -Text-only provenance correction, no schema shape or acceptance change: - -- `ActionSessionSchema`'s `positions` and `roles` `.describe()` strings - (`packages/spec/src/ui/action-params.zod.ts`) — regenerates - `content/docs/references/ui/action-params.mdx`. -- The `action-session-roles-to-positions` migration rationale - (`packages/spec/src/migrations/registry.ts` and - `packages/spec/src/migrations/entries/semantic/17.action-session-roles-to-positions.ts`) - — regenerates `spec-changes.json` and `docs/protocol-upgrade-guide.md`. diff --git a/.changeset/view-door-list-view-field-rules.md b/.changeset/view-door-list-view-field-rules.md deleted file mode 100644 index 76be7c7877..0000000000 --- a/.changeset/view-door-list-view-field-rules.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -feat(lint): the two list-view field rules reach a standalone list view at the runtime publish gate — `view` writes are now judged by `validateSearchableFields` and `validateSortableFields` (#9313) - -An `active`-state `view` save through `saveMetaItem` (Studio, REST `/meta` item -CRUD, an MCP/AI author) is now refused with the existing 422 `invalid_metadata` -envelope when its list view declares a `sort` or `searchableFields` entry the -bound object cannot honor — an unknown field name, a virtual (`formula`) sort -target with no stored column to ORDER BY, or a search narrowing the #4254 -ingress gate would refuse on every toolbar search. Both rules already gated -`os validate` / `os build` / `os lint`; the runtime door — the only door a -Studio tenant or an MCP/AI author has — ran neither, and an author writing the -exact declaration these rules exist to refuse got it accepted. - -Two halves, because either alone is a silent no-op: the reference-integrity -suite's registry entry gains `runtimeTypes: ['view']`, and both rules' metadata -walks gain the SELF rung — a `views[]` entry that IS a flattened standalone -list overlay (`ViewMetadataSchema`'s list-overlay member: `viewKind: 'list'`, -no nested `config`), the shape a standalone list view takes on the wire and the -shape the gate snapshots as `views: [item]`. - -The suite dispatches per member on this door: a `view` snapshot reaches exactly -the two list-view field rules (`ReferenceIntegrityRule.runtimeTypes`, default -`['flow']`), never the members whose resolution universe the per-write snapshot -does not carry — `validateActionNameRefs` resolving against `stack.actions` -would otherwise refuse legitimate view writes. CLI behaviour is unchanged (the -commands run the full suite as before); `flow` snapshots keep every member. -Measured before crossing: 0 refusals and 0 advisories over 50 shipped -view-door bodies (11 containers + 39 console-shaped personalization overlays, -`sort[].id` decorations included) across four authoring lineages — a lower -bound, as every authored corpus is. Draft saves are untouched (D1), stored rows -keep being served (ADR-0087 asymmetry), and -`OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to a loud log. diff --git a/.changeset/view-door-viewitem-record-config-rung.md b/.changeset/view-door-viewitem-record-config-rung.md deleted file mode 100644 index 2ee1343555..0000000000 --- a/.changeset/view-door-viewitem-record-config-rung.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@objectstack/lint": minor ---- - -feat(lint): a standalone ViewItem record's nested `config.sort` / `config.searchableFields` reach the runtime publish gate (#10001) - -An `active`-state `view` save through `saveMetaItem` (Studio, REST `/meta` -item CRUD, an MCP/AI author) whose body is a standalone ViewItem RECORD — -`ViewMetadataSchema`'s member 1, `{ name, object, viewKind: 'list', config }`, -the shape a Studio-saved view takes and the shape objectui's `updateView` -round-trips on every pin/reorder toggle — is now refused with the existing -422 `invalid_metadata` envelope when its `config.sort` / `config.searchableFields` -declares a field the bound object cannot honor: an unknown name, a virtual -(`formula`) sort target with no stored column to ORDER BY, or a search -narrowing the #4254 ingress gate would refuse on every toolbar search. #9313 -closed the same gap for the flattened list overlay, one union member over; -the record's declarations live one level down, inside `config`, and were -judged by neither list-view field rule — so a record write carrying -`config.sort: [{ field: '' }]` published in silence and answered -`400 INVALID_SORT` (#6994/#7095) on the view's first fetch, every load. - -Walk-only, by design: #9313 already widened the reference-integrity suite -entry and exactly these two members onto `view` writes, so this change adds -the RECORD rung to both twin walks — recognised by the wire union's own -member discrimination (`viewKind: 'list'` AND a record-shaped `config`; the -flattened-overlay rung keeps its `no nested config` guard, a strict container -carries neither key, and a `form` record has no list-field surface), judged -against `listViewObject(config) ?? record.object` at path -`views[i].config.sort[…]` / `views[i].config.searchableFields[…]`. The -per-member granularity split is unchanged: no further suite member crosses -onto `view`. Measured before shipping: 0 refusals and 0 advisories over 39 -record-shaped console round-trip bodies (one per shipped list surface, -`config.sort[].id` decorations and `isPinned`/`sortOrder` riding along, the -shape `saveMetaItem` really stores) across the four shipped stacks — a lower -bound, as every authored corpus is. Draft saves are untouched (D1), stored -rows keep being served (ADR-0087 asymmetry), and -`OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to a loud log. diff --git a/.changeset/views-lint-posture-one-voice.md b/.changeset/views-lint-posture-one-voice.md deleted file mode 100644 index fa7ea38b60..0000000000 --- a/.changeset/views-lint-posture-one-voice.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -fix(spec): read the unknown-key lint's posture from the schema the parse applies, and report each record exactly once (#10039) - -An otherwise valid view container carrying one undeclared key produced two -contradicting messages from `defineStack`: - -``` -WARN: defineStack: views.v1.bogusViewKey: 'bogusViewKey' is not a declared view - key, so its value is dropped at load. -THREW: ✗ views.0: Unrecognized key(s) on this view container: `bogusViewKey`. … -``` - -The warning promises a silent drop — the view loads, minus one key — and the -refusal one step later says nothing loads at all. An author who reads the -warning and stops there draws the opposite conclusion from the truth, and a -warn channel that is sometimes really an error trains readers to discount it. - -**Root cause: the lint read posture off a different schema than the parse.** -`lintUnknownAuthoringKeys` took each collection's unknown-key posture from -`getMetadataTypeSchema(type)`. That registry answers a different question — it -names the schema for a *persisted metadata body* of that type. What -`defineStack` applies to a *stack collection entry* is the element schema in -`ObjectStackDefinitionSchema`'s own shape, and for `view` the two are not the -same object: - -- `getMetadataTypeSchema('view')` → `ViewMetadataSchema`, a strip-mode **union** - over the three persisted runtime shapes; -- `ObjectStackDefinitionSchema.shape.views` → `z.array(ViewSchema)`, and - `ViewSchema` is the `.strict()` defineView **container**. - -`lintUnknownStackKeys` has always avoided exactly this at the top level, and its -own source says why: a schema that rejects loudly must make the lint go quiet -"rather than become a second, possibly disagreeing voice". The per-collection -walker read the same rule off the wrong schema. - -The posture source is now the stack schema's own slot for the collection. -Measured across all 29 collections `PLURAL_TO_SINGULAR` names, the registry and -the stack slot agree everywhere except: - -| collection | type registry | stack slot | effect | -| --- | --- | --- | --- | -| `views` | `strip` / 91 keys | `strict` / 15 keys | **leaves the lintable set** | -| `themes`, `analyticsCubes` | unregistered | `strict` | skipped either way | - -So `connectors` is the honest remainder — it genuinely warns and drops — and no -other collection changes. - -**Second defect, same walk: every finding on a union root was emitted twice.** -`lintUnknownKeysAgainstSchema` reported the root record itself and *also* handed -that same record to `descend`, whose object arm skipped `depth === 0` ("already -reported by the caller") while its union arm had no such guard. `view` was the -only union root in the wild, so `defineStack` never showed it — the warn-once -set in `warnUnknownAuthoringKeys` absorbed the second copy — while every other -consumer of the exported walker saw both. The root report now lives in `descend` -alone, so each record is reported by exactly one place. That also closes a -latent third copy: a discriminated-union root whose branch the author *did* pick -was reported once against the merged key set and again against the branch's, and -is now reported once, against the branch — the narrower and more accurate set. - -**Nothing about what `defineStack` accepts or rejects changes.** The parse is -untouched; only which of the two existing voices speaks. - -### API change - -`lintUnknownAuthoringKeys` and `listLintableAuthoringCollections` now take -`ObjectStackDefinitionSchema` as a **required** parameter, injected the same way -and for the same reason `lintUnknownStackKeys` already required it — -`stack.zod.ts` imports this module, so importing the schema back would close a -cycle. Required rather than optional deliberately: an omitted argument falling -back to the type registry would silently reinstate the bug, which is the -silent-loss shape this whole rule family exists to report. Every in-repo call -site (`defineStack`, `os validate`, `os compile`) already had the schema in hand -for the sibling call on the adjacent line. - -Marked `minor` rather than `patch` because of that signature, not because of any -behavioural widening — the fix itself only makes one voice go quiet. diff --git a/apps/docs/CHANGELOG.md b/apps/docs/CHANGELOG.md index cc884ea343..20b01dc4f8 100644 --- a/apps/docs/CHANGELOG.md +++ b/apps/docs/CHANGELOG.md @@ -1,5 +1,23 @@ # @objectstack/docs +## 4.2.2 + +### Patch Changes + +- 72d75eb: docs site: drop `output: 'standalone'` so the production build stops failing + + The production build of the docs site died at the end of `next build` with + `ENOENT: no such file or directory, open '.../apps/docs/.next/next-server.js.nft.json'`, + so nothing merged to `main` reached the site. + + That file is opened by the standalone packer (`writeStandaloneDirectory` -> + `copyTracedFiles`), which Next calls **only** when `output === 'standalone'`. + Nothing in this repo consumes `.next/standalone` — no Dockerfile, workflow, + script or config references it, and `docker/Dockerfile` does not build + `apps/docs` at all — and Vercel does its own serverless packaging. The setting + served no consumer and was the sole reason that read happened, so removing it + removes the only code path that can raise this error. + ## 4.2.1 ### Patch Changes diff --git a/apps/docs/package.json b/apps/docs/package.json index 4195ea4687..16f9e9dd8b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/docs", - "version": "4.2.1", + "version": "4.2.2", "private": true, "description": "ObjectStack Protocol Documentation Site", "license": "Apache-2.0", diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index e165cfc70c..36d3121cf5 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -76,7 +76,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.1.0 + ghcr.io/objectstack-ai/objectstack:17.2.0 ``` (`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -94,7 +94,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.1.0 + ghcr.io/objectstack-ai/objectstack:17.2.0 ``` Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly @@ -145,7 +145,7 @@ COPY . . RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── -FROM ghcr.io/objectstack-ai/objectstack:17.1.0 +FROM ghcr.io/objectstack-ai/objectstack:17.2.0 COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json ``` @@ -163,7 +163,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.1.0 +RUN npm install -g @objectstack/cli@17.2.0 WORKDIR /srv/app RUN chown node:node /srv/app diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index 42df9410c1..510d215b40 100644 --- a/content/docs/upgrading.mdx +++ b/content/docs/upgrading.mdx @@ -38,7 +38,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.1.0 +image: ghcr.io/objectstack-ai/objectstack:17.2.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 f1855f4de2..041d537695 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.1.0 +FROM ghcr.io/objectstack-ai/objectstack:17.2.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.1.0 + ghcr.io/objectstack-ai/objectstack:17.2.0 ``` `OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -62,5 +62,5 @@ reverse-proxy / multi-node guidance: ## Local build of this image ```bash -docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.1.0 docker/ +docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.2.0 docker/ ``` diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 1d566502dc..8cfd274ae3 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/example-crm +## 4.0.94 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/runtime@17.2.0 + ## 4.0.93 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index c7e63345da..36d9602e52 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.93", + "version": "4.0.94", "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-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index 9a1e94c79d..9d5e8251b4 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,78 @@ # @objectstack/example-showcase +## 0.3.16 + +### Patch Changes + +- 6cca75c: Fix the showcase react pages' `useAdapter()` query contract, and pin it (#10288) + + `renewals-pipeline` passed `top: 500` and `crm-workbench` passed `limit: 200` to + `adapter.find`. Neither is a query option: `QueryParams` declares only `$`-prefixed keys + and `ObjectStackAdapter.convertQueryParams` copies exactly those, so the key reached no + branch and was dropped with no error. The consequence is the opposite of a truncated + read — the GET list route has **no default page size**, so an absent `top` returns the + ENTIRE match set, and the cap the author wrote never happened. + + The same effect then read its rows off `.records`. `find()` resolves to a normalized + `QueryResult` (`data` + `total`), never the REST envelope, so `pr.records` was + `undefined` on every call and the renewals KPI strip sat at `0 / 0 / 0` while the + `` beside it showed the same rows correctly. Measured on a 640-row account with + the real page source driven against a contract-faithful adapter double: before, + `$top` arrives `undefined` and the strip reads `{projects: 0, invoices: 0, openInvoices: 0}`; + after, the cap is applied and it reads `{projects: 640, invoices: 640, openInvoices: 100, + capped: true}`. + + Applying the cap is only half a fix, because `data.length` under a `$top` is exactly the + silently-capped count the card was filed about — so both pages now count the envelope's + `total` (the server's real count over the same `$filter` whenever a limit was applied). + The one number a cap genuinely bounds, "Open AR", is a per-row verdict over the fetched + window; it renders as `100+` rather than passing for a total. + + `test/react-page-adapter-query-contract.test.ts` executes the page's real rollup effect + and then sweeps every `kind:'react'` page in the app for both contracts, with an + extraction control, a census control, and a positive control on the scanners. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [46cfa5b] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [6d5c4fa] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/driver-sql@17.2.0 + - @objectstack/runtime@17.2.0 + - @objectstack/cloud-connection@17.2.0 + - @objectstack/connector-openapi@17.2.0 + - @objectstack/connector-rest@17.2.0 + - @objectstack/connector-slack@17.2.0 + - @objectstack/connector-mcp@17.2.0 + - @objectstack/service-datasource@17.2.0 + ## 0.3.15 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 4920fbe26c..14e8337712 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.15", + "version": "0.3.16", "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 237fda1fd0..4b214fca8b 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,57 @@ # @objectstack/example-todo +## 4.0.94 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [2570ab0] +- Updated dependencies [d23e3a0] +- Updated dependencies [f3a8134] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [6d5c4fa] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [502dc6f] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/client@17.2.0 + - @objectstack/runtime@17.2.0 + - @objectstack/service-knowledge@17.2.0 + - @objectstack/mcp@17.2.0 + - @objectstack/metadata@17.2.0 + - @objectstack/driver-sqlite-wasm@17.2.0 + - @objectstack/knowledge-memory@17.2.0 + ## 4.0.93 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index e2f14a1e5e..f5efb426c6 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.93", + "version": "4.0.94", "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 d3b3f542a8..82485132aa 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,44 @@ # @objectstack/example-embed-objectql +## 0.0.34 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [2570ab0] +- Updated dependencies [d23e3a0] +- Updated dependencies [f3a8134] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/driver-memory@17.2.0 + ## 0.0.33 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index fa9e19db11..7aeb7d194e 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.33", + "version": "0.0.34", "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 09686fc1e4..9b392cc09f 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,18 @@ # @objectstack/hono +## 17.2.0 + +### Patch Changes + +- Updated dependencies [128684d] +- Updated dependencies [b03a880] +- Updated dependencies [914c413] +- Updated dependencies [67630c4] +- Updated dependencies [9f483d9] + - @objectstack/runtime@17.2.0 + - @objectstack/plugin-hono-server@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index 1890a357e7..a3915889a1 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "17.1.0", + "version": "17.2.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 9d9f9877aa..4b70b50edc 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/account +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index d187c8f90b..ea2bc77c7f 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "17.1.0", + "version": "17.2.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 9ceef3ec8c..bb53ce0fad 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/setup +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index 8c13355071..00bdf16fcb 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "17.1.0", + "version": "17.2.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 5e4965a622..f07f4ed29a 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/studio +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 414b2f34f9..c8573cbc5c 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "17.1.0", + "version": "17.2.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 5bc67b1a63..de9fc5417d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,331 @@ # @objectstack/cli +## 17.2.0 + +### Patch Changes + +- 675ab57: **First-run polish:** a brand-new scaffold's very first `pnpm install` no longer reports two unmet peer dependencies (#10326). + + Reproduced on a clean scaffold from published `create-objectstack@17.1.0` — no lockfile, `node_modules` removed, nothing configured by the user — and again on the second scaffold path, `objectstack init`. Both printed the same two: + + ``` + ✕ unmet peer better-call + Installed: 1.4.0 + Wanted: + 1.3.7: + @better-auth/scim@1.7.0-rc.1 + + ✕ unmet peer better-sqlite3 + Installed: 13.0.3 + Wanted: + ^12.0.0: + better-auth@1.7.1 + ``` + + Nothing was broken — but it is the first screen a newcomer sees, and there is nothing they did to cause it or can do about it. + + **`better-sqlite3`: the pin is right and the upstream range is stale — so it is widened, not corrected.** better-auth 1.7.1 declares `better-sqlite3` as an **optional** peer at `^12.0.0`, and it governs exactly one configuration: a raw better-sqlite3 `Database` handed to better-auth's `database` option, which its Kysely dialect then drives. ObjectStack never takes that path — `AuthManager.createDatabaseConfig()` returns `createObjectQLAdapterFactory(dataEngine)`, and every `better-sqlite3` use under `plugin-auth` is knex's `client: 'better-sqlite3'` beneath ObjectQL. Measured anyway on the configuration the range *does* govern: better-auth 1.7.1 with `database: new Database(':memory:')`, running `getMigrations().runMigrations()`, `signUpEmail`, `signInEmail` and adapter `findOne`/`update`/`delete`, is green on **better-sqlite3 13.0.3** and byte-for-byte equivalent on **12.11.1**. The same probe with `Database.prototype.prepare` neutered fails, so that green is the driver's and not an unexercised path. Pinning our own `^13.0.3` declarations back to `^12` would downgrade a native module across the platform to satisfy a range measurement shows is simply behind. + + **`@better-auth/scim`: the rc pin stays, and one `better-call` copy is the correct tree.** `npm view @better-auth/scim dist-tags` reads `latest: '1.7.1'`, but stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is a separate migration rather than a version bump; the exact `1.7.0-rc.1` pin is deliberate. The rc peers an exact `better-call@1.3.7` while better-auth 1.7.1 depends on `1.4.0` — and a better-auth plugin has to share the **host's** better-call instance, so the single 1.4.0 copy every install already resolves is right, not a skew to repair. This declaration retires together with the rc pin. + + **What changed, and what deliberately did not.** Both remedies are pnpm `peerDependencyRules.allowedVersions` entries, scoped `>` so each widens exactly one declaration. They ship *inside* the scaffold — the bundled `pnpm-workspace.yaml` template and the one `objectstack init` renders — because a block in this repo's own workspace file does not travel with published packages. `allowedVersions` changes what pnpm **reports**, never what it resolves: measured on both scaffold paths, the lockfile is byte-identical with and without it (0 lines of diff), and no dependency version, range or resolution moved anywhere. This repo's own resolutions are untouched. +- 8d1fa00: Two ways to invoke the CLI wrong used to present as a crashed boot. Both now say what they are. + + `node packages/cli/dist/index.js` — the package `main`, which is a re-export barrel — ran to completion, printed nothing and exited 0. Backgrounded, that is indistinguishable from a server that came up and died. It now writes two lines to stderr, the first saying that running this file starts nothing and the second naming `bin/run.js` as the CLI entry point, and exits 1. + + A rejected invocation such as `objectstack dev --no-ui` answered with oclif's error line followed by a full usage dump, and in a background log the dump is what the eye lands on. One line now goes to stderr ahead of it: + + ``` + objectstack: INVOCATION ERROR — Nonexistent flag: --no-ui. The command never ran: nothing was started and nothing is listening. Invoked as: objectstack dev --no-ui + ``` + + No flag surface changed: `dev` still rejects `--no-ui` (only `serve` declares `ui` with `allowNo`). What changed is what the CLI says when it rejects an invocation. +- bde0ab9: Remove the abandoned tsup build path from `packages/cli` (#10185): the + `tsup.config.ts`, the orphaned `src/bin.ts` it was the only referrer of, and + the now-unused `tsup` devDependency. + + The package has built with `tsc -p tsconfig.build.json` since the oclif + migration, which also introduced `oclif.commands.target: "./dist/commands"` + and moved the `bin` field onto `bin/run.js`. The tsup config was left behind + by that commit and never invoked again — but it was not inert. It declared + `clean: true` with only `src/bin.ts` and `src/index.ts` as entries, so anyone + running the obvious `tsup` next to a `tsup.config.ts` would wipe `dist/` and + emit no `dist/commands/**` at all, leaving a CLI that resolves zero commands. + Deleting it removes the trap rather than documenting it. + + No published behaviour changes: the resolved oclif command surface is + identical before and after (60 commands, 68 topics). The only build-output + difference is that `dist/bin.js` — a re-export of `execute` from + `@oclif/core` that nothing imported — is no longer emitted. +- 53428b8: Fix `os serve` failing to boot with `OS_CLUSTER_DRIVER=redis` when the app + declares `@objectstack/service-cluster` (#10645). The cluster gate and its + driver were reached through a bare dynamic `import()`, which Node ESM resolves + against the CLI's own realpath — inside the framework workspace — so packages + installed under the host app were invisible to it and boot died with + `Cannot find package '@objectstack/service-cluster'`. Both loads now go through + the host-anchored importer `serve` already uses for its other optional and + enterprise packages, so any package the app declares resolves the way the app + declares it. The host importer is now defined at the top of the boot sequence + rather than partway down, which is what made these two loads fall back to bare + resolution in the first place. No change to what `serve` accepts or refuses: + an undeclared package is still refused by the same declaration gate. +- 63d603e: **Fix:** `os datasource list-tables`, `os datasource introspect` and + `os datasource validate` now read the response envelope the server actually + emits, so all three work against a live server for the first time (#10675). + + The three commands read the pre-#3843 **flat** shape — `body.tables`, + `body.draft`, `body.results`, and `body.error` as a string — while every REST + body the platform sends is the declared envelope written by `sendOk` / + `sendError`: `{ success: true, data: { … } }` or + `{ success: false, error: { code, message } }`. Nothing failed loudly, because + each payload simply read `undefined` and every command reported that as an + ordinary empty result: + + - `list-tables` printed `No remote tables found.` while the server was + returning two tables. + - `introspect` printed `Failed to generate draft` for drafts the server had + generated. + - `validate` printed `No federated objects to validate.` and exited **0** + against drift the server had flagged `missing_column … severity:error` — a + schema gate green-lighting a CI-breaking condition it had never read. + - An unknown datasource crashed with `TypeError: first argument must be a + string or instance of Error`, because the error **object** was handed to + oclif's `this.error()` instead of `error.message`. + + `validate`'s exit code is the behaviour change to note: a datasource whose + federated objects have drifted now exits **1** where it previously exited 0. If + you have a pipeline that treats this command as advisory, it starts failing on + drift that was always there. + + A body that is **not** the declared envelope is now a loud failure rather than + an empty payload. That distinction is the point: "nothing found" is reachable + only from a server that really said so, never from a response the CLI could not + read. The legacy flat shape is deliberately *not* also accepted — a + consumer-side fallback would re-create the divergence as a second de-facto + contract. +- a7ea328: `os doctor` no longer prints `✓ Test coverage` / `✓ Deprecations` about a tree it + never examined, and no longer warns `@objectstack/spec Not built` about a + workspace that is not part of the tree (#10679). + + `findMissingTests()` and `findDeprecatedUsages()` both walk + `/packages/spec/src` — a path that exists in this monorepo and in no + application built with the framework. Both answered "that directory is not here" + with the same value they return for "I walked it and found nothing wrong" (an + empty array), so in a stock `create-objectstack -t blank` scaffold every run + printed, verbatim: + + ``` + ✓ Test coverage All *.zod.ts files have matching tests + ✓ Deprecations No @deprecated tags found + ``` + + about files doctor never opened. The command exits 0 either way, so "no problems + found" and "I never looked" were byte-identical to every downstream reader. + + Doctor already refuses to do this one screen down: the ADR-0120 D5e advisory's + `✓ Unique scope` is withheld unless `ledgerReadingIsComplete()` says the ledger + half was read in full. These two checks escaped that discipline; this restores + it, in the same shape #5413 used for the ledger — whether the tree was examined + is now a fact in the return type rather than an absence, so the print site + cannot reach the `✓` from the unexamined arm. Where the tree is absent doctor + prints an informational, named-reason skip instead: + + ``` + ℹ Test coverage Skipped — no packages/spec/src in this directory (monorepo-only check) + ℹ Deprecations Skipped — no packages/spec/src in this directory (monorepo-only check) + ``` + + `--verbose` adds the resolved directory it looked for. The skip is deliberately + not a warning: nothing is wrong in an application that has no + `packages/spec/src`, and withholding a false `✓` must not manufacture a false + `⚠`. + + The adjacent `⚠ @objectstack/spec Not built` probe read `/packages/spec/dist` + with no check that the workspace it names exists, so in an application it warned + about an absent package and prescribed `pnpm --filter @objectstack/spec build`, a + command that cannot succeed there. It is now gated on `packages/spec/package.json` + being present. Inside the monorepo the row is unchanged; outside it there is no + row, and an application's spec dependency stays covered by the `Dependencies` + check and by the spec-version-gap advisory. + + Exit codes are untouched — 1 exactly when an error row exists, warnings never + flip it. One visible consequence: a stock scaffold with no other findings now + ends on `✅ Environment is healthy and ready for development!` instead of + `⚠️ Environment is functional but has some warnings`, because the warning it + used to carry was about a workspace that was never there. +- 13fa51e: `objectstack init` now writes both build-approval keys into the scaffolded + `pnpm-workspace.yaml`, so a brand-new project's first `pnpm install` succeeds + on pnpm 11 (#10405). + + The renderer emitted only `onlyBuiltDependencies`. pnpm 11 does not read that + key at all, and it turned an unapproved dependency build script from a warning + into a hard error — so `objectstack init my-app && cd my-app && pnpm install` + exited 1 with `ERR_PNPM_IGNORED_BUILDS`, on the very first command after + scaffolding. The rendered file now also carries `allowBuilds`, built from the + same source list, which is the only key pnpm 11 reads. Measured one clean + install per pnpm version, each with its own store: pnpm 10.0.0-10.25.0 read + `onlyBuiltDependencies`, 10.26.0-10.34.x read either key, and 11.x reads + `allowBuilds` only — so both keys are load-bearing and neither is redundant. + + Build permission is still granted to exactly the two packages that need it and + nothing else: `esbuild` (a `postinstall` that installs its platform binary, + used to compile `objectstack.config.ts`) and `better-sqlite3` (ships a + `binding.gyp`, which pnpm treats as a native build; without it `objectstack + serve` can fail with "Could not locate the bindings file"). No wildcard. + + Existing scaffolds are unaffected — `init` never overwrites a + `pnpm-workspace.yaml` that is already there. To fix a project scaffolded by an + earlier CLI, add to its `pnpm-workspace.yaml`: + + ```yaml + allowBuilds: + better-sqlite3: true + esbuild: true + ``` +- 621a487: **Bug fix (silent failure made loud):** `serve` now prints a boot-time diagnostic when the configured auth base URL cannot be parsed, instead of discarding the failure in an empty `catch` (#10202). + + The base URL was resolved through a `??` chain and parsed inside `try { new URL(baseUrl) } catch { /* ignore malformed baseUrl */ }`. That catch was the only place in the boot that learned the value was unusable, and it threw the knowledge away: the deployment's own origin never reached the `trustedOrigins` allow-list, boot continued normally, and the operator's first news of it was a browser-side `403 INVALID_ORIGIN` that names neither the variable nor the value. + + The shape that reaches it is ordinary env plumbing. `readEnvWithDeprecation` returns the preferred variable whenever it is `!== undefined`, so a **present-but-empty** variable resolves to `''` rather than `undefined`; `??` falls through only on `null`/`undefined`, so `OS_AUTH_URL=` on its own line in an env file (or a Helm/systemd/CI template rendering an absent key) consults neither `OS_BASE_URL` nor the `http://localhost:` default; and `new URL('')` throws. + + Measured on a real `os serve` boot with `NODE_ENV=production`, `OS_AUTH_URL=` set-but-empty and `OS_TRUSTED_ORIGINS` / `OS_ROOT_DOMAIN` / preview mode unset, probing `POST /api/v1/auth/sign-in/email` so a trusted origin answers `401 INVALID_EMAIL_OR_PASSWORD` and an untrusted one `403 INVALID_ORIGIN`: + + | Origin | `OS_AUTH_URL=` (empty) | `OS_AUTH_URL=https://app.example.com` | unset | + | --- | --- | --- | --- | + | `https://app.example.com` | 403 | **401** | 403 | + | `http://localhost:` | **401** | 403 | **401** | + | `http://tenant.localhost:` | **401** | 403 | 403 | + | `/api/v1/health`, `/api/v1/ready` | 200 | 200 | 200 | + + Two corrections to how this was expected to behave, both from that table. The allow-list does **not** come out empty: `serve` passes `trustedOrigins.length ? trustedOrigins : undefined`, and `AuthManager` substitutes a localhost wildcard trio for an absent list — so better-auth receives a non-empty list and localhost origins are trusted. Which makes set-but-empty strictly **more permissive than unset**: `http://tenant.localhost:` is trusted in the empty case and refused in the unset case, so an env template that renders an absent key to the empty string silently widens a production CSRF allow-list. + + **What changed is only what is said, never what is resolved.** The precedence chain, its order, and the `${protocol}//${host}` origin spelling are byte-for-byte identical; a set-but-empty `OS_AUTH_URL` still stops the chain exactly as before. Treating empty as unset inside the shared `readEnvWithDeprecation` would change behaviour for every caller of that helper and remains a separate, deliberate decision. The diagnostic is a warning, not a refusal to boot: a deployment running set-but-empty today keeps starting, and now says why authentication will not work. + + The resolution is exported as a seam — `resolveAuthBaseUrl()` and `formatUnusableAuthBaseUrlDiagnostic()`, alongside this file's sibling helpers — so the behaviour is reachable from tests without booting a server. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [da891e0] +- Updated dependencies [a38c3ff] +- Updated dependencies [76deca2] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [dd41df3] +- Updated dependencies [2570ab0] +- Updated dependencies [5886ee6] +- Updated dependencies [8163a1c] +- Updated dependencies [02d56b4] +- Updated dependencies [46cfa5b] +- Updated dependencies [b20c8d2] +- Updated dependencies [6ce58a7] +- Updated dependencies [d23e3a0] +- Updated dependencies [9a1ed7a] +- Updated dependencies [f3a8134] +- Updated dependencies [b03a880] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [5b0af2b] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [78818ec] +- Updated dependencies [9d7d2de] +- Updated dependencies [13f533a] +- Updated dependencies [900e489] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [9e04c3e] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [112a8c6] +- Updated dependencies [a16ff50] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [6d5c4fa] +- Updated dependencies [3ee8ddf] +- Updated dependencies [0c24898] +- Updated dependencies [16cef97] +- Updated dependencies [4389fe9] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [b419135] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [bc400af] +- Updated dependencies [5f2e54c] +- Updated dependencies [e2bb237] +- Updated dependencies [86a8ec9] +- Updated dependencies [35ad101] +- Updated dependencies [502dc6f] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [aa765b9] +- Updated dependencies [6439f8b] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [24ba050] +- Updated dependencies [f399618] +- Updated dependencies [adbcbfd] +- Updated dependencies [f1b5ad3] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/plugin-auth@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/driver-sql@17.2.0 + - @objectstack/driver-turso@17.2.0 + - @objectstack/driver-mongodb@17.2.0 + - @objectstack/driver-memory@17.2.0 + - @objectstack/client@17.2.0 + - @objectstack/service-storage@17.2.0 + - @objectstack/plugin-audit@17.2.0 + - @objectstack/runtime@17.2.0 + - @objectstack/service-analytics@17.2.0 + - @objectstack/service-automation@17.2.0 + - @objectstack/service-cache@17.2.0 + - @objectstack/service-job@17.2.0 + - @objectstack/metadata-protocol@17.2.0 + - @objectstack/plugin-security@17.2.0 + - @objectstack/service-messaging@17.2.0 + - @objectstack/plugin-email@17.2.0 + - @objectstack/rest@17.2.0 + - @objectstack/plugin-hono-server@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/lint@17.2.0 + - @objectstack/plugin-approvals@17.2.0 + - @objectstack/cloud-connection@17.2.0 + - @objectstack/plugin-reports@17.2.0 + - @objectstack/plugin-sharing@17.2.0 + - @objectstack/plugin-webhooks@17.2.0 + - @objectstack/service-settings@17.2.0 + - @objectstack/mcp@17.2.0 + - @objectstack/trigger-schedule@17.2.0 + - @objectstack/account@17.2.0 + - @objectstack/setup@17.2.0 + - @objectstack/metadata@17.2.0 + - @objectstack/service-queue@17.2.0 + - @objectstack/service-realtime@17.2.0 + - @objectstack/verify@17.2.0 + - @objectstack/service-sms@17.2.0 + - @objectstack/driver-sqlite-wasm@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/service-datasource@17.2.0 + - @objectstack/service-package@17.2.0 + - @objectstack/trigger-api@17.2.0 + - @objectstack/trigger-record-change@17.2.0 + - @objectstack/types@17.2.0 + - @objectstack/plugin-pinyin-search@17.2.0 + - @objectstack/console@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 6549dd0738..b897d78fa3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.1.0", + "version": "17.2.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 d98a7feb70..72793a66d4 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/client-react +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/client@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index e3ebd79c1a..04fe0b30fa 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "17.1.0", + "version": "17.2.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 79f32dbbbf..2f2371ee2e 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,84 @@ # @objectstack/client +## 17.2.0 + +### Minor Changes + +- 67630c4: The `/meta` FSM state route is singular: `meta.getLegalNextStates` moves, the plural registration is retired (#10077) + + Step 2 of the #9180 ruling — the `/meta` type segment is always singular, no + exception and no tolerated plural alias. Maintainer re-weigh, 2026-08-17, + verbatim: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」 + + - `client.meta.getLegalNextStates(object, field, from?)` now requests + `GET /api/v1/meta/object/:name/state/:field`. Same method, same arguments, + same response body — only the path segment changes. + - `GET /api/v1/meta/objects/:name/state/:field` is **no longer registered**. + The singular twin has been mounted alongside it since #7526, so the + migration for a hand-rolled HTTP caller is to drop the `s`. A request to the + retired spelling now gets the transport 404, which is the loud answer; the + one shape that changes hands rather than 404ing is a field literally named + `published`, which the compound `/:type/:section/:name/published` route + picks up. + - The two route ledgers follow what is mounted and what the SDK calls: the + plural row is deleted from `rest-route-ledger.ts` and the dispatcher ledger's + mirror row is respelled. + + **What this does not change.** The boundary fold `META_URL_TO_SINGULAR` is + untouched, so no `/meta/:type/...` spelling that is accepted today becomes + refused: the retired route matched a **literal** path segment and never + consulted the fold. The 2026-08-17 re-weigh (item 3) defers that break with no + scheduled window. The legacy dispatcher branch in `runtime/src/domains/meta.ts` + also still matches both literals; narrowing it is not part of this step. + +### Patch Changes + +- 59eb04d: Stop documenting bare `POST /api/v1/ai/chat` as agent-resolved (#10510). Two + shipped docblocks described a resolution step the route does not perform: + `client.ai.agents` claimed `/ai/chat` "talks to the environment's default + agent", and `App.defaultAgent` claimed that endpoint auto-resolves the app's + agent from `context.appName`. The bare route loads no agent and never reads + `context.appName`; the default-agent chain (explicit > `defaultAgent` of the + named app > first active) is driven by the assistant chat endpoint, + `POST /api/v1/ai/assistant/chat`, and `client.ai.agents.chat()` is the only SDK + method that reaches an agent at all. + + Both sites read as a security-relevant scoping guarantee — an agent-resolved + endpoint would have its tool offer scoped by that agent's skills (ADR-0063 + §1/§5) — so a reader auditing "which endpoints are surface-scoped?" from these + declarations got the wrong answer at both. Documentation text only: no schema + key, no parse behaviour and no runtime path changes. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index d4cce14c5e..4c52955214 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.1.0", + "version": "17.2.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 ad0b32a148..660b0c4e4e 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,99 @@ # @objectstack/cloud-connection +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/runtime@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 488b7d943f..3307bee7ca 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "17.1.0", + "version": "17.2.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 (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 92d4adb373..b4dc7e11bc 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/connector-mcp +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 70213de48a..b9a5c77364 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.1.0", + "version": "17.2.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 7a3a089c92..08d3f87ad7 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,58 @@ # @objectstack/connector-openapi +## 17.2.0 + +### Patch Changes + +- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel + actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and + `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and + `LiteKernel.destroy()`, which walk the plugins in reverse calling + `plugin.destroy()`, walked straight past every plugin whose teardown was spelled + `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still + armed, the REST/OpenAPI/Slack connectors still registered on the automation + engine, the approvals SLA escalation job still scheduled, and the knowledge + event-sync subscription still open. + + Each teardown body now lives in `destroy()`. `stop()` is retained as a + delegating alias with its parameter made optional, so an embedder that learned + to call it directly — precisely because the kernel never did — keeps working + unchanged. No export is removed and the `Plugin` interface is untouched. + + Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as + fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted + from the merge queue. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 45b2824f25..1878199797 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.1.0", + "version": "17.2.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 a577fed1a1..8682029de4 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,58 @@ # @objectstack/connector-rest +## 17.2.0 + +### Patch Changes + +- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel + actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and + `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and + `LiteKernel.destroy()`, which walk the plugins in reverse calling + `plugin.destroy()`, walked straight past every plugin whose teardown was spelled + `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still + armed, the REST/OpenAPI/Slack connectors still registered on the automation + engine, the approvals SLA escalation job still scheduled, and the knowledge + event-sync subscription still open. + + Each teardown body now lives in `destroy()`. `stop()` is retained as a + delegating alias with its parameter made optional, so an embedder that learned + to call it directly — precisely because the kernel never did — keeps working + unchanged. No export is removed and the `Plugin` interface is untouched. + + Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as + fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted + from the merge queue. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 94336a9923..a7980d95bd 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.1.0", + "version": "17.2.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 49735eee38..d8c1f2d662 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,58 @@ # @objectstack/connector-slack +## 17.2.0 + +### Patch Changes + +- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel + actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and + `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and + `LiteKernel.destroy()`, which walk the plugins in reverse calling + `plugin.destroy()`, walked straight past every plugin whose teardown was spelled + `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still + armed, the REST/OpenAPI/Slack connectors still registered on the automation + engine, the approvals SLA escalation job still scheduled, and the knowledge + event-sync subscription still open. + + Each teardown body now lives in `destroy()`. `stop()` is retained as a + delegating alias with its parameter made optional, so an embedder that learned + to call it directly — precisely because the kernel never did — keeps working + unchanged. No export is removed and the `Plugin` interface is untouched. + + Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as + fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted + from the merge queue. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 30cc0b6be7..58209d8dac 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.1.0", + "version": "17.2.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 6fc263d1ca..991af1a200 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/console +## 17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index 46b661cae6..7ed26afcf8 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.1.0", + "version": "17.2.0", "description": "Prebuilt Console SPA pinned to this @objectstack/framework release. 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 d5d0f67353..bc50347067 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,76 @@ # @objectstack/core +## 17.2.0 + +### Patch Changes + +- 47cd3ec: The kernel's two `Promise.race` timeout guards — the startup guard around each + plugin's `init`/`start`, and the shutdown guard around `performShutdown()` — + now reclaim **both** halves of the guard when the race settles: the timer is + cleared *and* the losing promise is settled (#10604). + + Neither site settled its loser, so the timeout promise and the reaction + `Promise.race` held on it were retained for the life of the process — four + leaking promises per showcase test run under `vitest --detectAsyncLeaks`, now + zero. The two hand-rolled copies had also drifted into doing opposite halves of + the same cleanup: the startup site cleared its timer and never `unref`'d, the + shutdown site `unref`'d and never cleared. Both now go through one internal + `TimeoutGuard`, so they cannot drift apart again. No exported API changes. + + **Behaviour change, at the shutdown guard:** the shutdown timer is no longer + `unref()`d. Two consequences for an embedding host (CLI, auth-proxy, test + runner): + + - After a **successful** shutdown, no timer is left armed. Previously the guard + survived its own race and stayed scheduled to fire against a kernel already + `'stopped'`. That late rejection was *handled* — `Promise.race` had attached a + rejection handler to it — so this was never an unhandled-rejection risk; it + was retained work and a wakeup after teardown. + - When teardown **hangs**, the guard now actually fires. An unref'd timer does + not keep the event loop alive, so a process with nothing else to run could + exit silently — status 0, teardown incomplete — before `shutdownTimeout` + elapsed, leaving `Shutdown timed out — forcing exit` and its `exit(1)` + unreachable in exactly the case they exist for. Reclaiming on settle keeps the + guard ref'd exactly as long as the race is undecided, which is the guarantee + the startup guard already had (#4813). + + If your host relied on a hung `shutdown()` letting the process fall out of the + event loop on its own, it will now wait up to `shutdownTimeout` (default 60s) + and then hard-exit with status 1. Lower `shutdownTimeout` in the kernel config + to shorten that window. +- 9d7d2de: `resolveLocalizationContext` now memoizes a FAILED read's fallback per `(ql, tenantId, userId)` for 30s (#10221). + + On a fresh environment whose `sys_setting` table hasn't been created/migrated yet, every authenticated request re-ran the same `sys_setting` localization read, and every one of those reads failed the same way ("no such table"). The `#2409` batching had already collapsed the three per-key reads a single request used to issue into one query, but that one query still repeated on every subsequent request, and `driver-sql`'s `backendStatementFault` logs a `[sql-driver] DATABASE_ERROR` warning on every failed read — so the identical warning printed once per request and buried real errors in between. + + Only the case where the underlying read genuinely fails (a backend fault, e.g. the missing table) is cached; a successful read — including a legitimate "nothing configured yet" empty result — is never cached and always re-reads on the next call, so a settings write takes effect immediately. (An earlier version of this fix cached every outcome, mirroring `packages/plugins/plugin-audit/src/audit-writers.ts`'s existing TTL cache of this same read — safe there because audit-trail enrichment is best-effort, but not safe for `@objectstack/rest`'s use of this function: analytics date-bucketing reads the org timezone on every query and `packages/qa/dogfood/test/analytics-timezone.dogfood.test.ts` — the #1982/#2018 golden regression — asserts the very next read reflects a just-written timezone.) The `UTC` / `en-US` fallback behavior itself is unchanged; this only stops the failing query — and its log line — from re-running every request. The cache is keyed on the `ql` engine instance first, so two environments/tenants sharing one process never share a cached outcome, and self-heals within one TTL window once `sys_setting` exists. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index daa56a2c38..a1d5d6e74c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.1.0", + "version": "17.2.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 f280be998e..ebfd53f72e 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,87 @@ # create-objectstack +## 17.2.0 + +### Minor Changes + +- 5a616d5: `create-objectstack` now closes with a "Created files" summary derived from a + walk of the finished project directory, so it names everything the run wrote — + including the files written after the template copy (#10323). + + The old summary was the template copy's own list, printed before + ` install` and before `npx skills add`. Measured against published + `create-objectstack@17.1.0` (`create-objectstack demo-app`, then a full walk of + the result): 12 entries printed, 18,045 paths on disk, **18,033 of them + unreachable from the summary** — `AGENTS.md`, `.github/copilot-instructions.md`, + `pnpm-lock.yaml`, `skills-lock.json`, `node_modules/`, and two ~968 KB trees of + agent instructions at `.agents/skills/` and `agent/skills/`. + + That mattered because the same run ends with the `skills` CLI printing *"Review + skills before use; they run with full agent permissions."* Advice to review + files the run never named, at paths it never showed, is advice a newcomer + cannot act on — the wrong failure direction for a security-flavoured warning. + + The list could not have been correct where it stood: two of the three write + phases belong to other processes, and the `skills` installer's destination set + moves with **its** releases, not ours. Reading the directory afterwards makes + the summary self-correcting instead. Large directories collapse to one line + carrying their path, entry count and size, so the bulk stays reviewable without + 18,000 lines of output, and the paths the skills installer created are marked + `⚠ skills` with the permissions warning tied to them. + + Same run, after the change: 20 entries printed, **0 written paths unreachable**. + +### Patch Changes + +- 3a3f209: Tell a newcomer that the `blank` starter ships no app, so an empty Console + reads as the intended starting point rather than a broken install (#10317). + + Measured on a real scaffold-and-boot (`create-objectstack my-app -t blank`, + published 17.1.0 packages, `objectstack dev --ui`): `GET /api/v1/meta/app` + returns the two platform apps (Setup, Account) and nothing of the project's + own, while `GET /api/v1/data/my_app_note` serves the scaffolded object the + whole time. The template ships `src/objects/` only — deliberately, as every + scaffolder template in this repo does — but nothing the newcomer could reach + said so, and `pnpm dev` advertises the Console URL on every boot. + + Documentation only: a new "The Console" section in the generated `README.md` + naming the Console path, the consequence, and `src/apps/*.app.ts` as the + remedy. No change to what the scaffolder writes into `src/`. +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- 675ab57: **First-run polish:** a brand-new scaffold's very first `pnpm install` no longer reports two unmet peer dependencies (#10326). + + Reproduced on a clean scaffold from published `create-objectstack@17.1.0` — no lockfile, `node_modules` removed, nothing configured by the user — and again on the second scaffold path, `objectstack init`. Both printed the same two: + + ``` + ✕ unmet peer better-call + Installed: 1.4.0 + Wanted: + 1.3.7: + @better-auth/scim@1.7.0-rc.1 + + ✕ unmet peer better-sqlite3 + Installed: 13.0.3 + Wanted: + ^12.0.0: + better-auth@1.7.1 + ``` + + Nothing was broken — but it is the first screen a newcomer sees, and there is nothing they did to cause it or can do about it. + + **`better-sqlite3`: the pin is right and the upstream range is stale — so it is widened, not corrected.** better-auth 1.7.1 declares `better-sqlite3` as an **optional** peer at `^12.0.0`, and it governs exactly one configuration: a raw better-sqlite3 `Database` handed to better-auth's `database` option, which its Kysely dialect then drives. ObjectStack never takes that path — `AuthManager.createDatabaseConfig()` returns `createObjectQLAdapterFactory(dataEngine)`, and every `better-sqlite3` use under `plugin-auth` is knex's `client: 'better-sqlite3'` beneath ObjectQL. Measured anyway on the configuration the range *does* govern: better-auth 1.7.1 with `database: new Database(':memory:')`, running `getMigrations().runMigrations()`, `signUpEmail`, `signInEmail` and adapter `findOne`/`update`/`delete`, is green on **better-sqlite3 13.0.3** and byte-for-byte equivalent on **12.11.1**. The same probe with `Database.prototype.prepare` neutered fails, so that green is the driver's and not an unexercised path. Pinning our own `^13.0.3` declarations back to `^12` would downgrade a native module across the platform to satisfy a range measurement shows is simply behind. + + **`@better-auth/scim`: the rc pin stays, and one `better-call` copy is the correct tree.** `npm view @better-auth/scim dist-tags` reads `latest: '1.7.1'`, but stable 1.7.x ships the rc.2 whole-model rewrite, so adopting it is a separate migration rather than a version bump; the exact `1.7.0-rc.1` pin is deliberate. The rc peers an exact `better-call@1.3.7` while better-auth 1.7.1 depends on `1.4.0` — and a better-auth plugin has to share the **host's** better-call instance, so the single 1.4.0 copy every install already resolves is right, not a skew to repair. This declaration retires together with the rc pin. + + **What changed, and what deliberately did not.** Both remedies are pnpm `peerDependencyRules.allowedVersions` entries, scoped `>` so each widens exactly one declaration. They ship *inside* the scaffold — the bundled `pnpm-workspace.yaml` template and the one `objectstack init` renders — because a block in this repo's own workspace file does not travel with published packages. `allowedVersions` changes what pnpm **reports**, never what it resolves: measured on both scaffold paths, the lockfile is byte-identical with and without it (0 lines of diff), and no dependency version, range or resolution moved anywhere. This repo's own resolutions are untouched. + ## 17.1.0 ### Minor Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index c6db18aaec..bbb1b7bfab 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "17.1.0", + "version": "17.2.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 727fd2f092..6e47c1fa75 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/driver-memory +## 17.2.0 + +### Patch Changes + +- 6936d07: `engine.aggregate` honours a per-aggregation `filter` (#10576, the contract + half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but + marked experimental and enforced by nothing — is now live with SQL + `FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one + aggregation reads while sibling aggregations in the same call keep seeing + every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) + can finally reach the engine instead of being silently dropped (the #10413 + wrong-numbers defect on the ObjectQL analytics path). The + `StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) + gains the same optional `filter` on its aggregation entries so analytics + strategies can lower measure filters into it (#10413 phase 2 consumes this + seam next). + + Execution is the correct-first two-tier shape date bucketing and HAVING use: + the engine lowers filtered aggregations in memory for every driver (unknown + operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation + position; a group emptied by its filter answers the ruled empty-group values + — count/sum 0, avg/min/max null). No driver compiles conditional aggregation + natively today, so each native aggregate face (driver-sql — inherited by + driver-sqlite-wasm and Turso local —, the Turso remote transport, + driver-mongodb's pipeline builder, driver-memory's `performAggregation`) + refuses a directly-delivered per-aggregation filter with + `NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. + Aggregations without a `filter` are byte-identically unchanged, including + their native pushdown path. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index 5ed92bf491..52a872a036 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.1.0", + "version": "17.2.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 ee4817e470..cf446fbea2 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/driver-mongodb +## 17.2.0 + +### Patch Changes + +- 6936d07: `engine.aggregate` honours a per-aggregation `filter` (#10576, the contract + half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but + marked experimental and enforced by nothing — is now live with SQL + `FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one + aggregation reads while sibling aggregations in the same call keep seeing + every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) + can finally reach the engine instead of being silently dropped (the #10413 + wrong-numbers defect on the ObjectQL analytics path). The + `StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) + gains the same optional `filter` on its aggregation entries so analytics + strategies can lower measure filters into it (#10413 phase 2 consumes this + seam next). + + Execution is the correct-first two-tier shape date bucketing and HAVING use: + the engine lowers filtered aggregations in memory for every driver (unknown + operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation + position; a group emptied by its filter answers the ruled empty-group values + — count/sum 0, avg/min/max null). No driver compiles conditional aggregation + natively today, so each native aggregate face (driver-sql — inherited by + driver-sqlite-wasm and Turso local —, the Turso remote transport, + driver-mongodb's pipeline builder, driver-memory's `performAggregation`) + refuses a directly-delivered per-aggregation filter with + `NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. + Aggregations without a `filter` are byte-identically unchanged, including + their native pushdown path. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index 01391b204a..92cc662e6d 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.1.0", + "version": "17.2.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 5141ecb25d..fd53bf57b8 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,77 @@ # @objectstack/driver-sql +## 17.2.0 + +### Patch Changes + +- 6936d07: `engine.aggregate` honours a per-aggregation `filter` (#10576, the contract + half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but + marked experimental and enforced by nothing — is now live with SQL + `FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one + aggregation reads while sibling aggregations in the same call keep seeing + every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) + can finally reach the engine instead of being silently dropped (the #10413 + wrong-numbers defect on the ObjectQL analytics path). The + `StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) + gains the same optional `filter` on its aggregation entries so analytics + strategies can lower measure filters into it (#10413 phase 2 consumes this + seam next). + + Execution is the correct-first two-tier shape date bucketing and HAVING use: + the engine lowers filtered aggregations in memory for every driver (unknown + operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation + position; a group emptied by its filter answers the ruled empty-group values + — count/sum 0, avg/min/max null). No driver compiles conditional aggregation + natively today, so each native aggregate face (driver-sql — inherited by + driver-sqlite-wasm and Turso local —, the Turso remote transport, + driver-mongodb's pipeline builder, driver-memory's `performAggregation`) + refuses a directly-delivered per-aggregation filter with + `NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. + Aggregations without a `filter` are byte-identically unchanged, including + their native pushdown path. +- 46cfa5b: **Bug fix:** on Postgres, index and schema introspection now resolve tables the way the session does, instead of assuming the `public` schema (#9350). + + `introspectIndexes` pinned `n.nspname = 'public'` and `introspectSchema` pinned `table_schema = 'public'`. For a driver whose connection carries a `searchPath` pointing anywhere else, both returned **empty** — not an error, an empty result. Measured on a live Postgres 16: for a table carrying a primary key *and* a declared unique index, `introspectIndexes` returned `[]` and `introspectSchema` listed no tables at all. + + Empty does not read as "I could not see" downstream; it reads as "there are no indexes". `assertConflictTargetHonoured` turns that into a refusal, so an `upsert` against a perfectly well-indexed table would be rejected with *no PRIMARY KEY or UNIQUE index backs them* — and index-drift detection would propose creating indexes that already exist. + + - `introspectIndexes` now resolves the table with `to_regclass(?)` and reads `pg_index` by OID. That is the same resolution every other statement in the session performs — first match along `search_path` — and it removes an ambiguity a schema list would introduce, since two schemas on the path can hold the same table name and only one of them is the one a query reaches. + - `introspectSchema` now lists `table_schema = ANY (current_schemas(false))`. + + **No change for a default deployment.** With the default `search_path`, `current_schemas(false)` is exactly `{public}` and `to_regclass` resolves into `public`, so both queries return what they returned before. The behaviour only differs where the old queries returned nothing. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index ee6a6fa9b0..bcb6e0498b 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.1.0", + "version": "17.2.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 24621f7edf..48ddd93e20 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/driver-sqlite-wasm +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [46cfa5b] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/driver-sql@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index 4211f447eb..594b5bcb40 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.1.0", + "version": "17.2.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 4234e8c805..200e7566c9 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/driver-turso +## 17.2.0 + +### Patch Changes + +- 6936d07: `engine.aggregate` honours a per-aggregation `filter` (#10576, the contract + half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but + marked experimental and enforced by nothing — is now live with SQL + `FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one + aggregation reads while sibling aggregations in the same call keep seeing + every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) + can finally reach the engine instead of being silently dropped (the #10413 + wrong-numbers defect on the ObjectQL analytics path). The + `StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) + gains the same optional `filter` on its aggregation entries so analytics + strategies can lower measure filters into it (#10413 phase 2 consumes this + seam next). + + Execution is the correct-first two-tier shape date bucketing and HAVING use: + the engine lowers filtered aggregations in memory for every driver (unknown + operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation + position; a group emptied by its filter answers the ruled empty-group values + — count/sum 0, avg/min/max null). No driver compiles conditional aggregation + natively today, so each native aggregate face (driver-sql — inherited by + driver-sqlite-wasm and Turso local —, the Turso remote transport, + driver-mongodb's pipeline builder, driver-memory's `performAggregation`) + refuses a directly-delivered per-aggregation filter with + `NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. + Aggregations without a `filter` are byte-identically unchanged, including + their native pushdown path. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [46cfa5b] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/driver-sql@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index 121c153053..5321dbb087 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.1.0", + "version": "17.2.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 b7dd106749..7344aa2ef8 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,37 @@ # @objectstack/formula +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index 9e46f6653f..76724b47a7 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.1.0", + "version": "17.2.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 5f9b0a9191..91663cc113 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,222 @@ # @objectstack/lint +## 17.2.0 + +### Minor Changes + +- 78818ec: Report an unparseable source instead of scoring it CLEAN (#10653). + + Four validators parsed authored source with `ts.createSourceFile` and never read + `parseDiagnostics`. That call **cannot throw**, so a source with syntax errors + came back as a tree built by error recovery, got walked like any other, and + produced no findings — a source the validator could not read, reported as a + source with nothing to report. Two of the sites carried a `try/catch` around the + parse that never once ran. + + Each now reports what it could not read, as a finding the author receives rather + than as an exit — a publish-time validator is handed metadata by someone else, + so ending the process on their input is not its call. Four new advisory + (`warning`) rule ids, all additive: every finding these rules produce today they + still produce, including from a partially recovered tree. + + - `react-page-source-unparseable` — `kind:'react'` page source + (`validateReactPageProps`) + - `startup-source-unparseable` — plugin source (`findStartupRegistryVerdicts`) + - `hook-body-source-unparseable` — L2 hook body (`validateHookBodyWrites`) + - `action-body-source-unparseable` — L2 action body (`validateActionBodyWrites`) + + New exports: the four rule-id constants, plus `describeParseFailure`, + `PARSE_FAILURE_HINT` and the `SourceParseFailure` / `CheckedParse` / + `CheckedParseOptions` types. `ExtractedHookBodyWriteSet` gains an optional + `parseFailure`, so a consumer of the extractor can tell "wrote nothing" from + "could not be read" — the distinction that was missing. + + Nothing is removed or renamed, and no source that parses gains a finding. A + stack whose authored sources all parse lints exactly as before; one carrying a + source with a syntax error gains a warning that names the file, line and column + instead of silently skipping the checks. +- def0d3e: Runtime publish-gate findings for collection-resident write types (`object` / + `permission` / `book`) now key the top-level collection entry in + `issues[].path` / `advisories[].path` by NAME — + `objects.acme_invoice.sharingModel` — instead of by the gate's private + per-write snapshot index (`objects[417].sharingModel`), which no caller could + resolve: that index numbered an in-memory array a Studio / MCP / REST receiver + has never seen. Single-member write types keep their trivially-stable + positional form (`flows[0].nodes[1]…`), and nested positions inside one named + item (`objects.acme_invoice.indexes[1]`) stay positional — they index the + author's own document. An entry with no splice-safe name falls back to the + positional spelling. The accepted metadata set is unchanged; only the spelling + of the emitted finding `path` changes, and `RuntimeAuthoringIssueSchema.path`'s + description now states the convention. CLI (`os validate` / `os lint`) output + is unchanged — there the index resolves against the author's own config file. +- e2bb237: The SORT axis now asks the #8116 provenance question about a name the blanket + `SYSTEM_FIELDS` union told it not to flag — new rule `sort-field-unprovisioned` + (#10474), the twin of `searchable-field-unprovisioned` on the identical index + (#8404). + + `validate-sortable-fields` consulted the union and stopped there, so a list view + ordering by a registry-injected anchor on an ADR-0015 `external` object was + skipped in silence. The #8999 consumer census recorded that gap with the reason + that such an object never reaches the union branch at all — skip (2) was believed + to catch it. **That reason was measured wrong.** `declaredFieldTarget` returns + `null` on exactly one condition (`fields` missing, unreadable, or naming + nothing) and nothing in it tests `external`, so the shipped shape — a federated + object that declares a mapped field map, as `examples/app-showcase`'s + `showcase_ext_customer` does — is indexed like any other object and lands + squarely in the skip. The census ledger entry now carries the correction rather + than the inherited reason. + + Why the authoring gate is the only door available for it: both runtime doors on + this axis judge `formula` alone (`UNMATERIALIZED_SORT_TYPES`) — the REST ingress + `assertSortFieldsExist` (#6994) and the engine's `assertOrderByIsMaterializable` + (#7095). An injected anchor is a `datetime` or `lookup`, it *is* in `gate.known` + because the registry injected it into the served schema, and it is undotted, so + it clears every verdict and reaches the driver. Measured with a real `SqlDriver` + over better-sqlite3, the object declared exactly as the showcase declares it, + against a remote `customers` table carrying `[id, name, email, region, + lifetime_value]` and none of the seven injected anchors: + + ``` + orderBy name asc -> [c1,c2,c3] desc -> [c3,c2,c1] (a real column: reverses) + orderBy created_at asc -> [c1,c2,c3] desc -> [c1,c2,c3] asc === desc, 3 rows, no error + orderBy owner_id asc -> [c1,c2,c3] desc -> [c1,c2,c3] asc === desc, 3 rows, no error + ``` + + `asc` and `desc` byte-identical while the baseline reverses is what makes it a + dropped sort rather than a coincidence — the same signature this rule already + records for `formula`, reached by a second route, except that a formula sort is + refused at both doors and this one is not. A list view ordered by an anchor with + no storage answers `200` with the rows in the driver's arbitrary order, on the + view's first fetch and every fetch after it, which `limit`/`offset` then slice + into an arbitrary page. + + `warning`, never `error` and never gating (#4330's cost asymmetry, the call every + sibling makes): the remote schema is invisible to this pass, so the remote table + may genuinely carry a `created_at` of its own. Declaring that column — the first + remedy the shared hint prescribes — silences the finding, because + `unprovisionedInjectedColumnsFor` excludes an author-declared column of the same + name (#7859's security direction). The runtime publish gate sorts on severity, so + this lands as an advisory and refuses no write. + + Two deliberate narrowings, both pinned: + + - **Undotted names only** — the one place this axis departs from the SEARCH twin. + `resolveSearchFields` matches by exact string and drops a dotted entry like a + typo, but a dotted SORT name is refused by the ingress gate as its own verdict + (`400 INVALID_SORT`, loudly, on every fetch), so the silent degradation this + finding reports cannot happen there. Answering would give the SORT axis its own + dotted verdict, which is exactly the posture the rule shares with the FILTER + and PROJECTION axes (#4256 / #7532 / #7589) and declines to break. + - **`checkSortDeclaration`'s new anchor-index parameter is optional**, with the + same meaning `checkSearchableFieldList`'s carries: an out-of-repo caller that + never built the index keeps its pre-#10474 answers. Every in-repo caller passes + it. + + Also re-ruled, with fresh eyes and on evidence rather than inheritance: + `validate-translation-references` still correctly asks nothing. It reads the + union at exactly one site (the `fields.` orphan test), and the key it + decides about is derived from the *registered* metadata, into which the registry + injects the anchor on a federated object just as on a local one — so the key + resolves and the label renders. Warning there would flag a translation that + works. The blank-column consequence belongs to the surface that renders the + anchor (`validate-page-field-bindings`, #8340), not to the bundle that names it. +- adbcbfd: feat(lint): the two list-view field rules reach a standalone list view at the runtime publish gate — `view` writes are now judged by `validateSearchableFields` and `validateSortableFields` (#9313) + + An `active`-state `view` save through `saveMetaItem` (Studio, REST `/meta` item + CRUD, an MCP/AI author) is now refused with the existing 422 `invalid_metadata` + envelope when its list view declares a `sort` or `searchableFields` entry the + bound object cannot honor — an unknown field name, a virtual (`formula`) sort + target with no stored column to ORDER BY, or a search narrowing the #4254 + ingress gate would refuse on every toolbar search. Both rules already gated + `os validate` / `os build` / `os lint`; the runtime door — the only door a + Studio tenant or an MCP/AI author has — ran neither, and an author writing the + exact declaration these rules exist to refuse got it accepted. + + Two halves, because either alone is a silent no-op: the reference-integrity + suite's registry entry gains `runtimeTypes: ['view']`, and both rules' metadata + walks gain the SELF rung — a `views[]` entry that IS a flattened standalone + list overlay (`ViewMetadataSchema`'s list-overlay member: `viewKind: 'list'`, + no nested `config`), the shape a standalone list view takes on the wire and the + shape the gate snapshots as `views: [item]`. + + The suite dispatches per member on this door: a `view` snapshot reaches exactly + the two list-view field rules (`ReferenceIntegrityRule.runtimeTypes`, default + `['flow']`), never the members whose resolution universe the per-write snapshot + does not carry — `validateActionNameRefs` resolving against `stack.actions` + would otherwise refuse legitimate view writes. CLI behaviour is unchanged (the + commands run the full suite as before); `flow` snapshots keep every member. + Measured before crossing: 0 refusals and 0 advisories over 50 shipped + view-door bodies (11 containers + 39 console-shaped personalization overlays, + `sort[].id` decorations included) across four authoring lineages — a lower + bound, as every authored corpus is. Draft saves are untouched (D1), stored rows + keep being served (ADR-0087 asymmetry), and + `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to a loud log. +- f1b5ad3: feat(lint): a standalone ViewItem record's nested `config.sort` / `config.searchableFields` reach the runtime publish gate (#10001) + + An `active`-state `view` save through `saveMetaItem` (Studio, REST `/meta` + item CRUD, an MCP/AI author) whose body is a standalone ViewItem RECORD — + `ViewMetadataSchema`'s member 1, `{ name, object, viewKind: 'list', config }`, + the shape a Studio-saved view takes and the shape objectui's `updateView` + round-trips on every pin/reorder toggle — is now refused with the existing + 422 `invalid_metadata` envelope when its `config.sort` / `config.searchableFields` + declares a field the bound object cannot honor: an unknown name, a virtual + (`formula`) sort target with no stored column to ORDER BY, or a search + narrowing the #4254 ingress gate would refuse on every toolbar search. #9313 + closed the same gap for the flattened list overlay, one union member over; + the record's declarations live one level down, inside `config`, and were + judged by neither list-view field rule — so a record write carrying + `config.sort: [{ field: '' }]` published in silence and answered + `400 INVALID_SORT` (#6994/#7095) on the view's first fetch, every load. + + Walk-only, by design: #9313 already widened the reference-integrity suite + entry and exactly these two members onto `view` writes, so this change adds + the RECORD rung to both twin walks — recognised by the wire union's own + member discrimination (`viewKind: 'list'` AND a record-shaped `config`; the + flattened-overlay rung keeps its `no nested config` guard, a strict container + carries neither key, and a `form` record has no list-field surface), judged + against `listViewObject(config) ?? record.object` at path + `views[i].config.sort[…]` / `views[i].config.searchableFields[…]`. The + per-member granularity split is unchanged: no further suite member crosses + onto `view`. Measured before shipping: 0 refusals and 0 advisories over 39 + record-shaped console round-trip bodies (one per shipped list surface, + `config.sort[].id` decorations and `isPinned`/`sortOrder` riding along, the + shape `saveMetaItem` really stores) across the four shipped stacks — a lower + bound, as every authored corpus is. Draft saves are untouched (D1), stored + rows keep being served (ADR-0087 asymmetry), and + `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to a loud log. + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/sdui-parser@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 712f8c439d..608c5c8f57 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.1.0", + "version": "17.2.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 882977f1e0..28efcde494 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,108 @@ # @objectstack/plugin-mcp-server +## 17.2.0 + +### Minor Changes + +- 502dc6f: fix(mcp): the stdio MCP transport assembles its ExecutionContext with the shared assembler, and resolves localization (#7279) + + `resolveStdioExecutionContext` was the last hand-written `ExecutionContext` + assembly on the platform. #6216 converged the dispatcher, REST and share-link + sites onto `assembleExecutionContext`; this face was not in that card's + inventory, so it kept building the envelope field-by-field — and fell behind it + in two ways. + + | field | before | after | + |---|---|---| + | `tabPermissions` | dropped | **carried** | + | `timezone` / `locale` / `currency` | **resolved not at all** | **carried** (workspace values) | + | `accessToken` | absent by omission | **withheld by decision, on the record** | + | `positions` / `permissions` / `systemPermissions` / `userId` / `tenantId` / `email` / `posture` / `org_user_ids` / `accessible_org_ids` | carried | carried, unchanged | + + ## ⚠️ This changes output on the stdio surface — it is NOT a no-op + + **Formula fields evaluated during a stdio call move from `UTC` to the + workspace timezone.** The read path threads `ExecutionContext.timezone` into + `ExpressionEngine.evaluate`, which defaults to `UTC` when the context carries + none (`cel-engine.ts`: `ctx.timezone ?? 'UTC'`). Every stdio call previously + carried none. **A date-bucketing formula can therefore return a different + calendar day than it did before this change** — for a workspace whose timezone + is not UTC, that is the point: the same record read over REST and over stdio + now agree, where before they could disagree by a day. + + Two smaller shifts ride along: + + - **Denial messages localize.** A read refused by CRUD/FLS or RLS renders in the + workspace language (`userFacingDenialMessage`, `opCtx.context?.locale`) instead + of English. + - **Date-dependent driver generation on the write doors** (autonumber + `{YYYYMMDD}` tokens) resolves its calendar day from the workspace timezone. + `buildDriverOptions`' `hasTz` gate (`execCtx?.timezone !== undefined`) is one + of the few places where a field's ABSENCE is a meaningful state, and a stdio + call crosses it for the first time. Pinned in both directions by + `packages/objectql/src/engine-timezone-presence-gate.test.ts`. + + If a deployment's workspace timezone is unset, `resolveLocalizationContext` + falls back to `UTC` / `en-US` — the values this face effectively used before — + and nothing changes for it. + + ## `accessToken` is withheld, deliberately, and now says so + + The stdio face's credential is a **long-lived `osk_` API key** read from + `OS_MCP_STDIO_API_KEY`, not a session bearer. `ExecutionContext.accessToken` is + a **published hook surface** (`session.accessToken`, `spec/data/hook.zod.ts`), + so handing every `beforeFind`/`afterFind` a credential with far longer life than + the session token that surface was designed around is a product decision nobody + has made. This face passes `accessToken: undefined` with the reason written + down, matching the REST precedent. (It is also unreachable here: the value is + assigned only inside `resolve-authz-context.ts`'s + `if (!userId && typeof input.getSession === 'function')` branch, and this call + passes no `getSession`. The test injects a sentinel token at the seam anyway, so + the *decision* is pinned rather than the accident.) + + ## Cost, and where it is paid + + `resolveStdioExecutionContext` still re-resolves the **identity** on every call, + deliberately — ADR-0101 D1, so a revoked key stops working on the next one. + Localization is resolved **once, in `start()`**, and reused: the key's tenant + cannot change mid-session, and up to three settings reads per MCP call on a + long-lived process is not acceptable steady state. + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index ba8086ece2..e6f39d825e 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.1.0", + "version": "17.2.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 78c5ea0d8e..f62bedadd2 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/metadata-core +## 17.2.0 + +### Patch Changes + +- 26f3588: **Fix:** the REST `/meta` doors now decide **organization scope on the folded type**, never on the raw URL spelling (#10340). + + Storage folds `/meta/:type` through `META_URL_TO_SINGULAR` — the complete spelling map — while the doors' scope predicate (`declaresOrgOverride`) tolerates only the manifest-collection spellings. For the two registry-derived spellings, `translations` and `email_templates`, the doors therefore read and wrote **env-wide** where the singular twin was org-scoped: an org-active author's `PUT /meta/translations/:name` landed an env-wide row their own org-scoped read then shadowed (persisted, receipted as live, served by nothing), and `GET` under one spelling answered a different partition than the other — one item, two namespaces, addressed by spelling (#4432 / #7894's defect one layer down). + + - All nine `/meta` org-scope call sites (list, single read, layers view, compound read, save, compound save, delete, publish, rollback) fold the segment through `canonicalMetaUrlType` **before** calling `organizationIdForMetaRead` / `organizationIdForMetaWrite`, exactly as `metadata-url-spelling.ts` mandates: folding happens at the boundary and only there. + - The `GET /meta/:type/:name/published` code-store fallback folds too — the smaller second site of the same class: it reads a registry keyed by canonical types, so a recognised plural of a code-published item answered 404 while the singular answered 200. + - **Deliberately unchanged:** `GET /meta/_drafts` still applies no fold (it filters by the draft row's *stored* type, which is canonical because the protocol folds on save), the request `type` handed to the protocol stays the raw segment (the protocol owns its own fold), and `declaresOrgOverride` does **not** absorb the URL map — a predicate below the boundary consuming the URL spelling contract is the repair #7894 forbids. `@objectstack/metadata-core` changes are documentation and pins only: the predicate's header no longer claims parity with the protocol's normalization (measured false), and new tests pin both the composed fold→predicate contract and the predicate's deliberate limit. + + No stored rows move: rows previously minted env-wide through a plural spelling stay env-wide and keep serving org-less callers (and org-active callers until an org overlay exists), which is the same layering the singular spelling always had. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index 8ed015443a..ebd28eaffc 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "17.1.0", + "version": "17.2.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 7bed9aae23..a87aaa8927 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,12 @@ # @objectstack/metadata-fs +## 17.2.0 + +### Patch Changes + +- Updated dependencies [26f3588] + - @objectstack/metadata-core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index 70c50c2b51..388443c44d 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "17.1.0", + "version": "17.2.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 15da6b77fe..137eb3a1d5 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,305 @@ # @objectstack/metadata-protocol +## 17.2.0 + +### Minor Changes + +- 5886ee6: Stop issuing two DB queries for questions already answered earlier in the same + request (#10757). One authenticated `GET /data/:object?$top=1` measured **24 DB + queries before, 23 after** — **22** when the caller opts out of the count. + Measured with `X-OS-Debug-Timing: json` on `pnpm dev:crm`, whose `Server-Timing` + carries `db;dur=…;desc="N queries"`. + + **`$count=false` now skips the COUNT query** (`@objectstack/metadata-protocol`). + The parameter has been declared (`ODataQuerySchema.$count`), aliased on the wire + (`$count` → `count`), reserved out of the implicit-field-filter bucket, + arity-checked and boolean-coerced for a long time — and then deleted unread, so + every paginated list ran `engine.count()` whether or not the caller wanted a + total. It is honoured now: + + ``` + GET /data/task?$top=25 → { records, total, hasMore } (unchanged) + GET /data/task?$top=25&$count=true → { records, total, hasMore } (unchanged) + GET /data/task?$top=25&$count=false → { records, hasMore } (no COUNT query) + ``` + + Read the shape of that carefully before adopting it: + + - **Only an explicit `false` opts out.** An ABSENT `$count` still counts and + still reports `total`. OData reads absent as "omit the count", and taking that + reading here would silently strip `total` from every existing caller — none of + them send the parameter, all of them read the number. The asymmetry is + deliberate and pinned by tests. + - **`total` is OMITTED, never estimated.** `FindDataResponse.total` is declared + optional ("if requested"), so absent is the declared shape for "not + requested". A caller that opted out and then reads `total` gets `undefined`, + not a plausible-looking guess — guard the read (`total ?? undefined`) or do + not send `$count=false`. + - **`hasMore` is still answered**, from the page alone: a full page means there + may be more. Same page-local rule the `$search` path already uses. + + **A find and its COUNT resolve permission sets once, not twice** + (`@objectstack/plugin-security`). `findData` answers a paginated list with two + engine operations, and the security middleware runs on both; each pass re-read + `sys_permission_set` for the same context with identical bindings. The + resolution is now memoized per execution context — a `WeakMap` keyed on the + context object, which is built once per request and collected with it, so + nothing outlives the caller it was resolved for — and **retired by any write**: + a process-wide epoch is bumped on every `insert`/`update`/`delete` the engine + middleware sees, ahead of the `isSystem` bypass so a seeder, a package publish + or an auto-org-admin grant invalidates too. A context whose grants are rewritten + in place re-resolves as well (the memo key covers `positions`, `permissions`, + `principalKind` and the presence of `userId`). No authorization answer is reused + across a write, across a context, or across a request. + + Not a fix for the whole cost: the remaining ~22 queries per authenticated + request are session resolution, grant resolution, localization and metadata + reads that repeat on every request. Removing those needs cross-request caching + with an invalidation design, which is deliberately not in this change. +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + +- 16cef97: Declare `outcome: 'published' | 'refused' | 'nothing_to_publish'` as a required + key on the `publishPackageDrafts` response (#10462) — the first-class + discriminant for WHICH exit answered, the fact `success` compresses into one + boolean. Before this field, a publish with nothing to promote and a genuine + refusal (pre-flight violation or ADR-0067 D2 rollback) were indistinguishable: + both answer `success: false` with `publishedCount: 0` on a 200, and the no-op + left no trace at all — an AI consumer graded the no-op as "refused and rolled + back" and burned two repair rounds on artifacts that were already correct + (cloud#1488; cloud#1492's patch discriminates on `failed.length > 0`, an + invariant the producer never stated). + + The producer invariants, now stated and pinned in the conformance suites, both + directions of each: `outcome === 'refused'` ⟺ `failed.length > 0`; + `outcome === 'nothing_to_publish'` ⟺ + `published.length === 0 && failed.length === 0`; + `success === (outcome === 'published')`. `success` keeps its exact pre-#10462 + value on every exit — a no-op still answers `success: false` — so consumers + reading only `success` see no change, and cloud#1492's `failed.length` + discrimination stays valid during its convergence onto `outcome`. The no-op + exit additionally logs one `info` line naming the package and both facts + (nothing pending, nothing refused), so that exit is no longer traceless. + + Additive for response consumers. A custom protocol implementation that serves + `publishPackageDrafts` must now emit `outcome` on every return — + `PublishPackageDraftsResponseSchema` declares it required, and the conformance + suites treat a producer return without it as a drifted seam. + +### Patch Changes + +- 02d56b4: fix(metadata-protocol): `getMetaDiagnostics` refuses an unrecognised `type` spelling with the producer's 400 instead of answering "scanned 1 type, 0 problems" (#8924) + + + + This is a **narrowing that makes an already-classified 400 reach the caller**. + `GET /api/v1/meta/diagnostics?type=` + (and the SDK method `client.meta.getDiagnostics({ type })`) used to answer + `200 {"entries":[],"total":0,"scannedTypes":1,"scannedItems":0,"stats":{}}` — + "scanned 1 type, no issues" — for a spelling every sibling `/meta` door + refuses with a 400 that names both accepted spellings. The producer had + already classified the mistake (`status: 400`, `code: 'INVALID_REQUEST'`, + raised by `canonicalizeMetaRequestType` inside `getMetaItems`); the + diagnostics sweep's per-type `catch` swallowed the verdict into a benign + skip, and `scannedTypes: 1` then published a sweep that scanned nothing as + coverage. Maintainer ruling 2026-08-20: rethrow the 400 the same way #8855's + fix rethrows the 503. + + **Measured on a booted kernel (real HTTP), before → after:** + + ``` + GET /api/v1/meta/diagnostics?type=fieldes 200 {"scannedTypes":1,"stats":{}} → 400 [invalid_request] "… Address it as 'field' or 'fields'." + GET /api/v1/meta/diagnostics?type=fields 200 (recognised plural) → 200 unchanged + GET /api/v1/meta/fieldes 400 → 400 unchanged + ``` + + What is unchanged: recognised plurals (`fields`, `views`, …) still fold and + answer; a name that is a plural of nothing (`fieldz`) still answers an honest + `count: 0` entry; a genuine, unclassified listing failure still skips that + one type instead of failing the sweep; the whole-corpus sweep (no `?type=`) + cannot produce the refusal at all — its target set comes canonical out of the + registry. A caller that treated the old `200`-with-empty-stats answer as + "clean" now hears the refusal that names the accepted spellings. +- d728325: Per-item publish (`POST /api/v1/meta/:type/:name/publish`) now re-binds runtime consumers + and finds drafts authored env-wide — the two things the package-scoped publish door + already did. + + **A metadata publish now announces `metadata:reloaded` on BOTH doors.** The event that + tells boot-cached consumers to re-read had two announcers: the dev-artifact watcher and + the runtime dispatcher after `POST /packages/:id/publish-drafts`. Publishing item by item + — what AI authoring and the item-level Studio doors do — announced nothing, so a flow + published while the server ran stayed `state='active'` and completely inert (no trigger + bound, no execution) until the kernel was rebuilt. `publishMetaItem` now notifies its host + through a new `onMetaItemPublished` seam and `ObjectQLPlugin` turns that into the kernel + announce, so `service-automation`'s flow re-bind, the authored hook/action re-sync, + declarative connectors and authored translations all catch up without a restart. The + announce is awaited, so the publish's own 2xx means the re-bind was attempted; a + subscriber failure is logged and never fails the publish. The batch door is unchanged — + it keeps its single per-publish announce rather than gaining one per promoted draft. + + **A per-item publish now resolves the draft's own org scope.** For the types the registry + declares `allowOrgOverride: true` (`view`, `dashboard`, `report`, `translation`, + `email_template`) the REST seam threads the session's active organization into the + publish, while package/AI authoring writes the draft env-wide — so the strict org lookup + matched nothing and answered `404 [no_draft] … nothing to publish` over a draft the + console's pending-changes banner was listing and the batch button published fine. The + per-item door now discovers the draft's scope the way `publishPackageDrafts` has since + #3115, with the ADR-0005 precedence (an org holding its own draft publishes that one) and + the same `NO_DRAFT` refusal when no scope holds a draft. +- 0c24898: fix: a package publishes as a self-consistent unit — `publishPackageDrafts` judges each draft against the batch's own pending declarations + + The batch publish door built the author-time validation context from + `engine.registry` alone, i.e. the ALREADY-LIVE universe. A draft is not in that + registry, and the batch's own promotions do not put it there either: the + registry write-through runs in Phase 2, after the Phase-1 transaction that gates + and promotes every draft. So while a batch was being judged, no member of it was + visible to any other member — in any order. + + Measured consequence: a package shipping `dataset/x` together with a `dashboard` + whose widget binds `x` could NEVER publish. `validateWidgetBindings` raises + `widget-dataset-unknown` at `severity: 'error'`, which refuses the promotion, + and the batch being all-or-nothing rolls the whole package back. Renaming the + dataset could not help, and neither could re-ordering the items. + + `publishPackageDrafts` now reads its own pending drafts once, before any + promotion, and folds them into all four context collections the closure carries + (`objects`, `permissions`, `books`, `datasets`) — pending declarations replace a + live one of the same name, never sit beside it. A binding that resolves to + neither the batch nor the live universe is still refused exactly as before. +- 0ab81d1: fix(metadata-protocol): the seed/API tenancy repair now records each applied run in `sys_migration`, so "was my data rewritten, and when" survives the container being replaced (#9451) + + `backfillSeedTenancy` (#8686) is the platform's only row-rewriting repair that + runs unattended: it stamps `organization_id` onto business rows, merges one + autonumber counter and deletes another. It persisted nothing about having done + so. The only evidence was one `logger.info` line, and the healthy path is silent + by design — so once that line had scrolled, a silent boot and a boot that + rewrote data were indistinguishable. The operator most likely to need the record + (a fresh install, repaired during the first admin sign-up, where nobody is + reading server stdout) was the one least likely to have captured it. + + An `applied` run now writes one row into the **existing** `sys_migration` + deployment ledger — the face that already answers "has this deployment run this + data migration", and is already written at boot by the ADR-0104 attestation + path: + + ```sql + SELECT last_run_at, advisory, details FROM sys_migration + WHERE id = 'seed-tenancy-backfill'; + ``` + + `details` carries the run's status, the objects stamped, the organization + adopted and the identifiers that could not be adopted because they were already + minted on both sides of the split. + + Deliberately narrow: + + - **`applied` only.** `no-split` stays silent — a row per healthy boot would be + a ledger of non-events. + - **`verified_at: null`, `blocking: 0`, always.** This repair runs no self-check + and gates no consumer, so it claims no certificate; the collision count goes + to `advisory`, which never gates. Every reader of this ledger looks a row up + by `id`, so the new id cannot reach another migration's gate. + - **Best-effort, and loud when it fails.** A boot is never failed by + bookkeeping (2026-08-15 ruling), so a failed receipt write is reported at + `error` — naming that the rows *were* rewritten, that the repair is not + retried, and what to do — rather than rethrown. + - **No new schema, no new authoring surface, no new dependency.** The row is + written against the `@objectstack/spec/system` contract that + `metadata-protocol` already depends on. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [78818ec] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [e2bb237] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [adbcbfd] +- Updated dependencies [f1b5ad3] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/lint@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/metadata@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index 1e2f723860..8aa515de3e 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "17.1.0", + "version": "17.2.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 2a6f27a542..61a079a8eb 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/metadata +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/types@17.2.0 + - @objectstack/metadata-fs@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index 92f53f7c11..683c11f467 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.1.0", + "version": "17.2.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 a4935c0231..c82824df3b 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,239 @@ # @objectstack/objectql +## 17.2.0 + +### Minor Changes + +- 6936d07: `engine.aggregate` honours a per-aggregation `filter` (#10576, the contract + half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but + marked experimental and enforced by nothing — is now live with SQL + `FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one + aggregation reads while sibling aggregations in the same call keep seeing + every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) + can finally reach the engine instead of being silently dropped (the #10413 + wrong-numbers defect on the ObjectQL analytics path). The + `StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) + gains the same optional `filter` on its aggregation entries so analytics + strategies can lower measure filters into it (#10413 phase 2 consumes this + seam next). + + Execution is the correct-first two-tier shape date bucketing and HAVING use: + the engine lowers filtered aggregations in memory for every driver (unknown + operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation + position; a group emptied by its filter answers the ruled empty-group values + — count/sum 0, avg/min/max null). No driver compiles conditional aggregation + natively today, so each native aggregate face (driver-sql — inherited by + driver-sqlite-wasm and Turso local —, the Turso remote transport, + driver-mongodb's pipeline builder, driver-memory's `performAggregation`) + refuses a directly-delivered per-aggregation filter with + `NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. + Aggregations without a `filter` are byte-identically unchanged, including + their native pushdown path. +- 2570ab0: The `__search` companion is no longer provisioned or backfilled on objects whose only companion source is the primary key (#10290) + + `resolveSearchCompanionSources` resolves the companion's source through + ADR-0079's `resolveDisplayField`. That derivation ends at "first title-eligible + field by declaration order", and on a table whose only text column IS its + primary key — system tables, junction tables, append-only logs — it lands on + `id`. `id` is `type: 'text'`, not hidden and carries no `requiredPermissions`, + so it passed the eligibility gate: `provisionSearchCompanion` declared a + `__search` column on those objects and `plugin-pinyin-search`'s backfill walked + them at every boot. + + That work is doomed by construction rather than merely unlikely. Both writers — + the `beforeInsert`/`beforeUpdate` stamp and the boot backfill — gate on + `containsCJK(row[source])`, and a platform-generated primary key is ASCII by + construction, so the predicate can never be true. Measured on a real + `bootStack` of `examples/app-showcase`: **20 of the 66 objects** the backfill + enumerated were in this state, walking whole platform tables to compute nothing + — `sys_secret`, `sys_oauth_access_token` and `sys_jwks` among them. + + `resolveSearchCompanionSources` now returns `[]` when the resolved display + field is the record's primary key, and `isPrimaryKeyField` is exported as the + named judgement behind it. + + **Keyed on the field's ROLE, not on "resolved by fallback".** The registry's + materialization seam runs `provisionPrimary(schema, { synthesize: false })` + before this module — a contractual order — and that pass writes `nameField: + 'id'` onto the document, so by the time provisioning asks, a derived fallback + and an author's explicit pointer are byte-identical. The role is readable from + the name because that is where the platform keeps it: the driver provisions + `id` on every physical table unconditionally and there is no per-field + `primaryKey` marker in the spec, which is why `isPreservableUnderAudit` already + keys on `SystemFieldName.ID` for the same reason. `_id` is refused as the + alternate spelling of the same address. + + **This interprets ADR-0079, it does not amend it.** The title contract is + untouched: `resolveDisplayField` still resolves `id`, `provisionPrimary` still + designates it, and `resolveRecordDisplayName` still renders the `Record #` + floor. Only the search normalizer declines to take its input from there — the + same distinction #4483 drew one seam over on the READ path, where the display + field's job in the `$search` auto-default is to ORDER the set and never to + ADMIT a field the exclusions already rejected (`SEARCH_AUTO_EXCLUDED_FIELDS` + names `id` and `_id`). + + **What does not change.** Existing permanently-NULL `__search` columns on + already-migrated tables stay: ADR-0045 migrations are additive and dropping a + physical column is a separate decision. Those deployments still stop walking — + the backfill skips an object whose sources resolve empty even when its schema + still declares the column. Objects with a real name/title field are unaffected: + provisioning, write-time stamping and the query-time `$or` clause all behave + exactly as before, including when the object also declares an `id` field and + when its display field is a plain text column that is not named `name`/`title`. +- 8012960: `lifecycle.ttl` now accepts an `onlyWhen` row filter mirroring `retention.onlyWhen`, and the shared `onlyWhen` value union gains the platform's canonical null predicate `{$null: boolean}` (on both blocks). A `transient` object that interleaves live rows with terminal audit tombstones can now spare rows defined by a value's absence — e.g. `ttl: { field: 'expires_at', expireAfter: '1d', onlyWhen: { revoked_at: { $null: true } } }` — instead of the TTL reaping backdated tombstones first. The LifecycleService Reaper passes `ttl.onlyWhen` into the same reap scope `retention.onlyWhen` already rides; declaring `ttl.onlyWhen` together with rotation storage or archive is refused at parse time, mirroring retention's guards. + +### Patch Changes + +- 530c1df: **Behaviour change:** a `lifecycle` that declares both `ttl` and `archive` now + has its **`ttl` enforced** — the Archiver selects the rows it moves by the + declared TTL cutoff (`ttl.field` past `ttl.expireAfter`) instead of by + `created_at` age (#10347). + + That pair has always parsed — ADR-0057 §3.5 is satisfied because `ttl` is a + bounding policy, and the `archive.after === retention.maxAge` refine only fires + when `retention` is present — but it did nothing: `LifecycleService.reapObject` + returns into `archiveObject` before its `ttl` branch is reachable, so no reap on + `ttl.field` ever ran and the Archiver copied and hot-deleted by `created_at` age + alone. Declared, not enforced. What the author wrote is now what executes; they + no longer have to discover that the two keys cannot usefully be written + together. + + **Lifecycles that declare `archive` without `ttl` are unaffected** — they keep + selecting rows by `created_at` past `archive.after`, unchanged. Every + archive-declaring object shipped with the platform (`sys_audit_log`, + `sys_metadata_audit`) is that shape, so no bundled object changes behaviour. + + Two details of the new selection, both deliberate: + + - A row whose `ttl.field` is **null or absent is retained, not archived**. `$lt` + is a positive comparison and a value that is not there satisfies none of them + (the platform-wide null answer settled in #5298/#5299), which is also the + right reading: a row with no expiry stamp has not been given one, and treating + "absent" as "expired at the epoch" would archive exactly the rows whose expiry + the author has not yet decided. + - The cold-side `archive.keep` prune is unchanged. It bounds how long **archived** + rows survive in cold storage, not which hot rows are due, and it still measures + from `created_at` under either policy. + + If you declare `retention` beside `ttl` and `archive`, the TTL cutoff is what + selects: the age window no longer separately bounds the hot store for that + triple. Whether the Archiver should honour both windows is a separate open + question, filed as #10527 rather than decided here. +- d23e3a0: **Waste removed:** the lifecycle dangling-reference audit no longer asks a federated (ADR-0015 `external`) remote for platform anchor columns that were never provisioned on it (#8414). + + `applySystemFields` injects `organization_id`, `owner_id`, `owning_business_unit_id` and the audit `*_by` lookups into every registered object, federated ones included — that is deliberate (#7865, direction B). `Engine.syncObjectSchema` then issues no DDL for a federated object, because the remote database owns its schema. So those five reference columns existed in the registered schema and nowhere else, and `auditDanglingReferences` — which enumerated reference fields off `fields` alone — projected all of them onto the remote table. Measured on a real boot of `examples/app-showcase`, against a `customers` table whose real columns are `id, name, email, region, lifetime_value`: + + ``` + select `id`, `organization_id`, `created_by`, `updated_by`, `owner_id`, `owning_business_unit_id` from `customers` limit ? + select * from `customers` limit ? + ``` + + The first statement cannot compile (`no such column` — a backtick-quoted identifier does not take SQLite's double-quote literal fallback, and Postgres/MySQL raise their own error); `SqlDriver.find`'s unknown-column recovery caught it and retried `select *`, fetching up to 500 whole rows to audit columns that cannot exist — once per federated object, every lifecycle sweep interval, each pass also emitting a #4363 non-deterministic-paging warning. **No answer was ever wrong**; the pass was pure waste, and it was being absorbed by a safety net rather than by a design. + + The enumerator now consults `unprovisionedInjectedColumns` (`@objectstack/spec/data`, the #7865 provenance derivation) and skips columns that are the platform's own injected anchor on an object the platform provisions no storage for. + + **This reads provenance, not `external != null`.** A federated object that declares a real remote `organization_id` — or any other anchor name — keeps its audit on that column: the author's definition is not byte-identical to the shipped one, so provenance answers `'author'` and nothing is withheld. Objects the platform provisions storage for are untouched: the derivation returns an empty set for them, so an ordinary object is still swept with its full column set. + + Two consequences worth knowing: + + - A federated object left with **no real reference column** is no longer read at all, and is deliberately not filed in `unscannedObjects` — a column that was never provisioned stores no reference, so its absence from `dangling` is proven, not assumed. A federated object that declares a real reference column is still opened and audited on it. + - `AuditableObject` now carries an index signature. The port was already being handed the whole registered document (the engine passes `SchemaRegistry.getAllObjects()` straight through); the type now says so, because the provenance derivation reads the injection plan's inputs off it. Hand-written doubles carrying only `name`/`fields` still satisfy the type and behave exactly as before. + + The card also named `backfillSearchCompanion` (`@objectstack/plugin-pinyin-search`) for `select `id`, `name`, `__search` from `customers``. **That statement is already gone and this release changes no code for it:** #9469 stopped `provisionSearchCompanion` from declaring `__search` on a federated object, so the backfill's existing `if (!schema.fields[SEARCH_COMPANION_FIELD]) continue` early-out drops those objects before enumerating anything. A second federation-aware guard inside the backfill would have been redundant, and — spelled as "skip external objects" — would have wrongly withheld the companion from a federated object whose author declares a real remote `__search`. The precondition is now pinned on a real boot instead. +- f3a8134: Apply a `formula` field's declared `scale` when the formula is evaluated + (#10280). `Field.formula({ scale: 2 })` was accepted and then ignored: a + percentage formula such as `(record.num_responses * 100.0) / record.num_sent` + **returned** `41.666666666666664`, so the API response — and the record page + rendered from it — carried all fifteen digits despite the declaration. + + The value is now rounded where it is produced, in the engine's formula + evaluation, so all three surfaces that materialize a formula inherit it: list + reads, single-record reads, and the record a write responds with. + + - **Rounding is `Number(v.toFixed(scale))`** — round-half-away-from-zero, the + same arithmetic the console's client-side computed columns use. Negatives + round away from zero: `-1.5` at `scale: 0` is `-2`, not `-1`. + - **A formula declaring no `scale` is unchanged** and keeps full precision. + - **Non-numeric results are untouched** — a formula returning a string, + boolean or `null` is returned as-is. + - A formula value is **returned, never stored** — it is virtual and has no + column. Rounding it at the producer is what makes an app's own copy of that + result writable into a stored `DECIMAL(10, 2)`-style field, which previously + failed that field's decimal validation. + + Unchanged: `scale` on a **caller-supplied** number (`Field.number`, + `Field.currency`, …) is still enforced by **rejection** (`max_scale`), never by + rounding. A value someone sent has an author to refuse; a platform-computed + formula result does not. +- d728325: Per-item publish (`POST /api/v1/meta/:type/:name/publish`) now re-binds runtime consumers + and finds drafts authored env-wide — the two things the package-scoped publish door + already did. + + **A metadata publish now announces `metadata:reloaded` on BOTH doors.** The event that + tells boot-cached consumers to re-read had two announcers: the dev-artifact watcher and + the runtime dispatcher after `POST /packages/:id/publish-drafts`. Publishing item by item + — what AI authoring and the item-level Studio doors do — announced nothing, so a flow + published while the server ran stayed `state='active'` and completely inert (no trigger + bound, no execution) until the kernel was rebuilt. `publishMetaItem` now notifies its host + through a new `onMetaItemPublished` seam and `ObjectQLPlugin` turns that into the kernel + announce, so `service-automation`'s flow re-bind, the authored hook/action re-sync, + declarative connectors and authored translations all catch up without a restart. The + announce is awaited, so the publish's own 2xx means the re-bind was attempted; a + subscriber failure is logged and never fails the publish. The batch door is unchanged — + it keeps its single per-publish announce rather than gaining one per promoted draft. + + **A per-item publish now resolves the draft's own org scope.** For the types the registry + declares `allowOrgOverride: true` (`view`, `dashboard`, `report`, `translation`, + `email_template`) the REST seam threads the session's active organization into the + publish, while package/AI authoring writes the draft env-wide — so the strict org lookup + matched nothing and answered `404 [no_draft] … nothing to publish` over a draft the + console's pending-changes banner was listing and the batch button published fine. The + per-item door now discovers the draft's scope the way `publishPackageDrafts` has since + #3115, with the ADR-0005 precedence (an org holding its own draft publishes that one) and + the same `NO_DRAFT` refusal when no scope holds a draft. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [5886ee6] +- Updated dependencies [02d56b4] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [0c24898] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/metadata-protocol@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/metadata@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index 7d1eebb0eb..3c051d3c5f 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.1.0", + "version": "17.2.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 c6bb4fa4b3..6aa7af859f 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/observability +## 17.2.0 + +### Minor Changes + +- 914c413: fix(observability): **BREAKING** — `http_request_errors_total` is retired (ADR-0049 enforce-or-remove, #9834) + + **⛔ If you have a Grafana panel, an alert rule or a recording rule keyed on + `http_request_errors_total`, it will read a FLAT ZERO after this upgrade.** That + zero is the removal, not a healthy server, and it is the one way this change can + hurt you — nothing throws, nothing warns, the series simply stops receiving + samples. Rewrite the query before you deploy. + + Maintainer ruling 2026-08-20: **RETIRE**. The name was declared in `SEMCONV` as + part of a stable namespace *"so hosts can wire alerts/dashboards against it"*, + but the only emitter was `@objectstack/runtime`'s `instrumentRouteHandler`, + applied only by the dispatcher's own route Proxy — so the series never saw + auth's `getRawApp()` mount, the REST data API via `RouteManager`, or any other + inbound surface. Its two siblings in the same HTTP family moved to the + `IHttpServer.afterResponse` transport seam (`http_requests_total`, #9650/#9835; + `http_request_duration_ms`, #9834/#10004) and this one could not follow: + `HttpResponseObservation` carries `{method, routePattern, status, elapsedMs}` + and **no throw signal of any kind**, so every transport-side shape would have + counted a *different* population rather than the same one more widely. + + Migration (FROM → TO): + + | Wrote | Write instead | + |---|---| + | `rate(http_request_errors_total[5m])` in a panel or alert | `rate(http_requests_total{status=~"5.."}[5m])` — emitted by the transport, so it covers every inbound surface instead of the dispatcher's routes only | + | `sum by (route) (http_request_errors_total)` | `sum by (route) (http_requests_total{status=~"5.."})` | + | `SEMCONV.httpRequestErrorsTotal` / `RUNTIME_METRICS.httpRequestErrorsTotal` in host code | Delete the read. Both members are gone; `tsc` reports the missing property at the read site. | + + One-line fix: replace the metric name with `http_requests_total{status=~"5.."}`. + + + + **The replacement is wider, not merely different.** The retired counter was + divergent from a 5xx rate in *both* directions, measured: the dispatcher answers + its own errors through `errorResponseBase`, which sets a status and does **not** + re-throw — so the counter **missed** those — while its `catch` incremented + unconditionally, so a **thrown 4xx WAS counted** as an error. And + `http_requests_total` already carries a `status` label, so a status-class error + counter was fully derivable from data the transport already publishes. Prove the + new query wider rather than merely non-empty: make an auth route or a REST + data-API route answer 5xx and confirm it moves, where the retired counter would + not have moved at all. + + **If what you were actually alerting on was "a handler threw rather than + returning an error envelope"** — the one signal this counter uniquely carried — + that is the `errorReporter`, not a metric. Wire an `ErrorReporter` adapter + (Sentry / Datadog / your own); it still fires on every 5xx throw and is + untouched by this change. + + What is NOT removed: `http_requests_total`, `http_request_duration_ms`, + request-id propagation, the 5xx error reporter, and the + `res.__obsRecordedError` side channel that carries a swallowed error to it. The + dispatcher still instruments every route it mounts; it just no longer publishes + a fourth series whose name promised more coverage than it had. + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index 319a791d9b..3e24b0901e 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.1.0", + "version": "17.2.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 8731f4ae7a..df04982f9a 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,132 @@ # @objectstack/platform-objects +## 17.2.0 + +### Minor Changes + +- dccbcec: Declare an ADR-0057 lifecycle policy on `sys_session` (#7826): the object is + now `class: 'transient'` with + `ttl: { field: 'expires_at', expireAfter: '1d', onlyWhen: { revoked_at: { $null: true } } }`. + + **Ordinary expired sessions are now reaped** by the LifecycleService Reaper one + day after `expires_at` passes — the same window `sys_device_code` uses. Until + now nothing swept this table: better-auth's only expiry-driven collector fires + inside `GET /get-session`, so it can never reach a row whose cookie is never + presented again, and an abandoned session was effectively immortal. + + **Revoked tombstones are deliberately spared.** The `onlyWhen` filter (#10165) + is load-bearing, not defensive: the #7732 revocation write backdates + `expires_at` to `now - 1000` and clears nothing, so an ADR-0069 D4 audit + tombstone looks *maximally* expired — a TTL on `expires_at` without the filter + would reap the audit trail first and hardest. + + Deliberate, known consequence: because tombstones are spared entirely, + `sys_session` still grows without bound on the revoked arm. How long a + revoked-session tombstone should be retained is compliance / audit-trail + policy and is not settled here. + +### Patch Changes + +- 8f04d9a: Correct a false vendor claim in the `organization/add-member` source comments: + `teamId` has **no** active-team fallback (#10532). Two comments — the + `sys_member` `add_member` action metadata (the origin) and the + `organization-add-member.ts` module header that cited it as authority — stated + that "organizationId/teamId default to the caller's active org/team when + omitted". Measured on the installed better-auth 1.7.1 + (`dist/plugins/organization/routes/crud-members.mjs`, inside `addMember`), only + the organization half is true: + + ```js + const orgId = ctx.body.organizationId || session?.session.activeOrganizationId; + const teamId = "teamId" in ctx.body ? ctx.body.teamId : void 0; + ``` + + `activeOrganizationId` is read 8 times in that module; `activeTeamId`, never. An + omitted `teamId` therefore stays `undefined` and the member joins no team — every + `if (teamId)` branch (team lookup, `TEAM_NOT_FOUND`, per-team limit) is skipped. + + No runtime behaviour changes, and no deployment was ever misled: the `add_member` + action's `params` list carries no `teamId`, so the toolbar never sent one and the + claim was never exercised. What the comment did mislead was the next reader of + the mount, which cited it as the justification for forwarding request headers — + forwarding buys the organization default only. Forwarding `teamId` itself remains + correct: pass it and it works. + + The asymmetry the docs now publish is held by a new pin, + `organization-add-member-team-fallback.test.ts`, which reads the fact out of the + installed vendor artifact (not out of our own comments) so that a future + better-auth bump *adding* an active-team fallback reddens instead of silently + putting the docs out of date. +- 0ab81d1: fix(metadata-protocol): the seed/API tenancy repair now records each applied run in `sys_migration`, so "was my data rewritten, and when" survives the container being replaced (#9451) + + `backfillSeedTenancy` (#8686) is the platform's only row-rewriting repair that + runs unattended: it stamps `organization_id` onto business rows, merges one + autonumber counter and deletes another. It persisted nothing about having done + so. The only evidence was one `logger.info` line, and the healthy path is silent + by design — so once that line had scrolled, a silent boot and a boot that + rewrote data were indistinguishable. The operator most likely to need the record + (a fresh install, repaired during the first admin sign-up, where nobody is + reading server stdout) was the one least likely to have captured it. + + An `applied` run now writes one row into the **existing** `sys_migration` + deployment ledger — the face that already answers "has this deployment run this + data migration", and is already written at boot by the ADR-0104 attestation + path: + + ```sql + SELECT last_run_at, advisory, details FROM sys_migration + WHERE id = 'seed-tenancy-backfill'; + ``` + + `details` carries the run's status, the objects stamped, the organization + adopted and the identifiers that could not be adopted because they were already + minted on both sides of the split. + + Deliberately narrow: + + - **`applied` only.** `no-split` stays silent — a row per healthy boot would be + a ledger of non-events. + - **`verified_at: null`, `blocking: 0`, always.** This repair runs no self-check + and gates no consumer, so it claims no certificate; the collision count goes + to `advisory`, which never gates. Every reader of this ledger looks a row up + by `id`, so the new id cannot reach another migration's gate. + - **Best-effort, and loud when it fails.** A boot is never failed by + bookkeeping (2026-08-15 ruling), so a failed receipt write is reported at + `error` — naming that the rows *were* rewritten, that the repair is not + retried, and what to do — rather than rethrown. + - **No new schema, no new authoring surface, no new dependency.** The row is + written against the `@objectstack/spec/system` contract that + `metadata-protocol` already depends on. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/metadata-core@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index a7ac27bd0d..c908e1a2b0 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "17.1.0", + "version": "17.2.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 e32be05440..21a6c79241 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,37 @@ # @objectstack/embedder-openai +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 81171f638d..83aef704fd 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.1.0", + "version": "17.2.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 67f54ed7db..ae7f0f53b4 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,44 @@ # @objectstack/knowledge-memory +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [6d5c4fa] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/service-knowledge@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index f26f4add1f..00c6e9cdae 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.1.0", + "version": "17.2.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 8475d249e0..4c128d5c74 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,53 @@ # @objectstack/knowledge-ragflow +## 17.2.0 + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [6d5c4fa] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/service-knowledge@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index 06f24194e2..134b2cb649 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.1.0", + "version": "17.2.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/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index 573f81b4d2..54c0544b71 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,207 @@ # @objectstack/plugin-approvals +## 17.2.0 + +### Minor Changes + +- 13f533a: fix(approvals): screen the `manager` approver to the request's organization (#10153) + + `expandApprovers` hands the directory organization to every graph-shaped + approver expansion — `department`, `position`, `org_membership_level`. The + `manager` branch did not: `lookupManager` read `sys_user.manager_id` under a + system context and took no organization argument at all. `sys_user` is a global + identity table with no `organization_id`, so nothing else on that path supplied + the tenancy fact either. A `manager_id` crossing an organization boundary + therefore routed the submission to an approver **in another organization** — an + out-of-tenant person granted approval authority over the record. + + The same column has been screened on the hierarchy side since cloud#1195. This + brings the approvals consumer into line for the `manager` branch. + + ## What the screen is + + `lookupManager(userId, organizationId)` now resolves the manager and then asks + whether he is **provably outside** the request's organization: + + | membership rows for the manager | result | + |---|---| + | some exist, none in the request's org | **screened out** — the slot falls through to the `manager:` literal | + | one is in the request's org | resolves, unchanged | + | none exist at all | resolves, unchanged — the tenancy fact is absent, not negative | + | the `sys_member` read failed | resolves, unchanged | + | the request carries no organization | resolves, unchanged — and no read is performed | + + The fail-open half is this file's ruled posture on addressing paths, stated + twice already: `filterApproversWhoCanRead` refuses to empty a live slate on an + infrastructure hiccup, and `expandPositionUsers` carries "a step routing to + nobody is worse than one routing to a lapsed holder". A drop is logged with the + manager's id, his organizations and the request's, so the fix ("repair the link" + / "grant the membership" / "retarget the step") is legible without a debugger. + + ## ⚠️ This moves one input from accepted to refused + + A node whose **sole** approver is a cross-org `manager` and which is authored + with the **non-default** `onEmptyApprovers: 'fail'` used to open successfully; + it now throws `NO_APPROVERS`. Nothing new is thrown — a screened-out manager + leaves only a `type:value` literal, which the pre-existing empty-slate test + already classifies as empty, and `'fail'` already throws on empty. Every + screened sibling has reached that same bucket since it was written. + + **The default policy is unaffected**: `admin_rescue` still opens the request + (decidable by a privileged admin) and warns, and `auto_approve` still + auto-approves. Both directions and both policies are pinned in + `manager-approver-org-screen.test.ts`. + + ## What this does NOT decide + + - **#7497** (does approver routing imply record read visibility?) stays open. + The screen reads `sys_member`, which looks like the D2 read filter beside it, + and the code says at length why it is the *sibling* treatment instead: two of + the three org-scoped expansions already screen on `sys_member.organization_id`, + and `sys_user` offers no other tenancy fact. No reads are granted and no read + screen is applied to any type that lacked one. + - **`team`** is still unscreened — it is a sibling graph expansion that is not + org-scoped either, tracked as #10230, and it touches this same file. + - `APPROVER_ORG_SCOPED` is untouched. It answers ADR-0105 D9 *retargetability* + (may an author write `organization:` on this type?), not screening, and + `manager: false` remains correct. +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel + actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and + `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and + `LiteKernel.destroy()`, which walk the plugins in reverse calling + `plugin.destroy()`, walked straight past every plugin whose teardown was spelled + `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still + armed, the REST/OpenAPI/Slack connectors still registered on the automation + engine, the approvals SLA escalation job still scheduled, and the knowledge + event-sync subscription still open. + + Each teardown body now lives in `destroy()`. `stop()` is retained as a + delegating alias with its parameter made optional, so an embedder that learned + to call it directly — precisely because the kernel never did — keeps working + unchanged. No export is removed and the `Plugin` interface is untouched. + + Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as + fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted + from the merge queue. +- aa765b9: **Who loses access:** members of a team belonging to a *different* organization + than the record being approved. Concretely — a request raised in `org_a` routed + to a `team` approver whose `sys_team.organization_id` is `org_b` used to place + every `sys_team_member` of that team into `pending_approvers`, giving them the + approve/reject buttons on a record they are not a tenant of. They no longer + enter the slate, and the step falls back to the dead `team:` literal with + the existing `#3807` "expanded to nobody" warning — the same shape a cross-org + `position` approver has always produced (#10230). + + `team` was the last approver expansion that resolved people without asking + which organization was asking; `department`, `position`, `org_membership_level` + and (since #10153) `manager` all do. The screen reads the team's own + `organization_id`, so it costs one row and a team that fails it never fans out. + + **Who does not lose access**, deliberately: a team stamped with the request's + own organization; a team stamped with **no** organization (`organization_id: + null` on a platform object means "owned by no organization" — what a seed + writes, since a seed cannot know the id the runtime mints at boot); a team id + with no `sys_team` row at all; and any request that carries no organization — + all four leave routing exactly as it was, because the tenancy fact is absent + rather than negative. + + ⚠️ One externally observable accept→reject change beyond the routing itself: + under the non-default `onEmptyApprovers: 'fail'` policy, a node whose *sole* + approver was a cross-org team used to open a request and now throws + `NO_APPROVERS`. Under the default (`admin_rescue`) the node still opens. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 084108f735..a25c814622 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.1.0", + "version": "17.2.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 89b7559e14..798772c192 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,135 @@ # @objectstack/plugin-audit +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- 76deca2: **Docs (published README) + ruling:** record-view auditing now documents how to turn it on under `objectstack serve`, and the answer to "should `os serve` grow an `appAuditPluginOptions(config)` helper?" is **no** (#9863). + + The README and `content/docs/permissions/record-view-auditing.mdx` both said the audited set is configured "where you compose the kernel", and the docs page went further: *"The CLI's `os serve` registers `AuditPlugin` with no options, so a stack served that way has record-view auditing off and no knob to turn it on."* That last clause stopped being true when #9864 declared and pinned the duplicate-registration contract. The knob is the stack's `plugins` array — a configured `new AuditPlugin({ readAudit: { objects: [...] } })` there supersedes the CLI's option-less instance by name, last-one-wins, on both kernels, with the displaced instance never reaching `init()`. Both pages now spell that path, and name the `Plugin superseded: 'com.objectstack.audit'` boot line as the opt-in working rather than a misconfiguration. + + **No new configuration surface was added, deliberately.** A `config.audit` key read by an `appAuditPluginOptions(config)` helper would reproduce, in `objectstack.config.ts`, exactly the failure #8992's ruling refused for the object-metadata spelling: a declaration that survives in a deployment which never installs this package, reading as coverage while recording nothing. It would also be a *second* configuration surface that silently loses to the first, since an app's own `plugins` entry supersedes whatever the CLI constructed. The `#7001` symmetry argument does not carry it either — `@objectstack/verify`'s `bootStack` constructs no `AuditPlugin` and does not depend on this package, so there is no second boot path to disagree with. + + No runtime behaviour changed. `packages/cli` gains only the reasoning at its registration site and `serve-audit-registration.contract.test.ts`, which pins the three facts the ruling rests on — including the load-bearing ordering (`AuditPlugin` registered above the stack `plugins` loop) that until now was asserted by a comment and nothing else. +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- dd41df3: **Behaviour change (tightening):** `enable.files` / `enable.feeds` are now enforced on the **update** verb, not only on insert (#10170). + + Both capability gates in `audit-writers.ts` registered on `beforeInsert` only. `enable.files` says whether `sys_attachment` rows may **target** an object and `enable.feeds` whether `sys_comment` rows may target it — properties of the target object, not of the verb that got a row there — so a re-point via update landed rows the declaration refuses: a caller who could not *create* an attachment on an object without `enable.files: true` could *move* an existing one onto it, and a comment could be re-threaded into a `feeds: false` object's thread. The access kits authorize the re-point (`comment-access-hooks.ts` since #4630, `attachment-access-hooks.ts` since #10091), but those are **access** checks — the capability half was never asked on update. + + What an operator will now observe: + + - An update of `sys_attachment` whose payload sets `parent_object` to an object that does not declare `enable: { files: true }` is refused with **403 `FILES_DISABLED`** — the same envelope the insert path has emitted since #2727 (ADR-0112: `code` + `status`). Fail-closed as on insert: an absent `enable` block, an absent flag, and an unknown parent object all reject. + - An update of `sys_comment` whose payload sets `thread_id` to a thread on an object declaring `enable: { feeds: false }` is refused with **403 `FEEDS_DISABLED`**. Opt-out semantics as on insert: only an explicit `false` rejects, and a missing or free-form `thread_id` is still allowed through — this is capability gating, not access control. + - Both apply on **both dispatch shapes**: a by-id update (`dispatch.mode` `record`) and a predicate `multi: true` update, which is evaluated per matched row (#5574 / ADR-0058 Addendum II). An unscoped predicate update is refused on its first matched row. + + **No existing row is newly refused, and no update that is not a re-point changes.** The gates read the payload: an update that never names `parent_object` / `thread_id` returns on the gate's first line, so renames, body edits, reaction writes and other column updates on a row whose parent object has since had the capability flipped off keep working exactly as before. Only a write that makes a row *newly target* a walled object is refused. + + **Blast radius.** A structural sweep of the 4 660 in-tree source files found **no** caller — none in `packages/` source, `examples/`, or the dogfood apps — that issues an update whose payload names `parent_object`, and none that re-points `thread_id`; in the console the only `sys_attachment` write is a create, and the only `sys_comment` update writes `reactions`. If you have your own "move this attachment" or "move this comment" flow, point it at a target object that declares the capability, or declare it on the target. + + No new error code: both codes are existing standard-catalog members already registered in `packages/spec/src/api/error-code-ledger.zod.ts` and already mapped to 403 by `packages/rest/src/error-response.ts`. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [2570ab0] +- Updated dependencies [d23e3a0] +- Updated dependencies [f3a8134] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 7b5ad1bbba..74dad716ea 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.1.0", + "version": "17.2.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 5a3455114e..bcd3010637 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,154 @@ # Changelog +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- 8f04d9a: Correct a false vendor claim in the `organization/add-member` source comments: + `teamId` has **no** active-team fallback (#10532). Two comments — the + `sys_member` `add_member` action metadata (the origin) and the + `organization-add-member.ts` module header that cited it as authority — stated + that "organizationId/teamId default to the caller's active org/team when + omitted". Measured on the installed better-auth 1.7.1 + (`dist/plugins/organization/routes/crud-members.mjs`, inside `addMember`), only + the organization half is true: + + ```js + const orgId = ctx.body.organizationId || session?.session.activeOrganizationId; + const teamId = "teamId" in ctx.body ? ctx.body.teamId : void 0; + ``` + + `activeOrganizationId` is read 8 times in that module; `activeTeamId`, never. An + omitted `teamId` therefore stays `undefined` and the member joins no team — every + `if (teamId)` branch (team lookup, `TEAM_NOT_FOUND`, per-team limit) is skipped. + + No runtime behaviour changes, and no deployment was ever misled: the `add_member` + action's `params` list carries no `teamId`, so the toolbar never sent one and the + claim was never exercised. What the comment did mislead was the next reader of + the mount, which cited it as the justification for forwarding request headers — + forwarding buys the organization default only. Forwarding `teamId` itself remains + correct: pass it and it works. + + The asymmetry the docs now publish is held by a new pin, + `organization-add-member-team-fallback.test.ts`, which reads the fact out of the + installed vendor artifact (not out of our own comments) so that a future + better-auth bump *adding* an active-team fallback reddens instead of silently + putting the docs out of date. +- 5b0af2b: **Fix:** `POST /api/v1/auth/admin/impersonate-user` now admits ObjectStack **platform admins**. It previously refused every one of them with `403 YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS` — byte-identical to the refusal a plain member received — so the `sys_user` "Impersonate User" button was dead on every deployment (#9968). + + better-auth's `admin` plugin authorizes on the legacy `user.role === 'admin'` scalar that ADR-0068 D2 stopped synthesizing. ObjectStack's platform admin is a `sys_user_permission_set` row pointing at `admin_full_access` with `organization_id = null`, which the vendor cannot be pointed at, and re-synthesizing the scalar is permanently vetoed. + + **What an operator will now observe.** A platform admin who could not impersonate anyone can now impersonate a non-admin user, and the impersonation takes effect for cookie and bearer clients alike. Refusals are unchanged for everyone else: a signed-in non-platform-admin (including an organization owner or admin, who is **not** a platform admin under ADR-0068) still gets `403 YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS`, and an anonymous caller still gets `401` from better-auth's own `adminMiddleware`. + + **One refusal is newly reachable.** The vendor refuses to impersonate an admin-grade *target* by reading that same `role` scalar against `adminRoles: ['admin']` — a column nothing writes after ADR-0068 D2, so the guard was inert. It is now asked through the ADR-0068 predicate, so impersonating a **platform-admin target** is refused with `403 YOU_CANNOT_IMPERSONATE_ADMINS` where it previously succeeded. + + Implemented as a better-auth **plugin endpoint**, replacing the vendor endpoint in place on the `admin` plugin's own `endpoints` record — not a raw Hono mount. That keeps the signed-cookie contract with `/admin/stop-impersonating` and keeps the `/admin/impersonate-user` path-keyed rotation hook attached, so bearer-client impersonation does not regress to a silent 200 no-op. + + Every other better-auth-native `/admin/*` route still gates on the legacy scalar and still refuses platform admins — unchanged here. +- 86a8ec9: **Behaviour change (tightening):** registering an SSO identity provider through the direct `POST /api/v1/auth/sso/register` endpoint now requires a **platform admin**. An organization **owner or admin** who is not a platform admin can no longer register an identity provider on any surface (#10009). + + Who loses access: an org owner/admin (a `sys_member` row graded owner/admin) with no org-less `admin_full_access` grant. They previously passed the ADR-0024 before-hook on the direct endpoint and now receive `403 SSO_REGISTER_FORBIDDEN`. Platform admins — an org-less `sys_user_permission_set` link to `admin_full_access`, per ADR-0068 D2 — are unaffected, as are anonymous callers, who still fall through to better-auth's `sessionMiddleware` (`401`). + + This closes a posture divergence: the four `/admin/sso/*` bridges the `sys_sso_provider` metadata actions call have gated on the platform-admin judge since #9653, while better-auth's own endpoint kept the wider ADR-0024 admit set — so the same principal was refused at one door and admitted at the other for the same underlying registration, leaving the bridge tightening as labelling rather than a boundary. Per the 2026-08-20 maintainer ruling, ADR-0068 D4 governs: registering an identity provider is a platform-operator action. If org-scoped IdP self-serve is ever wanted, it is a deliberate future decision rather than a vendor default inherited by omission. + + The direct endpoint also gains its first test pins; the now-callerless `isOrgOrPlatformAdmin` predicate was removed rather than left dead. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [6ce58a7] +- Updated dependencies [9a1ed7a] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [9e04c3e] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [4389fe9] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/rest@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 401327f704..c0ff297834 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.1.0", + "version": "17.2.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 cad28356b6..bff1a094f2 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,79 @@ # @objectstack/plugin-dev +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [da891e0] +- Updated dependencies [a38c3ff] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [2570ab0] +- Updated dependencies [5886ee6] +- Updated dependencies [b20c8d2] +- Updated dependencies [6ce58a7] +- Updated dependencies [d23e3a0] +- Updated dependencies [9a1ed7a] +- Updated dependencies [f3a8134] +- Updated dependencies [b03a880] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [5b0af2b] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [9e04c3e] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [a16ff50] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [4389fe9] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [b419135] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [86a8ec9] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [24ba050] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/plugin-auth@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/driver-memory@17.2.0 + - @objectstack/service-storage@17.2.0 + - @objectstack/runtime@17.2.0 + - @objectstack/service-i18n@17.2.0 + - @objectstack/plugin-security@17.2.0 + - @objectstack/rest@17.2.0 + - @objectstack/plugin-hono-server@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/account@17.2.0 + - @objectstack/setup@17.2.0 + - @objectstack/service-realtime@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 8d1071ca70..687b64a50d 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.1.0", + "version": "17.2.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 284a5f97c6..d628c147a7 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,120 @@ # @objectstack/plugin-email +## 17.2.0 + +### Minor Changes + +- a16ff50: `SweepLogger` and `ProjectionLogger` now declare `warn` as a REQUIRED channel, so a sink handed to the boot outbox sweep or to permission-set reconciliation can no longer be one that prints nothing (#9754) + + Both interfaces declared every member optional — `info?`, `warn?`, `error?` — which made `{ info }` a legal sink. Against such a sink both durability reports evaporated: each reaches for `error`, finds none, falls back to `warn`, and finds none of that either. For the sweep that is mail the platform accepted and never delivered, summarised to nobody; for reconciliation it is a permission set that will not survive a re-provision, with the `info` "reconciled" line skipped as well, so the sink heard neither the failure nor the reassurance. + + #9657 and #9748 repaired the call-site spellings. This is the other half, and the half that cannot regress: an optional `error` with no guaranteed alternative is a contract that permits silence, so an author reading the interface can write a report that never prints and be right about the type. Requiring `warn` makes that unrepresentable at the point of authoring rather than catchable one gate-run later. + + `error` deliberately stays optional on both types — hosts do inject reduced sinks, and requiring `error` would foreclose the `{ warn }`-only host the drivers were written for. + + If you pass a logger of your own and it declares no `warn`, add one; the kernel `Logger`, `ctx.logger` and `console` all satisfy the tightened shape unchanged. Consumers reach these types through `@objectstack/plugin-security`'s exported `ProjectionDeps`; `SweepLogger` is internal to `@objectstack/plugin-email`. + + The rule now has a checker of its own: `pnpm check:optional-error-sink` scans every sink type in `packages/**`, reports the population as a census on every run, and carries a shrink-only ledger of the 15 sinks that still permit silence. +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- b20c8d2: **Durability fix:** the two boot-time **summary** reports now reach a logger sink that has no `error` method, instead of printing nothing at all (#9748). + + `SweepLogger.error` and `ProjectionLogger.error` are both declared **optional**, and both summaries were spelled `logger?.error?.(…)` — an optional call that emits **nothing** when the method is absent. #9657 repaired the six per-row reports of this shape; it could not see these two, because `check:durability-log-level` only judges a call inside a `catch`, and a summary sits after the loop. Against a `{ info, warn }` sink the result was that the repair made the split **worse**: the per-row detail arrived at `warn` while the count of failures vanished, so the detail and the total reported through different channels. + + - `sweepStrandedOutbox()` — *"N stranded `sys_email` row(s) could NOT be delivered"*. Mail the platform **accepted** and never delivered, previously summarised to nobody. + - `reconcilePermissionSetProjection()` — *"N FAILED backfill(s)"*. Worse than a plain omission here: the `else` branch carrying the `info` "reconciled" line is skipped too, so such a sink heard **neither** — the reassuring half-truth this rule exists to remove, arrived at from the other side. + + Both now reach for `error` and fall back to `warn`, never to silence — the same repair shape #9657 applied to the per-row lines. A sink that **does** have `error` is unaffected and still gets the summary at `error`; a downgraded level is a degradation of the channel, never of the message, so the consequence and the fix survive the fallback intact. + + Also enforced from now on: `check:durability-log-level` grew a **summary limb** that judges a report keyed on the counter a durability-critical `catch` accumulated into, so this class cannot regress silently. The limb never second-guesses a chosen log **level** — it only checks that a call that reaches for `error` can actually print. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/formula@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index 864192f2f5..bbc7420e49 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.1.0", + "version": "17.2.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 10cec99700..e1e42e98cf 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/plugin-hono-server +## 17.2.0 + +### Patch Changes + +- b03a880: docs(plugin-hono-server): boot the kernel with the method it actually ships (#9870) + + `packages/plugins/plugin-hono-server/README.md` is in the package's `files` array + with `private` unset, so it is the page npm renders. Its Usage block ended: + + ```ts + const kernel = new ObjectKernel(); + kernel.use(new HonoServerPlugin({ port: 3000, /* … */ })); + await kernel.start(); + ``` + + Measured against the built type surface: `ObjectKernel` (re-exported by + `@objectstack/runtime` from `@objectstack/core`) declares `bootstrap()` and + `shutdown()` and has **no** `start` member. A reader copying the block gets a + compile error on its last line. + + The line reads plausibly because the `IKernel` *interface* in + `@objectstack/types` does declare `start()` — but the concrete class the fence + constructs does not implement that name, and eight sibling READMEs + (`objectql`, `rest`, `runtime`, `service-cache`, `service-job`, + `service-automation`, `service-package`, `service-cluster-redis`) all spell the + same step `await kernel.bootstrap()`. Fixed to match. + + Found by the call-site widening in the same PR, not by hand: the receiver is + never import-bound, so before that widening this call site was one of the 262 + `check:published-readme-exports` could not read. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 33d7500aa7..4295b93756 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.1.0", + "version": "17.2.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 36353d1b39..33498380c2 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,22 @@ # @objectstack/plugin-pinyin-search +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [530c1df] +- Updated dependencies [2570ab0] +- Updated dependencies [d23e3a0] +- Updated dependencies [f3a8134] +- Updated dependencies [47cd3ec] +- Updated dependencies [9d7d2de] +- Updated dependencies [d728325] +- Updated dependencies [8012960] + - @objectstack/objectql@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index fa7c70108f..7906af828c 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.1.0", + "version": "17.2.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-reports/CHANGELOG.md b/packages/plugins/plugin-reports/CHANGELOG.md index 6755b8ccd5..4b072875da 100644 --- a/packages/plugins/plugin-reports/CHANGELOG.md +++ b/packages/plugins/plugin-reports/CHANGELOG.md @@ -1,5 +1,116 @@ # @objectstack/plugin-reports +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel + actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and + `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and + `LiteKernel.destroy()`, which walk the plugins in reverse calling + `plugin.destroy()`, walked straight past every plugin whose teardown was spelled + `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still + armed, the REST/OpenAPI/Slack connectors still registered on the automation + engine, the approvals SLA escalation job still scheduled, and the knowledge + event-sync subscription still open. + + Each teardown body now lives in `destroy()`. `stop()` is retained as a + delegating alias with its parameter made optional, so an embedder that learned + to call it directly — precisely because the kernel never did — keeps working + unchanged. No export is removed and the `Plugin` interface is untouched. + + Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as + fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted + from the merge queue. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index 5d936b5096..fbeab87afa 100644 --- a/packages/plugins/plugin-reports/package.json +++ b/packages/plugins/plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-reports", - "version": "17.1.0", + "version": "17.2.0", "license": "Apache-2.0", "description": "Saved reports + scheduled email digests for ObjectStack — sys_saved_report + sys_report_schedule + IReportService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index 7031c94bf9..0eea6d3352 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,191 @@ # @objectstack/plugin-security +## 17.2.0 + +### Minor Changes + +- a16ff50: `SweepLogger` and `ProjectionLogger` now declare `warn` as a REQUIRED channel, so a sink handed to the boot outbox sweep or to permission-set reconciliation can no longer be one that prints nothing (#9754) + + Both interfaces declared every member optional — `info?`, `warn?`, `error?` — which made `{ info }` a legal sink. Against such a sink both durability reports evaporated: each reaches for `error`, finds none, falls back to `warn`, and finds none of that either. For the sweep that is mail the platform accepted and never delivered, summarised to nobody; for reconciliation it is a permission set that will not survive a re-provision, with the `info` "reconciled" line skipped as well, so the sink heard neither the failure nor the reassurance. + + #9657 and #9748 repaired the call-site spellings. This is the other half, and the half that cannot regress: an optional `error` with no guaranteed alternative is a contract that permits silence, so an author reading the interface can write a report that never prints and be right about the type. Requiring `warn` makes that unrepresentable at the point of authoring rather than catchable one gate-run later. + + `error` deliberately stays optional on both types — hosts do inject reduced sinks, and requiring `error` would foreclose the `{ warn }`-only host the drivers were written for. + + If you pass a logger of your own and it declares no `warn`, add one; the kernel `Logger`, `ctx.logger` and `console` all satisfy the tightened shape unchanged. Consumers reach these types through `@objectstack/plugin-security`'s exported `ProjectionDeps`; `SweepLogger` is internal to `@objectstack/plugin-email`. + + The rule now has a checker of its own: `pnpm check:optional-error-sink` scans every sink type in `packages/**`, reports the population as a census on every run, and carries a shrink-only ledger of the 15 sinks that still permit silence. +- 3ee8ddf: fix(security): **BREAKING** — `sys_position` retires the `permissions` column (ADR-0049 enforce-or-remove, #9885) + + Maintainer ruling 2026-08-20: **REMOVE**. The column — a "JSON-serialized array + of permission strings" textarea — was declared on the platform position table + while **no producer ever wrote it and no runtime path ever read it**. The + object-scoped census (every `sys_position`-naming file, with same-object + positive controls resolving `active` / `delegatable` / `is_default` / `name` + to real readers) measured it at zero on both sides: the builtin and declared + position bootstrappers set `label` / `description` / `managed_by` / `active` / + `is_default` only, and position→grant resolution consults + `sys_position_permission_set` rows plus the position `name` — never this + column. Its only reference was the `clone_position` action copying it between + rows (a copy of a value nothing writes), removed in the same stroke. objectui + was searched under the same discipline: no console surface names the column. + A free-text grant catalogue on a security object that no runtime enforces + tells an author — human or AI — that direct position-level permission strings + are a platform capability; they are not. This is an **accept-set narrowing**: + the platform stops declaring, projecting and accepting the column. + + Migration (FROM → TO): + + | Wrote | Write instead | + |---|---| + | `permissions` on a `sys_position` seed row or data-door write | Delete the key. Capability reaches a position **only** through permission-set bindings (`sys_position_permission_set` rows, created in Setup or by an app's kernel:ready binder); prose that was documenting intent belongs in `description`. | + + One-line fix: delete `permissions` from any authored `sys_position` row. + + + + Enforcement after the removal is loud, not silent: the engine's schema + preflight refuses an undeclared field with `400 INVALID_FIELD` before the + driver or any hook runs, and `PositionSchema`'s strict parse now rejects a + declared-position `permissions` key with guidance naming the binding table. + Physical columns on already-deployed databases are untouched (ADR-0045 schema + sync is additive). If position-level direct grants ever become a real need, + the column is re-declared **with a runtime reader in the same PR** — + declare-and-enforce or don't declare. + +### Patch Changes + +- 5886ee6: Stop issuing two DB queries for questions already answered earlier in the same + request (#10757). One authenticated `GET /data/:object?$top=1` measured **24 DB + queries before, 23 after** — **22** when the caller opts out of the count. + Measured with `X-OS-Debug-Timing: json` on `pnpm dev:crm`, whose `Server-Timing` + carries `db;dur=…;desc="N queries"`. + + **`$count=false` now skips the COUNT query** (`@objectstack/metadata-protocol`). + The parameter has been declared (`ODataQuerySchema.$count`), aliased on the wire + (`$count` → `count`), reserved out of the implicit-field-filter bucket, + arity-checked and boolean-coerced for a long time — and then deleted unread, so + every paginated list ran `engine.count()` whether or not the caller wanted a + total. It is honoured now: + + ``` + GET /data/task?$top=25 → { records, total, hasMore } (unchanged) + GET /data/task?$top=25&$count=true → { records, total, hasMore } (unchanged) + GET /data/task?$top=25&$count=false → { records, hasMore } (no COUNT query) + ``` + + Read the shape of that carefully before adopting it: + + - **Only an explicit `false` opts out.** An ABSENT `$count` still counts and + still reports `total`. OData reads absent as "omit the count", and taking that + reading here would silently strip `total` from every existing caller — none of + them send the parameter, all of them read the number. The asymmetry is + deliberate and pinned by tests. + - **`total` is OMITTED, never estimated.** `FindDataResponse.total` is declared + optional ("if requested"), so absent is the declared shape for "not + requested". A caller that opted out and then reads `total` gets `undefined`, + not a plausible-looking guess — guard the read (`total ?? undefined`) or do + not send `$count=false`. + - **`hasMore` is still answered**, from the page alone: a full page means there + may be more. Same page-local rule the `$search` path already uses. + + **A find and its COUNT resolve permission sets once, not twice** + (`@objectstack/plugin-security`). `findData` answers a paginated list with two + engine operations, and the security middleware runs on both; each pass re-read + `sys_permission_set` for the same context with identical bindings. The + resolution is now memoized per execution context — a `WeakMap` keyed on the + context object, which is built once per request and collected with it, so + nothing outlives the caller it was resolved for — and **retired by any write**: + a process-wide epoch is bumped on every `insert`/`update`/`delete` the engine + middleware sees, ahead of the `isSystem` bypass so a seeder, a package publish + or an auto-org-admin grant invalidates too. A context whose grants are rewritten + in place re-resolves as well (the memo key covers `positions`, `permissions`, + `principalKind` and the presence of `userId`). No authorization answer is reused + across a write, across a context, or across a request. + + Not a fix for the whole cost: the remaining ~22 queries per authenticated + request are session resolution, grant resolution, localization and metadata + reads that repeat on every request. Removing those needs cross-request caching + with an invalidation design, which is deliberately not in this change. +- b20c8d2: **Durability fix:** the two boot-time **summary** reports now reach a logger sink that has no `error` method, instead of printing nothing at all (#9748). + + `SweepLogger.error` and `ProjectionLogger.error` are both declared **optional**, and both summaries were spelled `logger?.error?.(…)` — an optional call that emits **nothing** when the method is absent. #9657 repaired the six per-row reports of this shape; it could not see these two, because `check:durability-log-level` only judges a call inside a `catch`, and a summary sits after the loop. Against a `{ info, warn }` sink the result was that the repair made the split **worse**: the per-row detail arrived at `warn` while the count of failures vanished, so the detail and the total reported through different channels. + + - `sweepStrandedOutbox()` — *"N stranded `sys_email` row(s) could NOT be delivered"*. Mail the platform **accepted** and never delivered, previously summarised to nobody. + - `reconcilePermissionSetProjection()` — *"N FAILED backfill(s)"*. Worse than a plain omission here: the `else` branch carrying the `info` "reconciled" line is skipped too, so such a sink heard **neither** — the reassuring half-truth this rule exists to remove, arrived at from the other side. + + Both now reach for `error` and fall back to `warn`, never to silence — the same repair shape #9657 applied to the per-row lines. A sink that **does** have `error` is unaffected and still gets the summary at `error`; a downgraded level is a degradation of the channel, never of the message, so the consequence and the fix survive the fallback intact. + + Also enforced from now on: `check:durability-log-level` grew a **summary limb** that judges a report keyed on the counter a durability-critical `catch` accumulated into, so this class cannot regress silently. The limb never second-guesses a chosen log **level** — it only checks that a call that reaches for `error` can actually print. +- b419135: Report a metadata-store OUTAGE as an outage, not as an absent declaration + (#10424). When an object's security posture cannot be resolved, the refusal + now consumes the `degraded` verdict `IMetadataService.getDiagnosed` was already + computing and discarding (#5840), so a store that could not answer no longer + wears the sentence written for an object that was never declared — "Check that + the object is declared and published on this runtime" sent operators to + re-check a healthy declaration in the middle of an incident. The refusal now + names the store, says the declaration may well be fine, and the operator log + line carries a grep-able `DEGRADED` / `metadata-store OUTAGE`. + + Explanation and logging only. The deny is unchanged in every case — same + `PermissionDeniedError`, same `PERMISSION_DENIED`, same 403, still fail-closed + per #3545 — and the set of requests that are accepted or rejected does not + move: the resolving read is untouched and `getDiagnosed` is consulted as a + separate best-effort probe on the path that is already refusing. A metadata + service that does not implement the optional `getDiagnosed` reports `unknown` + and keeps the previous wording; it is never reported as an outage. +- 24ba050: **Message change (no behaviour change):** a data-plane read against an object that exists only as an **unpublished draft** now says so, instead of reporting an internal security step (#10401). + + The refusal itself is unchanged and stays fail-closed (#3545): same `PermissionDeniedError`, same `PERMISSION_DENIED` code, same HTTP 403, same `[Security] Access denied` prefix — which is a **matcher** the transports read as "this is a 403", not house style. Nothing here widens access, and no access decision branches on the new information. + + What changed is what the refusal *says*. One sentence — "the security posture of object 'X' could not be resolved for operation 'find'" — covered two conditions with two different remedies, and described neither: because it named a *security* step, every reader took it for a permissions problem and went looking for a sharing rule to change. Measured downstream (objectstack-ai/cloud#1481): an end-user AI turn asked "how many customers do I have?" against a draft-only object, spent seven tool calls oscillating between a metadata plane that said the object existed and this refusal, then told the user the object was "missing its sharing/visibility setting" — confident, professional, and wrong. On a free plan that one turn also exhausted the daily allowance. + + The two conditions are now separated: + + - **The object has a `sys_metadata` draft and no published row** → *"object 'X' is not published — a draft declaration exists but no published one … Publish the object to make it queryable. This is NOT a permissions problem …"*. + - **The declaration genuinely cannot be read** (never declared, or a metadata-store outage) → the pre-existing clause **verbatim**, so any surface matching `the security posture of object 'X' could not be resolved for operation 'Y'` keeps matching, followed by the remedy and the same explicit statement that permissions are not the lever. + + Both sentences, and the operator log line beside them, are derived from one module (`unresolved-posture.ts`) shared with the explain engine's `object_crud` layer detail. Enforcement and explanation stating one refusal in two drifting wordings is the defect shape this closes, so the wording is a single source rather than two literals. + + The discriminator comes from a **best-effort** `sys_metadata` probe that runs only on the path already refusing, reads under a system context (so it cannot re-enter the middleware), and fails safe in one direction only: any failure — no `sys_metadata` in the deployment, an unprovisioned store, a driver error — reports the both-conditions wording rather than a claim. A posture that resolves never probes at all. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/formula@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 93d91ab6b9..41102170ac 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.1.0", + "version": "17.2.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 df60dbc6c6..7503621575 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,121 @@ # @objectstack/plugin-sharing +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- bc400af: **Behaviour change (narrowing):** an **org-stamped** sharing rule's criteria sweep is now scoped to that rule's own organization, where it previously swept **every** organization's records (#10119). + + `SharingRuleService.findMatchingRecords` (the whole-rule evaluation pass) and `recordMatches` (the per-record write-hook pass) ran the rule's criteria query under a bare system context carrying no tenant, for every rule. The recipient half was already org-aware — `expandRecipient` threads `rule.organization_id` into the team / business-unit / position graph services — so a rule stamped with an `organization_id` expanded recipients inside its own organization and then matched records belonging to all the others. `reconcile` materialized the cross product: `sys_record_share` rows granting one organization's users access to another organization's records. + + Measured on `main` before the change, through a real `ObjectQL` on a real `SqlDriver`: an `org_a`-stamped rule matched **the same four records as a platform-global rule** (`deal_a1`, `deal_b1`, `deal_b2`, `deal_p1`) and materialized a grant on each; the per-record hook pass minted a grant on `org_b`'s record with `grantsCreated: 1`. + + What changes, and for whom: + + - **Org-stamped rules** (`organization_id` non-null — what any org admin mints through `defineRule`) now run their criteria query with `tenantId` set to the rule's organization. The platform's existing chokepoint does the rest: `ObjectQLEngine.buildDriverOptions` threads it to `DriverOptions.tenantId` and `SqlDriver.applyTenantScope` emits `(organization_id = ? OR organization_id IS NULL)`. So such a rule matches its own organization's records **plus** platform-owned null-org records, and no other tenant's. `SharingRuleEvaluationResult.matchedRecords` falls accordingly, and the next reconcile pass **revokes** the cross-org `sys_record_share` rows it previously created, through the existing revoke-the-remainder branch — no migration is needed. + - **Platform-global rules** (`organization_id = null`) are unchanged: they keep the full unscoped sweep, which is their declared behaviour (documented at the `deleteRule` platform-authority guard). Both directions are pinned. + - **No public contract changes.** No schema, route, error code or accept/reject set moves; the system elevation on the criteria read is retained (the evaluator still sees rows no individual recipient could), only the tenant axis is added. + + The cross-org rows this stops creating were **inert** under a walled posture — the Layer-0 tenant wall AND-composes over sharing's Layer-1 widening, so such a grant could not open a read across the wall. The costs were `sys_record_share` bloat (every org-stamped rule scanning the whole table at `limit: 5000`) and a population that is wrong at rest, which any consumer reading `sys_record_share` directly, or any future softening of the wall, would inherit. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [2570ab0] +- Updated dependencies [d23e3a0] +- Updated dependencies [f3a8134] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index 2f28e1bd34..adde98c9be 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.1.0", + "version": "17.2.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 849ba6a21b..689b5d25ea 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,97 @@ # @objectstack/plugin-webhooks +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [8163a1c] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [900e489] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/service-messaging@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index 5e90c42de2..6bbdb7f72c 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.1.0", + "version": "17.2.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 f6c17d9c24..7dab0f5c25 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,88 @@ # @objectstack/dogfood +## 0.0.42 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [da891e0] +- Updated dependencies [a38c3ff] +- Updated dependencies [76deca2] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [dd41df3] +- Updated dependencies [2570ab0] +- Updated dependencies [5886ee6] +- Updated dependencies [8163a1c] +- Updated dependencies [b20c8d2] +- Updated dependencies [d23e3a0] +- Updated dependencies [f3a8134] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [5b0af2b] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [900e489] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [112a8c6] +- Updated dependencies [a16ff50] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [6d5c4fa] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [b419135] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [bc400af] +- Updated dependencies [6cca75c] +- Updated dependencies [5f2e54c] +- Updated dependencies [86a8ec9] +- Updated dependencies [35ad101] +- Updated dependencies [502dc6f] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [6439f8b] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [24ba050] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/plugin-auth@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/service-storage@17.2.0 + - @objectstack/plugin-audit@17.2.0 + - @objectstack/service-analytics@17.2.0 + - @objectstack/plugin-security@17.2.0 + - @objectstack/service-messaging@17.2.0 + - @objectstack/plugin-email@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/plugin-sharing@17.2.0 + - @objectstack/plugin-webhooks@17.2.0 + - @objectstack/connector-openapi@17.2.0 + - @objectstack/connector-rest@17.2.0 + - @objectstack/example-showcase@0.3.16 + - @objectstack/mcp@17.2.0 + - @objectstack/metadata@17.2.0 + - @objectstack/verify@17.2.0 + - @objectstack/example-crm@4.0.94 + - @objectstack/connector-mcp@17.2.0 + - @objectstack/types@17.2.0 + ## 0.0.41 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index e26cfa5176..0a7b5db2b4 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.41", + "version": "0.0.42", "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 836ade76d4..30a8df0927 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,37 @@ # @objectstack/downstream-contract +## 0.0.40 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 0.0.39 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index bb70093570..190948fb3e 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.39", + "version": "0.0.40", "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 a37558c4e3..463b089e02 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,13 @@ # @objectstack/http-conformance +## 0.1.2 + +### Patch Changes + +- Updated dependencies [47cd3ec] +- Updated dependencies [9d7d2de] + - @objectstack/core@17.2.0 + ## 0.1.1 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 3f89c62b48..196fb75a3a 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.1", + "version": "0.1.2", "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 7cfb33ec8a..b9880a7016 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,129 @@ # @objectstack/rest +## 17.2.0 + +### Minor Changes + +- 67630c4: The `/meta` FSM state route is singular: `meta.getLegalNextStates` moves, the plural registration is retired (#10077) + + Step 2 of the #9180 ruling — the `/meta` type segment is always singular, no + exception and no tolerated plural alias. Maintainer re-weigh, 2026-08-17, + verbatim: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」 + + - `client.meta.getLegalNextStates(object, field, from?)` now requests + `GET /api/v1/meta/object/:name/state/:field`. Same method, same arguments, + same response body — only the path segment changes. + - `GET /api/v1/meta/objects/:name/state/:field` is **no longer registered**. + The singular twin has been mounted alongside it since #7526, so the + migration for a hand-rolled HTTP caller is to drop the `s`. A request to the + retired spelling now gets the transport 404, which is the loud answer; the + one shape that changes hands rather than 404ing is a field literally named + `published`, which the compound `/:type/:section/:name/published` route + picks up. + - The two route ledgers follow what is mounted and what the SDK calls: the + plural row is deleted from `rest-route-ledger.ts` and the dispatcher ledger's + mirror row is respelled. + + **What this does not change.** The boundary fold `META_URL_TO_SINGULAR` is + untouched, so no `/meta/:type/...` spelling that is accepted today becomes + refused: the retired route matched a **literal** path segment and never + consulted the fold. The 2026-08-17 re-weigh (item 3) defers that break with no + scheduled window. The legacy dispatcher branch in `runtime/src/domains/meta.ts` + also still matches both literals; narrowing it is not part of this step. + +### Patch Changes + +- 6ce58a7: **Behaviour change (tightening) — `POST /datasources/:name/external/validate` now requires `manage_platform_settings`** (#10255, completing the #9901 federation-family gate). This was the one route of the external-datasource federation family still admitting **any authenticated caller**; it now requires the same capability as the family's two read routes. Maintainer ruling, 2026-08-20 (verbatim: 「同意你的意见。」, accepting option A on #10255). + + **This is published SDK surface.** `datasources.external.validate` on `ObjectStackClient` and the CLI's `os datasource validate` reach exactly this route. An existing integration that presents a valid credential — a better-auth session or a `sys_api_key` — and does not hold `manage_platform_settings` was served before and is **refused now**: `403` with the standard catalog code `PERMISSION_DENIED` (ADR-0112), the message naming the missing capability so the caller knows which grant to request. The anonymous floor is unchanged: no identity is still `401 UNAUTHENTICATED`. + + **Why the read capability.** `validateAll` drives the same live remote-schema introspection the family's gated read routes expose (`introspect` per datasource), and its report — schema diffs naming remote columns and types, driver error strings for unreachable remotes — is a read of the same federation surface. An unentitled caller refused at `GET /:name/external/tables` could previously still trigger live remote introspection through this route and read what it found. One family, one door-type: reads on `manage_platform_settings`, writes on `manage_metadata`. + + **Migration.** Grant the calling credential's permission set `manage_platform_settings` — the same grant the family's read routes have required since #10254, so an integration already migrated for those is covered. The platform's `admin_full_access` set carries it; a purpose-built operator set is the case to check. +- 9a1ed7a: **Behaviour change (tightening) — a capability is now required on the external-datasource federation family** (`/api/v1/datasources/:name/external/*`, #9901). These routes previously admitted **any authenticated caller**; four of the five now also require a platform capability. Maintainer ruling, 2026-08-20 (verbatim: 「其他接受你的建议。」). + + **This is published SDK surface.** `datasources.external.*` on `ObjectStackClient` reaches exactly these routes, and the CLI's `datasource` commands go through them. An existing integration that presents a valid credential — a better-auth session or a `sys_api_key` — and holds neither capability was served before and is **refused now**. Nothing about the credential itself changed; what changed is what the credential must carry. + + | route | SDK call | now requires | + | --- | --- | --- | + | `GET /:name/external/tables` | `datasources.external.listTables` | `manage_platform_settings` | + | `POST /:name/external/tables/:remote/draft` | `datasources.external.draft` | `manage_platform_settings` | + | `POST /:name/external/tables/:remote/import` | `datasources.external.import` | `manage_metadata` | + | `POST /:name/external/refresh-catalog` | `datasources.external.refreshCatalog` | `manage_metadata` | + | `POST /:name/external/validate` | `datasources.external.validate` | *(unchanged — authentication only)* | + + A refusal is **`403` with the standard catalog code `PERMISSION_DENIED`** (ADR-0112; deliberately not the grandfathered `FORBIDDEN` synonym), and the message names the missing capability so the caller knows which grant to request. The anonymous floor is unchanged: no identity is still `401 UNAUTHENTICATED`. + + **Why these two capabilities.** The first two routes are the declared twins of `GET /:name/remote-tables` and `POST /:name/object-draft` on the datasource-admin spelling, which has required `manage_platform_settings` since #9593 — the same operation was reachable through two mounted routes with two different admission policies, so an agent or integration refused at one spelling was served at the other. The two write routes have no twin and create live metadata (the import mounts a runtime-origin federated object; the refresh rewrites the cached catalog snapshot), so they take `manage_metadata`, this package's existing gate for metadata creation. + + **Migration.** Grant the caller's permission set the capability its routes need — `manage_platform_settings` for remote-schema introspection, `manage_metadata` for import/refresh. The platform's `admin_full_access` set already carries both, so admin-credentialed integrations are unaffected; a purpose-built operator set is the case to check. +- 26f3588: **Fix:** the REST `/meta` doors now decide **organization scope on the folded type**, never on the raw URL spelling (#10340). + + Storage folds `/meta/:type` through `META_URL_TO_SINGULAR` — the complete spelling map — while the doors' scope predicate (`declaresOrgOverride`) tolerates only the manifest-collection spellings. For the two registry-derived spellings, `translations` and `email_templates`, the doors therefore read and wrote **env-wide** where the singular twin was org-scoped: an org-active author's `PUT /meta/translations/:name` landed an env-wide row their own org-scoped read then shadowed (persisted, receipted as live, served by nothing), and `GET` under one spelling answered a different partition than the other — one item, two namespaces, addressed by spelling (#4432 / #7894's defect one layer down). + + - All nine `/meta` org-scope call sites (list, single read, layers view, compound read, save, compound save, delete, publish, rollback) fold the segment through `canonicalMetaUrlType` **before** calling `organizationIdForMetaRead` / `organizationIdForMetaWrite`, exactly as `metadata-url-spelling.ts` mandates: folding happens at the boundary and only there. + - The `GET /meta/:type/:name/published` code-store fallback folds too — the smaller second site of the same class: it reads a registry keyed by canonical types, so a recognised plural of a code-published item answered 404 while the singular answered 200. + - **Deliberately unchanged:** `GET /meta/_drafts` still applies no fold (it filters by the draft row's *stored* type, which is canonical because the protocol folds on save), the request `type` handed to the protocol stays the raw segment (the protocol owns its own fold), and `declaresOrgOverride` does **not** absorb the URL map — a predicate below the boundary consuming the URL spelling contract is the repair #7894 forbids. `@objectstack/metadata-core` changes are documentation and pins only: the predicate's header no longer claims parity with the protocol's normalization (measured false), and new tests pin both the composed fold→predicate contract and the predicate's deliberate limit. + + No stored rows move: rows previously minted env-wide through a plural spelling stay env-wide and keep serving org-less callers (and org-active callers until an org overlay exists), which is the same layering the singular spelling always had. +- 9e04c3e: **Additive:** `POST /meta/:type/:name/publish` now accepts `?package=`, so a single-item draft→active promotion can state the package it belongs to (#10063). + + #9612 taught the runtime publish gate to narrow `objects` to the written item's package closure, but only for callers that can NAME a package. Of the three write doors that reach the gate, `saveMetaItem` (`?package=` on the `PUT` door) and `publishPackageDrafts` (the batch names it) both did; the single-item promotion door named nothing — so every HTTP-driven promotion, which is exactly Studio's designer save→publish loop on every edit, handed the gate the whole tenant. The protocol half already existed and was waiting: `promoteDraftForPublish` declares `packageId?: string | null` and threads it into both the gate and `repo.promoteDraft`. Only the REST caller was mute. + + - **Wire spelling:** `?package=`, deliberately the same parameter name and the same normalisation the `PUT` door states it with — `all` and the empty value mean "env-local overlay, no package", not a package literally named `all`. One value, one spelling across both steps of the save→publish loop. + - **Multiplicity:** a repeated `?package=a&package=b` is refused `400 VALIDATION_ERROR` in the ADR-0112 nested envelope, via the shared `refuseRepeatedQueryParams` rule the sibling doors already carry; a single occurrence encoded as a one-element array is unwrapped and accepted. Previously the parameter was ignored outright on this route, so no caller relying on a documented behaviour changes. + - **Ordering:** the read sits AFTER the `manage_metadata` capability gate, so an uncapable caller still gets `403` rather than a `400` that would let it probe the shape of the surface. + - **Absent behaviour is unchanged, deliberately down to key presence.** The key is omitted from the `publishMetaItem` request when no package is stated, rather than passed as `undefined`. `promoteDraftForPublish` forwards to `repo.promoteDraft` on `'packageId' in request` — the KEY, not the value — because `null` there is a meaningful scope (pin the lookup to the unbound row) while an absent key means "match any package". A present-and-`undefined` key would therefore coerce to `null` downstream and stop package-bound drafts from being found, answering `no_draft` on a path this change was not supposed to touch. + + ⚠️ **The acceptance criterion is that the narrowing is now REACHABLE from HTTP, not that publishing got faster.** Package-closure narrowing has a second, independent gate this change does not touch: `narrowObjectsToPackageClosure` keeps any object carrying no `_packageId` provenance, unconditionally, and a tenant-authored overlay corpus carries none. On such a corpus supplying the package still narrows nothing. On a provenance-stamped corpus the shipped deriver measures 421 objects → 45. Both gates must hold; this closes the caller-side one. +- 4389fe9: Reworded the `501 NOT_IMPLEMENTED` message on `GET /meta/:type/:name/published` (and its + compound-name arity) to state its true post-#8278 condition. Since #8278 put the + runtime-published overlay consult ahead of this arm, the 501 no longer means "this kernel + cannot answer `/published`" — it means "nothing is runtime-published for this item, and + this kernel has no code/package store" (i.e. `metadata.getPublished()` is unavailable). + The old message ("metadata.getPublished() is not available in this kernel") overstated + that condition. Status code, `error.code`, and routing order are unchanged — only the + message text changed. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/service-package@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 812ffadfcd..7cfe5cf1cd 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.1.0", + "version": "17.2.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 cf9cc8deaa..9979dd6699 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,217 @@ # @objectstack/runtime +## 17.2.0 + +### Minor Changes + +- 914c413: fix(observability): **BREAKING** — `http_request_errors_total` is retired (ADR-0049 enforce-or-remove, #9834) + + **⛔ If you have a Grafana panel, an alert rule or a recording rule keyed on + `http_request_errors_total`, it will read a FLAT ZERO after this upgrade.** That + zero is the removal, not a healthy server, and it is the one way this change can + hurt you — nothing throws, nothing warns, the series simply stops receiving + samples. Rewrite the query before you deploy. + + Maintainer ruling 2026-08-20: **RETIRE**. The name was declared in `SEMCONV` as + part of a stable namespace *"so hosts can wire alerts/dashboards against it"*, + but the only emitter was `@objectstack/runtime`'s `instrumentRouteHandler`, + applied only by the dispatcher's own route Proxy — so the series never saw + auth's `getRawApp()` mount, the REST data API via `RouteManager`, or any other + inbound surface. Its two siblings in the same HTTP family moved to the + `IHttpServer.afterResponse` transport seam (`http_requests_total`, #9650/#9835; + `http_request_duration_ms`, #9834/#10004) and this one could not follow: + `HttpResponseObservation` carries `{method, routePattern, status, elapsedMs}` + and **no throw signal of any kind**, so every transport-side shape would have + counted a *different* population rather than the same one more widely. + + Migration (FROM → TO): + + | Wrote | Write instead | + |---|---| + | `rate(http_request_errors_total[5m])` in a panel or alert | `rate(http_requests_total{status=~"5.."}[5m])` — emitted by the transport, so it covers every inbound surface instead of the dispatcher's routes only | + | `sum by (route) (http_request_errors_total)` | `sum by (route) (http_requests_total{status=~"5.."})` | + | `SEMCONV.httpRequestErrorsTotal` / `RUNTIME_METRICS.httpRequestErrorsTotal` in host code | Delete the read. Both members are gone; `tsc` reports the missing property at the read site. | + + One-line fix: replace the metric name with `http_requests_total{status=~"5.."}`. + + + + **The replacement is wider, not merely different.** The retired counter was + divergent from a 5xx rate in *both* directions, measured: the dispatcher answers + its own errors through `errorResponseBase`, which sets a status and does **not** + re-throw — so the counter **missed** those — while its `catch` incremented + unconditionally, so a **thrown 4xx WAS counted** as an error. And + `http_requests_total` already carries a `status` label, so a status-class error + counter was fully derivable from data the transport already publishes. Prove the + new query wider rather than merely non-empty: make an auth route or a REST + data-API route answer 5xx and confirm it moves, where the retired counter would + not have moved at all. + + **If what you were actually alerting on was "a handler threw rather than + returning an error envelope"** — the one signal this counter uniquely carried — + that is the `errorReporter`, not a metric. Wire an `ErrorReporter` adapter + (Sentry / Datadog / your own); it still fires on every 5xx throw and is + untouched by this change. + + What is NOT removed: `http_requests_total`, `http_request_duration_ms`, + request-id propagation, the 5xx error reporter, and the + `res.__obsRecordedError` side channel that carries a swallowed error to it. The + dispatcher still instruments every route it mounts; it just no longer publishes + a fourth series whose name promised more coverage than it had. +- 67630c4: The `/meta` FSM state route is singular: `meta.getLegalNextStates` moves, the plural registration is retired (#10077) + + Step 2 of the #9180 ruling — the `/meta` type segment is always singular, no + exception and no tolerated plural alias. Maintainer re-weigh, 2026-08-17, + verbatim: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」 + + - `client.meta.getLegalNextStates(object, field, from?)` now requests + `GET /api/v1/meta/object/:name/state/:field`. Same method, same arguments, + same response body — only the path segment changes. + - `GET /api/v1/meta/objects/:name/state/:field` is **no longer registered**. + The singular twin has been mounted alongside it since #7526, so the + migration for a hand-rolled HTTP caller is to drop the `s`. A request to the + retired spelling now gets the transport 404, which is the loud answer; the + one shape that changes hands rather than 404ing is a field literally named + `published`, which the compound `/:type/:section/:name/published` route + picks up. + - The two route ledgers follow what is mounted and what the SDK calls: the + plural row is deleted from `rest-route-ledger.ts` and the dispatcher ledger's + mirror row is respelled. + + **What this does not change.** The boundary fold `META_URL_TO_SINGULAR` is + untouched, so no `/meta/:type/...` spelling that is accepted today becomes + refused: the retired route matched a **literal** path segment and never + consulted the fold. The 2026-08-17 re-weigh (item 3) defers that break with no + scheduled window. The legacy dispatcher branch in `runtime/src/domains/meta.ts` + also still matches both literals; narrowing it is not part of this step. + +### Patch Changes + +- 128684d: **Behaviour change (security tightening):** the `/api/v1/automation` **definition writes** now require the `manage_metadata` capability (#10145). + + `POST /api/v1/automation`, `PUT /api/v1/automation/:name` and `DELETE /api/v1/automation/:name` — `automation.create` / `automation.update` / `automation.delete` on the SDK — were reachable by **any authenticated caller**. They now answer **403 `PERMISSION_DENIED`** unless the caller holds `manage_metadata` (ADR-0066 D1's authoring capability), the same key the sibling `PUT /api/v1/meta/:type/:name` and every state-changing `/api/v1/packages/*` route already demand. Engine self-invocation (`isSystem`) bypasses, as on every other capability gate. + + **Existing credentialed callers that author flows over HTTP will start getting 403** and must be granted `manage_metadata`. A flow is authored metadata: this closes the last write door onto the metadata plane that did not ask the metadata plane's question. + + What was measured on a walled multi-organization deployment (`OS_TENANCY_POSTURE=isolated`): a plain tenant org owner holding `organization_admin` — the same session answered 403 by `PUT /meta/:type/:name`, `POST /ai/tools/:tool/execute` and `POST /packages/*` — created, modified and deleted flows through this door, all 200. Flow definitions are registered at **environment** scope, not organization scope, so the write crossed the tenant wall: a shipped flow deleted by one tenant read 404 for the actor, for an unrelated tenant **and** for the platform admin, and an injected flow read 200 for all three. + + **Deliberately unchanged — execution is not authoring:** + + - `POST /automation/:name/trigger` and the legacy `POST /automation/trigger/:name` **run** a flow. They keep their existing posture (authenticated, plus the flow's own `runAs` authorization envelope). + - `POST /automation/:name/runs/:runId/resume` is already fail-closed through the suspended node's `resumeAuthority`; a metadata capability in front of it would refuse the very user the flow paused for. + - `POST /automation/:name/toggle` mutates engine enablement rather than a definition, and is filed separately rather than folded into a security fix. + - The reads (`GET /automation`, `GET /automation/:name`, the run surfaces) are untouched; run-state reads keep their `sys_automation_run` grant. + + The gate sits ahead of the service probe and ahead of body validation, so a refused caller neither writes anything nor learns from a 501-vs-403 whether the deployment mounts automation at all. +- 9f483d9: Repair six false API claims in the published `@objectstack/runtime` README + (#10368). The README is in the package's `files` array, so it is the page npm + renders — a reader following it wrote code that could not compile. + + Found by hand-adjudicating every call site in that document that + `check:published-readme-exports` reports under `NOT read:` — receivers built + from free variables, parameters and globals, which neither the gate nor a human + reader can type by looking. 30 sites on 17 receivers were read; the repairs below + are what came out. + + - `engine.update('user', user.id, { name: 'Jane' })` → `engine.update('user', + { id: user.id, name: 'Jane' })`. `IDataEngine.update` is + `(objectName, data, options?)`; there is no `id` parameter. A by-id update is + identified by a truthy scalar `data.id` (or `options.where.id`) — the rule + `resolveEngineUpdateDispatch` in `@objectstack/metadata-core` defines. + - `engine.delete('user', user.id)` → `engine.delete('user', { where: { id: user.id } })`. + `IDataEngine.delete` is `(objectName, options?)`; the id belongs in + `options.where.id` (`assertEngineDeleteDispatch`). Passing it positionally + landed the id in the options bag. + - The **Interface Methods** bullet list restated both wrong signatures, so it is + corrected in the same edit — a repaired example beside a bullet list that still + contradicts it is not a repair. + - `reply.code(429).send({ retryAfterMs })` in the rate-limiting recipe → + `res.status(429).json({ retryAfterMs })`. `reply.code()` is Fastify; this + package's HTTP contract is `IHttpResponse`, which spells the step + `status(code)` and whose `send` takes `string | Uint8Array | ArrayBuffer`, not + an object. The `docs/HARDENING.md` recipe the same section links to already + answers 429 through the framework's own JSON responder. + - `status: res.statusCode` in the middleware example → dropped. + `IHttpResponse` has no `statusCode`; a response's status is observed through + `IHttpServer.afterResponse` (`HttpResponseObservation.status`), not read off + the response inside middleware. + - The `PluginContext` interface block declared `logger: Console` and + `getKernel?(): any`. The real contract (`@objectstack/core`) is + `logger: Logger` and a required `getKernel(): ObjectKernel`. + + Documentation only — no runtime, type or export change. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [2570ab0] +- Updated dependencies [5886ee6] +- Updated dependencies [02d56b4] +- Updated dependencies [46cfa5b] +- Updated dependencies [b20c8d2] +- Updated dependencies [6ce58a7] +- Updated dependencies [d23e3a0] +- Updated dependencies [9a1ed7a] +- Updated dependencies [f3a8134] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [5b0af2b] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [9e04c3e] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [a16ff50] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [0c24898] +- Updated dependencies [16cef97] +- Updated dependencies [4389fe9] +- Updated dependencies [923c424] +- Updated dependencies [b419135] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [86a8ec9] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [24ba050] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/plugin-auth@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/driver-sql@17.2.0 + - @objectstack/driver-memory@17.2.0 + - @objectstack/service-i18n@17.2.0 + - @objectstack/metadata-protocol@17.2.0 + - @objectstack/plugin-security@17.2.0 + - @objectstack/rest@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + - @objectstack/metadata@17.2.0 + - @objectstack/driver-sqlite-wasm@17.2.0 + - @objectstack/formula@17.2.0 + - @objectstack/service-cluster@17.2.0 + - @objectstack/service-datasource@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 293a47af3e..253bf53522 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.1.0", + "version": "17.2.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 411287dfda..0c1ffb8678 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/sdui-parser +## 17.2.0 + ## 17.1.0 ## 17.0.0 diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 9a7e619452..11b301b892 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "17.1.0", + "version": "17.2.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 7e3d7eddc4..fee8980040 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,76 @@ # Changelog — @objectstack/service-analytics +## 17.2.0 + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- 112a8c6: Apply a dataset's definition-level `filter` on the ObjectQL analytics path + (#10413, phase 1). `/api/v1/analytics/query` served by a driver that reports + `objectqlAggregate` but not `nativeSql` (MongoDB, the memory driver) reached + `engine.aggregate` with no `filter` key at all: the dataset's own scope — a + `filter: { is_deleted: false }` on the dataset definition — was dropped, so + every measure aggregated the whole table while the dashboard door, on the same + cube and the same measure names, answered the scoped numbers. The scope is now + ANDed into the strategy's whole-call filter (never merged key-by-key, so a + caller's own `where` and the time windows cannot be overwritten by it), and the + representative SQL echo renders it too. + + Per-MEASURE `filter`s on this path are still not applied: an + `engine.aggregate` aggregation is `{ field, method, alias }` and cannot carry a + predicate of its own. Widening that contract is #10576; lowering the measure + filters into it is phase 2 of #10413. The native-SQL path already applies both + (#10298). +- 6439f8b: Analytics measures are now compiled from everything they declare — `aggregate`, `field` **and** `filter` — on both the dashboard path and `POST /api/v1/analytics/query`. + + **Reported figures change, and the new ones are the declared ones.** Two corrections, both of which move numbers a dashboard or an API consumer is already reading: + + - A measure written `{ aggregate: 'count', field: 'some_column' }` used to compile to `COUNT(*)` and count **rows**. It now compiles to `COUNT("some_column")` and counts **non-null values**. Any such measure will report the same number as before or a **smaller** one, and a rate built on top of it (a numerator over a total) will drop accordingly — a "100%" tile whose column was mostly empty was reading its own denominator. + - `POST /api/v1/analytics/query` used to drop every per-measure `filter`, and the dataset's definition-level `filter` with it, returning unfiltered aggregates under the author's measure names. It now applies both, so the endpoint answers what the dashboard already answered for the same cube. Figures pulled through the API — agent tools, exports, downstream reports — will move to the filtered values; a measure declaring `filter: { stage: 'closed_won' }` stops counting every row. + + Measures that declare no `field` still compile to `COUNT(*)`, and a cube that is not a compiled dataset (an inferred or manifest cube) emits byte-for-byte the statement it did before. Measure filters lower to portable `CASE WHEN` conditional aggregates rather than `FILTER (WHERE …)`, which MySQL does not have. + + If a saved figure or a screenshot disagrees with what the platform now reports, the new number is the one the metadata declares. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index c3a4f21276..c2cf5a3b37 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.1.0", + "version": "17.2.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 598a17146c..ac9bbcecb5 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,76 @@ # @objectstack/service-automation +## 17.2.0 + +### Minor Changes + +- 73d9795: Time-relative sweeps are now idempotent per matched window (#10220). Previously the sweep + held no cross-tick memory, so every re-scan of the same window re-dispatched the same + records — a 5s-interval flow minted 15 duplicate reminders in ~70s, and even under a daily + cron a kernel rebuild re-dispatched the day's window. + + - `@objectstack/service-automation` — new platform object `sys_flow_dispatch`: a persisted + dispatch-claim ledger (ADR-0057 telemetry retention, 30 days), registered alongside + `sys_automation_run` and exposed as `AutomationEngine.claim(key): Promise` on + the automation service surface (check-and-record; a concurrent duplicate insert re-reads + and reports the key as already claimed). When no ObjectQL engine / registration is + available the engine degrades to in-process dedup and logs the weakened guarantee once; + when the ledger errors, the claim falls back to the in-process check for that key so a + store outage never blocks a dispatch (availability over strict-once). + - `@objectstack/trigger-schedule` — the time-relative sweep computes a dispatch key from + the MATCHED WINDOW's identity and claims it before launching: offset mode keys on + `(flowName, recordId, windowDay, offset)` — so a dateField edit that moves the window + legitimately re-fires — and range mode keys on `(flowName, recordId, sweepDay, + rangeSpec)`, preserving the documented `withinDays` semantic ("fires every day the + record stays in range") while never firing twice in one day. The trigger resolves the + claim surface structurally from the automation service; without one it dedups + in-process and warns once. + - `@objectstack/spec` — `sys_flow_dispatch` added to `PLATFORM_OBJECTS_BY_PACKAGE` under + `service-automation` (registry conformance). + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/formula@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index bae3224def..dc4ce9704c 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.1.0", + "version": "17.2.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 acfd34bdbe..1b7e9bf205 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,50 @@ # @objectstack/service-cache +## 17.2.0 + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 2ec3979965..5be7d646b4 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.1.0", + "version": "17.2.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 9c3d3f1465..a51116f163 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,38 @@ # @objectstack/service-cluster-redis +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/service-cluster@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index 782dd3efd1..2fded5360b 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.1.0", + "version": "17.2.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 2e59c61b29..70322ba9a0 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/service-cluster +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index d50e2b1776..af9ba8af13 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.1.0", + "version": "17.2.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 2c5bb7c7a0..0c8d5b38a6 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/service-external-datasource +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 7ad826c94d..b80c7a6e19 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.1.0", + "version": "17.2.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 70cb91861e..181c5d5dce 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,50 @@ # @objectstack/service-i18n +## 17.2.0 + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index 0990e19c4e..092f4db47a 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.1.0", + "version": "17.2.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 1f44dcf02b..0f18644848 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,53 @@ # @objectstack/service-job +## 17.2.0 + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 770bf5bc54..f4938e76b1 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.1.0", + "version": "17.2.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 9b5a6eac39..9081049e5d 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,121 @@ # @objectstack/service-knowledge +## 17.2.0 + +### Minor Changes + +- e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an + optional `error` now declare a **non-optional** `warn`, so a durability report + always has somewhere to land (#9754, #10556). + + `minor`, not `major`: during the launch window this stack ships breaking changes + as `minor` — every publishable package versions in lockstep, so a `major` would + promote the whole release. `patch` would be wrong in the other direction, because + this *can* break a consumer's build. + + `error` stays optional on every one of these types — hosts legitimately inject + reduced sinks, and requiring `error` was measured and rejected as #9754 option C. + What changes is that its *absence* now has a declared, guaranteed destination. + Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the + type cannot reach, so **no runtime behaviour changes**: nothing that printed + before stops printing, and nothing silent starts printing. + + ### Who has to change, and what to do + + Only a caller that hands one of these sinks an object with **no `warn` method** — + for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no + rename, no removal, and no stored value or metadata key to rewrite. Every + construction site inside this repo already supplied one, so the in-repo cost was + zero; the compile error is reserved for the callers that were silently discarding + these reports. + + The affected types, by package: + + - `@objectstack/cloud-connection` — the internal `PluginContext['logger']` + - `@objectstack/metadata-protocol` — `IndexMigrationLogger` + - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks` + - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger` + - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal + `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']` + - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions` + - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']` + - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`, + `rule-hooks` and `record-share-cascade` + - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions` + - `@objectstack/service-knowledge` — `KnowledgeLogger` + + `AuthManagerOptions['logger']` is the one most likely to be reached from outside: + `AuthManager` is public surface, its `logger` option stays optional, and a logger + that *is* supplied must now carry `warn`. The only non-test construction site in + this repo passes the kernel `Logger`, whose `warn` is already required. + + `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger + field to `{}`. The field is now honestly optional rather than holding an empty + object that declared it could report and discarded everything. Behaviour is + unchanged in both directions. + + + +### Patch Changes + +- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in + the project `create-objectstack` scaffolds — at the canonical docs origin + `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling. + + Both spellings reach the same pages (the alias redirects to the apex, + path-preserving), so no link was broken. The reason it needs a release rather + than an in-repo fix alone: a README ships inside the npm tarball, so the + version already on npm keeps showing the old host to every reader of the + package page until a new one is published. +- 6d5c4fa: Release these plugins' resources from `destroy()`, the teardown hook the kernel + actually calls (#10371). `Plugin` declares `init()`, `start?(ctx)` and + `destroy?()` — and no `stop()` — so `ObjectKernel.performShutdown()` and + `LiteKernel.destroy()`, which walk the plugins in reverse calling + `plugin.destroy()`, walked straight past every plugin whose teardown was spelled + `stop()`. `await kernel.shutdown()` resolved with the reports dispatcher still + armed, the REST/OpenAPI/Slack connectors still registered on the automation + engine, the approvals SLA escalation job still scheduled, and the knowledge + event-sync subscription still open. + + Each teardown body now lives in `destroy()`. `stop()` is retained as a + delegating alias with its parameter made optional, so an embedder that learned + to call it directly — precisely because the kernel never did — keeps working + unchanged. No export is removed and the `Plugin` interface is untouched. + + Same defect as #9371 in `@objectstack/service-messaging`, which surfaced as + fully green test runs exiting 1 on `EnvironmentTeardownError` and being evicted + from the merge queue. +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 2fabe783f6..0b8bc18c9b 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.1.0", + "version": "17.2.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 e75c5f5dc6..d18d6cc42b 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,70 @@ # @objectstack/service-messaging +## 17.2.0 + +### Patch Changes + +- 8163a1c: Classify the delivery dispatchers' predicate writes on `sys_http_delivery` and + `sys_notification_delivery` as global environment sweeps (#10673). On a walled + deployment (`OS_TENANCY_POSTURE=isolated|group`) the SQL driver's tenant-audit + gate reported every `updateMany` these outboxes issue from the claim path as an + un-isolated write. The audit was right to ask: both objects are tenant-scoped + via `organization_id`. The answer is that these six writes — the + visibility-timeout reap and the atomic claim in `SqlHttpOutbox.claim`, + `SqlNotificationOutbox.claim` and `SqlNotificationOutbox.claimDigest` — are + issued by a `setInterval` dispatcher tick under a cluster lock, with no request + context and no tenant anywhere in the `ClaimOptions` contract, and they must + cross organizations: one outbox drains the whole environment's queue, so a + per-organization predicate would strand every other organization's deliveries. + They now pass `bypassTenantAudit` through a single documented helper that + carries that warrant. Diagnostics only — per its spec the flag never changes + what a write touches, and the row-level `ack` / `redeliver` writes are + unaffected. +- 900e489: **Fix:** `MessagingServicePlugin` now releases its delivery dispatchers on `kernel.shutdown()`. Previously they kept running after shutdown had resolved (#9371). + + The plugin starts two `setInterval` dispatchers at `kernel:ready` — `NotificationDispatcher` over `sys_notification_delivery` and `HttpDispatcher` over `sys_http_delivery` — and released them from a method named `stop()`. The kernel's plugin teardown hook is `destroy()` (`Plugin.destroy?()` in `@objectstack/core`; the only teardown `ObjectKernel.performShutdown()` and `LiteKernel.destroy()` invoke), and `stop()` is not on that interface, so **nothing ever called it**. Both dispatchers went on claiming and updating delivery rows after `await kernel.shutdown()` returned. Measured on the new pin: 48 further delivery reads/writes in the 80 ms following a resolved shutdown. + + The teardown body now lives on `destroy()`. `stop()` is **retained as an alias** — it is public API of an exported class, and an embedder may well have learned to call it directly precisely because the kernel never did. No call site has to change, and no accept/reject behaviour of any contract moves. + + **Why it was invisible in production, and where the bill landed.** `start()` `unref()`s both timers, so a long-lived host process still exits and the leak is silent. Under vitest the worker process is alive throughout teardown, so a tick fires *after* a test file is over, reads a delivery table through a driver the suite already disconnected, and `SqlDriver`'s console fallback warns. `console.*` inside a vitest worker is an RPC to the main process (`onUserConsoleLog`); one issued after `rpcDone()` has snapshotted the pending set is rejected by `$rejectPendingCalls` as `EnvironmentTeardownError: [vitest-worker]: Closing rpc while "onUserConsoleLog" was pending`. Nothing awaits that promise, so it lands as an unhandled rejection and fails a run in which every test passed — twice measured on `examples/app-showcase` (334/334 and 337/337 green, exit 1, a merge-queue eviction each time). The width of the window is the duration of `rpcDone()`, which is why it only ever fired on a loaded queue runner and never on the PR-side run of the identical diff. + + Suites that boot a kernel with this plugin get quieter and finish cleaner as a result: over 48 loaded runs of the affected showcase file, console output emitted after the file's own `afterAll` went 3 → 0, and console RPC round-trips per run roughly halved (6574 → 3456 in aggregate). +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index b472e583d2..6669ce7f65 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.1.0", + "version": "17.2.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 fa580e6e1a..15ecca64b0 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/service-package +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/metadata-core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 9c835484cd..ffcf73eab4 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.1.0", + "version": "17.2.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 6d344cad5a..f941b92d1e 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,44 @@ # @objectstack/service-queue +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index a6c869ac55..09d560f8d3 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.1.0", + "version": "17.2.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 c816a917a3..bcc0132977 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,44 @@ # @objectstack/service-realtime +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index 9fa6110fa7..0299160bd9 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.1.0", + "version": "17.2.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 127d15619a..76e3506049 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/service-settings +## 17.2.0 + +### Patch Changes + +- 1ec36b7: **Behaviour change (tightening, boot-time only):** a settings write issued before `SettingsService`'s data engine is bound is now **refused loudly** instead of resolving successfully while nothing reaches `sys_setting` (#10159). + + `upsertRow` picks its store on `if (this.engine)`, and the engine is bound in exactly one place — `SettingsServicePlugin` registers a `kernel:ready` hook from its `start()` and calls `bindEngine` inside it. `kernel:ready` handlers run in registration order and every plugin's `init()` runs before any plugin's `start()`, so **every `kernel:ready` hook registered from an `init()` fires inside that window**. A `set()` from there landed in the in-process memory fallback, re-resolved off that same array, and handed the caller a fully resolved value; `sys_setting` received nothing, and neither audit ledger recorded anything (both sinks bind on the same `bindEngine` call). Nothing was logged at any level, because the write did not fail — it succeeded against the wrong store. + + **What an operator will now observe.** A write in that window throws `SettingsEngineNotBoundError` — code `SETTINGS_ENGINE_NOT_BOUND`, status **503** — whose message names the window, the reason, and the fix: move the write to `kernel:bootstrapped` (or later), which fires strictly after every `kernel:ready` handler has settled. Previously that same call returned a resolved value and the setting was silently absent after restart. + + **Nothing outside the window changes.** The refusal is armed only by the new opt-in `SettingsServiceOptions.engineBindPending`, which `SettingsServicePlugin` sets in `init()` and clears on both branches of its `kernel:ready` hook — by `bindEngine` when `objectql` is present, or by the new `SettingsService.settleWithoutEngine()` when it is not. So: + + - a `SettingsService` constructed directly (unit tests, bootstrap, control-plane mock) keeps the in-memory fallback exactly as before — it declares no pending bind, and the guard never arms; + - a lean kernel with no `objectql` keeps the plugin's deliberate degradation: once its `kernel:ready` hook has established that no engine is coming, writes resolve into the memory fallback again (now with a `warn` saying those values are lost on restart); + - reads are untouched in every state, so an ordinary boot-time read of a setting still resolves. + + No shipped caller wrote settings inside the window, so no existing startup sequence becomes an error. + + `SETTINGS_ENGINE_NOT_BOUND` is registered in `ERROR_CODE_LEDGER` per ADR-0112. The status is declared on the error class rather than at an HTTP door because no door can reach it: the window closes at `kernel:ready`, and HTTP servers open their socket at `kernel:listening`, strictly after. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index d883f40592..821a7a0b1a 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.1.0", + "version": "17.2.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 37f7acb166..ecbd301b8e 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,45 @@ # @objectstack/service-sms +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [5b0af2b] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [86a8ec9] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/plugin-auth@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index daa2f7bd65..74a98f4a4c 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.1.0", + "version": "17.2.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 243e259312..cb60c7c14c 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/service-storage +## 17.2.0 + +### Patch Changes + +- da891e0: **Behaviour change (tightening):** updates of `sys_attachment` rows are now authorization-gated, where they previously ran with **no record-level check at all** (#10091). + + `installAttachmentAccessHooks` gated insert (parent-edit access, `uploaded_by` server-stamped) and delete (uploader-or-parent-editor), but registered **no `beforeUpdate` hook** — so under the default member permission sets (wildcard CRUD, no row scoping) any member could rewrite any attachment row: re-point `parent_id` at a record they cannot read, or rewrite `uploaded_by` and then walk through the delete gate's uploader shortcut. The `sys_comment` kit — explicitly derived from this one — has gated update with the same rule since #4630; the source kit was missing the limb its derivative copied. + + The new `beforeUpdate` gate narrows the accept set as follows; if a currently-working update starts failing, the caller lacked rights the other two verbs already required: + + - **Row rule:** the caller must be the attachment's uploader OR hold edit on its parent record (`ISharingService.canEdit`; degrades to caller-scoped parent READ visibility when no sharing service is present). A multi-row update requires EVERY matched row to pass. Refusals are HTTP 403 with the **standard catalog code `RECORD_NOT_ACCESSIBLE`** (ADR-0112: generic permission conditions take the catalog — the same envelope the comment kit's update gate emits; the insert/delete gates keep their grandfathered `ATTACHMENT_*` codes). + - **Re-point rule:** an update that changes `parent_object`/`parent_id` must additionally satisfy the attach rule on the NEW parent (edit access, read visibility in degraded mode) — 403 `ATTACHMENT_PARENT_ACCESS` otherwise, and a re-point half that names no record (`null`/empty) is refused rather than left to validation. + - **Unscoped shape:** an unscoped `multi: true` update (no `where` at all) is refused outright via the `dispatchUnscopedMultiWrite` whole-operation dispatch (#9974), mirroring the delete verb's #4757 refusal. The explicit match-all `where: {}` is still accepted and authorized per row. + + System-context operations and context-less programmatic calls on bare kernels bypass the gate exactly as the insert/delete gates do. `uploaded_by` is deliberately not re-stamped on update: the caller is already verified as uploader or parent editor before the write proceeds, so the rewrite-then-uploader-delete escalation is closed by the row rule itself. +- a38c3ff: **Bug fix (retention leak):** an UPDATE that re-points a `sys_attachment` row's `file_id` now detaches the PRIOR file the same way deleting that row would — tombstoning it when the re-pointed row was its last reference (#10171). + + `installAttachmentLifecycleHooks` registered only delete-side and insert-side handlers, so a `file_id` re-point left the old `sys_file` sitting at `status='committed'` with zero join rows and no `deleted_at`. That is not the module's "fail toward retention" bias, which buys a **later** look: `sys_file`'s declared lifecycle nominates a row for the sweep only through `ttl { field: 'deleted_at' }` or `retention { onlyWhen: { status: 'pending' } }`, and a silently detached file matches neither — so the reap guard is never asked about it and the storage bytes are stranded permanently, with no later re-examination. + + The new `afterUpdate` handler fires only when the payload actually carries `file_id` and the value actually changes, then runs the existing orphan rule (zero remaining join rows, attachments-scope, committed) on the prior id. It is best-effort like its siblings and never blocks the user's write; with no pre-image available it tombstones nothing, keeping the file. + + The departed id comes from the engine-bound pre-image `ctx.previous`, **not** from a `beforeUpdate` stash mirroring the delete pair. Since #5574 (ADR-0058 Addendum II D1/D2) a predicate write dispatches one fresh context per matched row in each phase, so a stash written in `beforeUpdate` reaches `afterUpdate` on the by-id path and is lost on the predicate path — a stash-based twin would have been silently half-dead on exactly the multi-row updates that orphan the most files. Reading `previous` also adds no driver round trip: the prior-row read is memoized per operation and already demanded on this object. + + **No revival leg was added**, deliberately. Re-pointing a row ONTO a grace-window tombstone is already handled by the reap guard's sweep-time re-verification, which resolves current references, un-tombstones the file and vetoes the reap rather than reclaiming bytes. A second revival mechanism here would be a duplicate answer to a question that already has one. +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/observability@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index 932cd450ca..eac112af63 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.1.0", + "version": "17.2.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 b52dd0cfd8..32ef74be63 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,692 @@ # @objectstack/spec +## 17.2.0 + +### Minor Changes + +- 6936d07: `engine.aggregate` honours a per-aggregation `filter` (#10576, the contract + half of #10413). `AggregationNodeSchema.filter` — declared since #4286 but + marked experimental and enforced by nothing — is now live with SQL + `FILTER (WHERE …)` semantics: the predicate narrows the SOURCE rows that one + aggregation reads while sibling aggregations in the same call keep seeing + every row of the group, so a measure-scoped filter (`stage: 'closed_won'`) + can finally reach the engine instead of being silently dropped (the #10413 + wrong-numbers defect on the ObjectQL analytics path). The + `StrategyContext.executeAggregate` bridge (`@objectstack/spec/contracts`) + gains the same optional `filter` on its aggregation entries so analytics + strategies can lower measure filters into it (#10413 phase 2 consumes this + seam next). + + Execution is the correct-first two-tier shape date bucketing and HAVING use: + the engine lowers filtered aggregations in memory for every driver (unknown + operators refuse loudly with `INVALID_FILTER`/400 naming the aggregation + position; a group emptied by its filter answers the ruled empty-group values + — count/sum 0, avg/min/max null). No driver compiles conditional aggregation + natively today, so each native aggregate face (driver-sql — inherited by + driver-sqlite-wasm and Turso local —, the Turso remote transport, + driver-mongodb's pipeline builder, driver-memory's `performAggregation`) + refuses a directly-delivered per-aggregation filter with + `NOT_IMPLEMENTED`/501 instead of silently aggregating the unfiltered rows. + Aggregations without a `filter` are byte-identically unchanged, including + their native pushdown path. +- 9f05b7d: Declare `organization_id?: string | null` on `ApprovalRequestRow` and + `ApprovalActionRow` (#10331). The approval service has always stamped the + tenancy placement on the rows it inserts — and returns it on request-row + reads — but the published contract types omitted the field, so consumers had + to cast past the contract to reach it. Type-only widening: one declared + optional field per row, no runtime change. +- 7d2d112: Add an optional `sharingModel` slot (enum `private | public_read | public_read_write | controlled_by_parent`) to `BlueprintObjectSchema` and, as a required-but-nullable key, to the OpenAI-strict structured-output mirror (`SolutionBlueprintStrictSchema`). The propose-stage LLM can now author a deliberate Org-Wide Default (OWD) choice — e.g. `private` for an object the user described as personal/sensitive — instead of having the platform's deterministic default silently override the intent expressed at propose time. Omitting the key (or emitting `null` in the strict mirror) still defers to the platform default (business object → `public_read_write`, master-detail child → `controlled_by_parent`). +- 914c413: fix(observability): **BREAKING** — `http_request_errors_total` is retired (ADR-0049 enforce-or-remove, #9834) + + **⛔ If you have a Grafana panel, an alert rule or a recording rule keyed on + `http_request_errors_total`, it will read a FLAT ZERO after this upgrade.** That + zero is the removal, not a healthy server, and it is the one way this change can + hurt you — nothing throws, nothing warns, the series simply stops receiving + samples. Rewrite the query before you deploy. + + Maintainer ruling 2026-08-20: **RETIRE**. The name was declared in `SEMCONV` as + part of a stable namespace *"so hosts can wire alerts/dashboards against it"*, + but the only emitter was `@objectstack/runtime`'s `instrumentRouteHandler`, + applied only by the dispatcher's own route Proxy — so the series never saw + auth's `getRawApp()` mount, the REST data API via `RouteManager`, or any other + inbound surface. Its two siblings in the same HTTP family moved to the + `IHttpServer.afterResponse` transport seam (`http_requests_total`, #9650/#9835; + `http_request_duration_ms`, #9834/#10004) and this one could not follow: + `HttpResponseObservation` carries `{method, routePattern, status, elapsedMs}` + and **no throw signal of any kind**, so every transport-side shape would have + counted a *different* population rather than the same one more widely. + + Migration (FROM → TO): + + | Wrote | Write instead | + |---|---| + | `rate(http_request_errors_total[5m])` in a panel or alert | `rate(http_requests_total{status=~"5.."}[5m])` — emitted by the transport, so it covers every inbound surface instead of the dispatcher's routes only | + | `sum by (route) (http_request_errors_total)` | `sum by (route) (http_requests_total{status=~"5.."})` | + | `SEMCONV.httpRequestErrorsTotal` / `RUNTIME_METRICS.httpRequestErrorsTotal` in host code | Delete the read. Both members are gone; `tsc` reports the missing property at the read site. | + + One-line fix: replace the metric name with `http_requests_total{status=~"5.."}`. + + + + **The replacement is wider, not merely different.** The retired counter was + divergent from a 5xx rate in *both* directions, measured: the dispatcher answers + its own errors through `errorResponseBase`, which sets a status and does **not** + re-throw — so the counter **missed** those — while its `catch` incremented + unconditionally, so a **thrown 4xx WAS counted** as an error. And + `http_requests_total` already carries a `status` label, so a status-class error + counter was fully derivable from data the transport already publishes. Prove the + new query wider rather than merely non-empty: make an auth route or a REST + data-API route answer 5xx and confirm it moves, where the retired counter would + not have moved at all. + + **If what you were actually alerting on was "a handler threw rather than + returning an error envelope"** — the one signal this counter uniquely carried — + that is the `errorReporter`, not a metric. Wire an `ErrorReporter` adapter + (Sentry / Datadog / your own); it still fires on every 5xx throw and is + untouched by this change. + + What is NOT removed: `http_requests_total`, `http_request_duration_ms`, + request-id propagation, the 5xx error reporter, and the + `res.__obsRecordedError` side channel that carries a swallowed error to it. The + dispatcher still instruments every route it mounts; it just no longer publishes + a fourth series whose name promised more coverage than it had. +- 55809a0: fix(spec): reject the retired `key`/`defaultValue` spellings in inline locale maps BY NAME, in any combination — and stop claiming the retired form "resolves to nothing" (#10492) + + Two legs, both on `InlineLocaleMapSchema` in `packages/spec/src/ui/i18n.zod.ts`: + + 1. **Message accuracy.** The `INLINE_LOCALE_KEY` rejection message said the + retired key-reference form (#5055) "resolves to nothing". Measured false: + both resolvers — `resolveI18nLabel` here and objectui's `pickLocalized`, + parity-pinned — fall through to their last resort (first string value, in + key insertion order) and return the raw dotted key, which renders as the + visible label. The message now states the measured behaviour. + + 2. **Enforcement hole closed.** `key` is three letters — syntactically a valid + BCP-47 primary subtag — so `{ key: 'common.save' }` alone parsed as a + "language `key` inline locale map" and painted `common.save` on screen; the + pair form was rejected only because `defaultValue` fails the tag grammar. + The key pattern now refuses the two retired spellings by name, in any + combination, matching the emitted type's `{ key?: never; defaultValue?: + never }` narrowing (#9925, maintainer ruling 2026-08-19, option B). This is + an enforcement gap of the #5055 retirement, not a new contract: nothing else + is denied — real 2–3 letter subtags (`deu`, `fra`, `yue`) still parse. + + FROM → TO: a label authored as `{ key: '' }` (or any inline map + carrying a `key`/`defaultValue` entry) is now refused at parse time with the + named message; write the inline locale map form `{ en: '…', 'zh-CN': '…' }`, + or a plain string resolved through a translation bundle. This is the same + prescription the #5055 retirement and the #9925 type narrowing already carry — + the runtime now enforces what the type already refused. + + +- 2306a76: fix(spec): `theme` / `analytics_cube` are validated at the `/meta` write door (#10194) + + The two doors #6245 left open, closed the same way. Both are declared, + authorable stack collections with real `.strict()` schemas — + `defineStack({ themes })` validates with `ThemeSchema`, + `defineStack({ analyticsCubes })` with `CubeSchema` — yet neither was bound in + `UNREGISTERED_KIND_SCHEMAS`, so `getMetadataTypeSchema()` answered `undefined` + and `saveMetaItem` took its documented "unregistered type → store without + validation" branch: a body the stack door strictly refuses was stored, + unvalidated and badged `success: true`, through the metadata door. For `theme` + that is the console's own styling surface — a malformed one failed at render + rather than at write, with nothing at the write point to say so. + + **FROM** `PUT /meta/theme/:name` / `PUT /meta/analytics_cube/:name` with any + JSON → `200 { success: true }`, stored unvalidated. + **TO** a malformed body → `422 INVALID_METADATA` with structured `issues[]`, + the same envelope every other kind already returned. A well-formed body is + accepted exactly as before. + + Each entry binds the **same schema its stack collection is validated against** + (`ThemeSchema` at `stack.zod.ts` `themes:`, `CubeSchema` at `analyticsCubes:`), + and that closing invariant is now pinned by identity for all five map entries. + + **No new capability surface.** Shape validation only: no `MetadataTypeSchema` + member, no `DEFAULT_METADATA_TYPE_REGISTRY` entry, so every authorization + verdict keeps taking the identical "no static entry ⇒ synthesised + `allowRuntimeCreate: true`" branch. The write *door* is unchanged; only the + 422 is new. #2657's B/C decision on whether these should become kinds is + untouched and unprejudged. `rag_pipeline` is deliberately not bound — it has + no stack collection to take a schema from (#6242 row 2). + + Graded **minor**, following #6245's landed precedent for the identical change + (itself following #5271): a write that previously returned 200 can now return + 422. Nothing well-formed changes behaviour, but a caller relying on the API + accepting malformed bodies will see the difference. + + **One schema change rides along per kind, and it is load-bearing.** + `Theme` and `Cube` now declare the ADR-0010 protection envelope (`_lock`, + `_lockReason`, `_lockSource`, `_lockDocsUrl`, `_packageId`, `_packageVersion`, + `_provenance`) — the sharing_rule precedent from #6245: both metadata load + paths call `applyProtection` on **every** type, and these shapes are + `.strict()`, so binding the door without the spread would have aimed the new + 422 at the runtime's own stamp instead of at malformed author input. Additive + and internal-only — no authored field changes. +- a40dcc1: feat(spec): retire `MetricSchema.filters` — the per-metric raw-SQL filter nothing read (#10414, ADR-0049) + + **BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep + launch-window convention ships it as `minor`; the migration prescription is + registered under protocol major 18, where `os migrate meta` users will look). + + `filters` on a cube metric (`filters: [{ sql: string }]`) was a real authoring + surface — `defineCube()` parses an author literal and + `defineStack({ analyticsCubes })` carries every cube through `StackSchema.parse` + — with ZERO consumers, measured with a positive control: no `.filters` read in + `service-analytics` or any driver's non-test code, while the neighbouring + `format` key IS read. `NativeSQLStrategy.resolveMeasureSql` and + `ObjectQLStrategy.resolveMeasureAggregation` both wrap the metric's `sql` in + the aggregate and never look at `filters` — so a hand-authored + `filters: [{ sql: "stage = 'closed_won'" }]` parsed, registered, and silently + returned the UNFILTERED aggregate under the author's metric name. That is the + #10298 dataset-measure failure for a hand-authored cube; the dataset half was + repaired through its own structured channel (#10411), which left this key inert + with the fix built around it. The raw-SQL fragment also ran against the + platform's structured-`FilterCondition` direction: it cannot be parameterized, + re-targeted per driver dialect, or walked by the lint filter rules + (`packages/lint/src/filter-walk.ts` deliberately never enumerated it). + + **What is refused:** `filters` on a metric. `MetricSchema` is `strictObject`, + so the key is deleted from the shape and the unknown-key rejection carries the + retirement prescription via the schema's `guidance` entry (fully-qualified key, + why it was inert, the replacement channels, the `os migrate meta` pointer). + The nested `strictObject` the key carried (closed by #4001 batch D) is gone + with it. + + **What stays accepted:** every other metric key (`name`, `label`, + `description`, `type`, `sql`, `format`) parses byte-identically. Filtering + that actually works is unchanged: the query's `where` (canonical Query DSL + `FilterCondition`), the condition folded into the metric's own `sql` + expression, or an ADR-0021 dataset measure's structured `filter`. + + The retirement kit: + + - strict deletion + `guidance` prescription at the schema + (`packages/spec/src/data/analytics.zod.ts`); the `AnalyticsQuerySchema` + `filters` guidance no longer points authors at the removed key + - ADR-0087 registration: retired-key entry `data/Metric:filters` and the D2 + conversion `metric-filters-removed` (protocol 18), wired into the step-18 + chain — `os migrate meta --from 17` strips the key from every metric in + `analyticsCubes[].measures` (pure lossless delete; it never had an effect to + lose) + - pin tests (`analytics.test.ts` — the old parse-survival pin flips to a + refusal pin asserting the prescription; `analytics-strictness-batchd.test.ts` + records the nested batch-D surface as superseded) + - generated baselines/docs follow the schema (`authorable-surface/`, + spec-changes, upgrade guide, reference docs) + + ## FROM → TO + + ```ts + // before — parsed green; both SQL strategies ignored it and the query + // returned the unfiltered aggregate + defineCube({ + name: 'orders', + sql: 'orders', + measures: { + closed_won_revenue: { + name: 'closed_won_revenue', label: 'Closed-Won Revenue', + type: 'sum', sql: 'amount', + filters: [{ sql: "stage = 'closed_won'" }], + }, + }, + dimensions: {}, + }); + + // after — delete the key; express the condition where something reads it: + // query time: { where: { stage: 'closed_won' } } + // in the metric: { type: 'sum', sql: "CASE WHEN stage = 'closed_won' THEN amount END" } + // dataset measure: a structured `filter` (ADR-0021, the #10411 channel) + ``` + + +- 3ee8ddf: fix(security): **BREAKING** — `sys_position` retires the `permissions` column (ADR-0049 enforce-or-remove, #9885) + + Maintainer ruling 2026-08-20: **REMOVE**. The column — a "JSON-serialized array + of permission strings" textarea — was declared on the platform position table + while **no producer ever wrote it and no runtime path ever read it**. The + object-scoped census (every `sys_position`-naming file, with same-object + positive controls resolving `active` / `delegatable` / `is_default` / `name` + to real readers) measured it at zero on both sides: the builtin and declared + position bootstrappers set `label` / `description` / `managed_by` / `active` / + `is_default` only, and position→grant resolution consults + `sys_position_permission_set` rows plus the position `name` — never this + column. Its only reference was the `clone_position` action copying it between + rows (a copy of a value nothing writes), removed in the same stroke. objectui + was searched under the same discipline: no console surface names the column. + A free-text grant catalogue on a security object that no runtime enforces + tells an author — human or AI — that direct position-level permission strings + are a platform capability; they are not. This is an **accept-set narrowing**: + the platform stops declaring, projecting and accepting the column. + + Migration (FROM → TO): + + | Wrote | Write instead | + |---|---| + | `permissions` on a `sys_position` seed row or data-door write | Delete the key. Capability reaches a position **only** through permission-set bindings (`sys_position_permission_set` rows, created in Setup or by an app's kernel:ready binder); prose that was documenting intent belongs in `description`. | + + One-line fix: delete `permissions` from any authored `sys_position` row. + + + + Enforcement after the removal is loud, not silent: the engine's schema + preflight refuses an undeclared field with `400 INVALID_FIELD` before the + driver or any hook runs, and `PositionSchema`'s strict parse now rejects a + declared-position `permissions` key with guidance naming the binding table. + Physical columns on already-deployed databases are untouched (ADR-0045 schema + sync is additive). If position-level direct grants ever become a real need, + the column is re-declared **with a runtime reader in the same PR** — + declare-and-enforce or don't declare. +- 16cef97: Declare `outcome: 'published' | 'refused' | 'nothing_to_publish'` as a required + key on the `publishPackageDrafts` response (#10462) — the first-class + discriminant for WHICH exit answered, the fact `success` compresses into one + boolean. Before this field, a publish with nothing to promote and a genuine + refusal (pre-flight violation or ADR-0067 D2 rollback) were indistinguishable: + both answer `success: false` with `publishedCount: 0` on a 200, and the no-op + left no trace at all — an AI consumer graded the no-op as "refused and rolled + back" and burned two repair rounds on artifacts that were already correct + (cloud#1488; cloud#1492's patch discriminates on `failed.length > 0`, an + invariant the producer never stated). + + The producer invariants, now stated and pinned in the conformance suites, both + directions of each: `outcome === 'refused'` ⟺ `failed.length > 0`; + `outcome === 'nothing_to_publish'` ⟺ + `published.length === 0 && failed.length === 0`; + `success === (outcome === 'published')`. `success` keeps its exact pre-#10462 + value on every exit — a no-op still answers `success: false` — so consumers + reading only `success` see no change, and cloud#1492's `failed.length` + discrimination stays valid during its convergence onto `outcome`. The no-op + exit additionally logs one `info` line naming the package and both facts + (nothing pending, nothing refused), so that exit is no longer traceless. + + Additive for response consumers. A custom protocol implementation that serves + `publishPackageDrafts` must now emit `outcome` on every return — + `PublishPackageDraftsResponseSchema` declares it required, and the conformance + suites treat a producer return without it as a drifted seam. +- 923c424: Schema-free `/meta` spelling entry, and the package becomes tree-shakeable (#10096, #10031). + + - New fine-grained export `@objectstack/spec/meta-spelling`: the `/meta/:type` + URL-spelling contract — `META_URL_TO_SINGULAR`, `canonicalMetaUrlType`, + `metaUrlSpellingRefusal`, `unrecognisedMetaTypeRefusal` — importable for a few + hundred bytes instead of the schema graph the same symbols cost through + `/shared` (measured +246.9 KB minified / +69.7 KB gzipped marginal on a graph + already carrying `/ui` + `/kernel`). `/shared` keeps all four symbols + (re-exported from the one declaration); nothing moves or breaks. + - The map is now materialized at build time (`gen:meta-url-spelling`, gated by + `check:meta-url-spelling`). The module-load `assertMetaUrlSpellingsAgree()` + moved into that gate — same assertion, build-time enforcement home. + - `package.json` declares `sideEffects: false` (module-scope evaluation purity + measured per entry), and emitted bundles carry `/* @__PURE__ */` on deferred + schema construction, so consumer bundlers can drop schemas an entry never + reaches instead of retaining a subpath's whole module graph. + - Standing principle recorded in the package docs: a browser-reachable spec + export surface must be schema-free (maintainer ruling 2026-08-20, #10096). +- 35ad101: feat(spec): retire the `themes` carrier key and `ThemeSchema` — the authoring surface nothing ever applied (#10485, ADR-0049) + + **BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep + launch-window convention ships it as `minor`; the migration prescription is + registered under protocol major 18, where `os migrate meta` users will look). + Maintainer ruling 2026-08-21, recorded verbatim on #10485: 「B:退役授权面 — + 收掉 `themes` 载体键与 schema,`app.branding` 留作唯一颜色面;objectui 引擎代码 + 与单测保留。」 + + `defineStack({ themes })` was a real authoring surface — parsed strictly at the + authoring gate, ingested and stored by artifact ingest + (`ARTIFACT_FIELD_TO_TYPE`) — with ZERO consumers past that point, measured: + no non-test read of `.themes` or of stored `theme` items anywhere in + core/runtime/rest/services/plugins; `theme` never in `MetadataTypeSchema`, + `DEFAULT_METADATA_TYPE_REGISTRY` or `BUILTIN_METADATA_TYPE_SCHEMAS`; the only + mounted `ThemeProvider` is the app-shell chrome light/dark toggle (unrelated to + `ThemeSchema`); and no stack- or app-level key ever selected an active theme. + An author who wrote a theme shipped it through every green gate and the console + looked exactly the same. + + **What is refused:** the top-level `themes:` key. `ObjectStackDefinitionSchema` + is a `strictObject`, so the key is deleted from the shape and the unknown-key + rejection carries the retirement prescription via the schema's `guidance` entry + (removal citation, why it was inert, and the `app.branding` replacement). + `ThemeSchema`, `ColorPaletteSchema`, `TypographySchema`, `BorderRadiusSchema`, + `ShadowSchema`, `ThemeModeSchema`, `defineTheme` and the `Theme` / + `ThemeParsed` / `ColorPalette` / `Typography` / `BorderRadius` / `Shadow` / + `ThemeMode` types are removed from `@objectstack/spec` / `@objectstack/spec/ui` + (orphaned value schemas leave with their one consumer, #3950). `PUT + /api/v1/meta/theme/:name` now gets the #8421 unrecognised-type refusal — the + `themes: 'theme'` fold left `PLURAL_TO_SINGULAR` and with it the generated + URL-spelling contract — instead of the pre-#10194 store-anything branch. + + **What stays:** `app.branding.primaryColor` / `accentColor` — the one live + colour surface (objectui's `AppShell` reads it and derives `--primary`, + `--accent` and friends) — plus objectui's `ThemeEngine` / `ThemeContext` engine + code and their unit tests, explicitly retained by the ruling. Legacy stored + `theme` rows are untouched: reads still answer, DELETE still works, and + `applyConversionsToStoredItem` passes them through unchanged. + + The retirement kit: + + - strict deletion + `guidance` prescription at the stack schema + (`packages/spec/src/stack.zod.ts`); `packages/spec/src/ui/theme.zod.ts` + deleted whole + - ADR-0087 registration: retired-def entries `ui/Theme`, `ui/ThemeMode`, + `ui/ColorPalette`, `ui/Typography`, `ui/BorderRadius`, `ui/Shadow` and the + D3 **semantic** entry `stack-themes-carrier-retired` (protocol 18). Semantic + rather than a D2 conversion on the lossless-only scope guard: a stack may + declare N themes and M apps, so which palette entry becomes which app's + `branding.primaryColor` is a judgment the transform cannot make — the entry + prescribes the hand move instead of auto-deleting authored content + - ingest mapping removed (`packages/metadata/src/plugin.ts`), CLI stats row + removed, showcase example re-based on app branding + - pin tests: `stack-top-level-strict.test.ts` (refusal carries `#10485` + + `app.branding` + no rename suggestion; replacement parses green; no theme + export survives on `./ui`) and `protocol.unrecognised-meta-type.test.ts` + (`/meta/theme` refused with the ADR-0112 envelope, nothing stored) + - generated baselines/docs follow the schema (`authorable-surface/`, + `json-schema.manifest/`, api-surface, export-origins, meta-url-spelling, + spec-changes, upgrade guide, reference docs, skill references) + + ## FROM → TO + + ```ts + // before — parsed green, stored by artifact ingest, applied by NOTHING: + defineStack({ + themes: [{ name: 'corporate', label: 'Corporate', mode: 'light', + colors: { primary: '#7C3AED' } }], + }); + + // after — delete the key; colour the console where something reads it: + defineApp({ + name: 'my_app', + label: 'My App', + branding: { primaryColor: '#7C3AED', accentColor: '#06B6D4' }, + }); + // a custom CSS variable your own stylesheet consumed has no spec slot any + // more — move it into your own CSS. + ``` + + +- ceb33a9: Add `nameField` to the solution-blueprint strict mirror's object schema (required-but-nullable, matching the strict convention), so the design-stage structured output can author the ADR-0079 record-title choice instead of always deferring to the platform auto-pick. The key-parity pin between the strict mirror and the lenient schema is widened from the field schemas to the object schemas, so the next object-level divergence fails a test. +- 8012960: `lifecycle.ttl` now accepts an `onlyWhen` row filter mirroring `retention.onlyWhen`, and the shared `onlyWhen` value union gains the platform's canonical null predicate `{$null: boolean}` (on both blocks). A `transient` object that interleaves live rows with terminal audit tombstones can now spare rows defined by a value's absence — e.g. `ttl: { field: 'expires_at', expireAfter: '1d', onlyWhen: { revoked_at: { $null: true } } }` — instead of the TTL reaping backdated tombstones first. The LifecycleService Reaper passes `ttl.onlyWhen` into the same reap scope `retention.onlyWhen` already rides; declaring `ttl.onlyWhen` together with rotation storage or archive is refused at parse time, mirroring retention's guards. +- 75e9301: fix(spec): read the unknown-key lint's posture from the schema the parse applies, and report each record exactly once (#10039) + + An otherwise valid view container carrying one undeclared key produced two + contradicting messages from `defineStack`: + + ``` + WARN: defineStack: views.v1.bogusViewKey: 'bogusViewKey' is not a declared view + key, so its value is dropped at load. + THREW: ✗ views.0: Unrecognized key(s) on this view container: `bogusViewKey`. … + ``` + + The warning promises a silent drop — the view loads, minus one key — and the + refusal one step later says nothing loads at all. An author who reads the + warning and stops there draws the opposite conclusion from the truth, and a + warn channel that is sometimes really an error trains readers to discount it. + + **Root cause: the lint read posture off a different schema than the parse.** + `lintUnknownAuthoringKeys` took each collection's unknown-key posture from + `getMetadataTypeSchema(type)`. That registry answers a different question — it + names the schema for a *persisted metadata body* of that type. What + `defineStack` applies to a *stack collection entry* is the element schema in + `ObjectStackDefinitionSchema`'s own shape, and for `view` the two are not the + same object: + + - `getMetadataTypeSchema('view')` → `ViewMetadataSchema`, a strip-mode **union** + over the three persisted runtime shapes; + - `ObjectStackDefinitionSchema.shape.views` → `z.array(ViewSchema)`, and + `ViewSchema` is the `.strict()` defineView **container**. + + `lintUnknownStackKeys` has always avoided exactly this at the top level, and its + own source says why: a schema that rejects loudly must make the lint go quiet + "rather than become a second, possibly disagreeing voice". The per-collection + walker read the same rule off the wrong schema. + + The posture source is now the stack schema's own slot for the collection. + Measured across all 29 collections `PLURAL_TO_SINGULAR` names, the registry and + the stack slot agree everywhere except: + + | collection | type registry | stack slot | effect | + | --- | --- | --- | --- | + | `views` | `strip` / 91 keys | `strict` / 15 keys | **leaves the lintable set** | + | `themes`, `analyticsCubes` | unregistered | `strict` | skipped either way | + + So `connectors` is the honest remainder — it genuinely warns and drops — and no + other collection changes. + + **Second defect, same walk: every finding on a union root was emitted twice.** + `lintUnknownKeysAgainstSchema` reported the root record itself and *also* handed + that same record to `descend`, whose object arm skipped `depth === 0` ("already + reported by the caller") while its union arm had no such guard. `view` was the + only union root in the wild, so `defineStack` never showed it — the warn-once + set in `warnUnknownAuthoringKeys` absorbed the second copy — while every other + consumer of the exported walker saw both. The root report now lives in `descend` + alone, so each record is reported by exactly one place. That also closes a + latent third copy: a discriminated-union root whose branch the author *did* pick + was reported once against the merged key set and again against the branch's, and + is now reported once, against the branch — the narrower and more accurate set. + + **Nothing about what `defineStack` accepts or rejects changes.** The parse is + untouched; only which of the two existing voices speaks. + + ### API change + + `lintUnknownAuthoringKeys` and `listLintableAuthoringCollections` now take + `ObjectStackDefinitionSchema` as a **required** parameter, injected the same way + and for the same reason `lintUnknownStackKeys` already required it — + `stack.zod.ts` imports this module, so importing the schema back would close a + cycle. Required rather than optional deliberately: an omitted argument falling + back to the type registry would silently reinstate the bug, which is the + silent-loss shape this whole rule family exists to report. Every in-repo call + site (`defineStack`, `os validate`, `os compile`) already had the schema in hand + for the sibling call on the adjacent line. + + Marked `minor` rather than `patch` because of that signature, not because of any + behavioural widening — the fix itself only makes one voice go quiet. + +### Patch Changes + +- 59eb04d: Stop documenting bare `POST /api/v1/ai/chat` as agent-resolved (#10510). Two + shipped docblocks described a resolution step the route does not perform: + `client.ai.agents` claimed `/ai/chat` "talks to the environment's default + agent", and `App.defaultAgent` claimed that endpoint auto-resolves the app's + agent from `context.appName`. The bare route loads no agent and never reads + `context.appName`; the default-agent chain (explicit > `defaultAgent` of the + named app > first active) is driven by the assistant chat endpoint, + `POST /api/v1/ai/assistant/chat`, and `client.ai.agents.chat()` is the only SDK + method that reaches an agent at all. + + Both sites read as a security-relevant scoping guarantee — an agent-resolved + endpoint would have its tool offer scoped by that agent's skills (ADR-0063 + §1/§5) — so a reader auditing "which endpoints are surface-scoped?" from these + declarations got the wrong answer at both. Documentation text only: no schema + key, no parse behaviour and no runtime path changes. +- 5fa0d72: docs(spec): record the live read points of `element:button.icon` and `object-metric.icon` — the last two icon slots whose describes stated only the vocabulary (#10053) + + Both keys parsed and rendered while saying only what alphabet their value is + drawn from: `Icon name (Lucide icon)` and `Icon name (Lucide)`. That sentence is + equally true of the `page:header` `icon` retired in #6946 — refused *precisely + because no render path reads it* — so the prose could not separate a live key + from a dead one. It is the same absence that sent #9397 through a full dispatch + cycle re-deriving the accordion read point from scratch before the retirement + candidate was closed premise-overtaken. #9881 and #9972 recorded the accordion + and tab items; these two close the set for `component.zod.ts`. + + **Both are live**, re-measured rather than transcribed from the card. Note the + pin: the earlier records cite `82a94170c`, but `.objectui-sha` moved to + `9a3daf8d3` in #10137, and these were measured there. + + - `element:button.icon` — `packages/components/src/renderers/form/button.tsx:44-47` + resolves `schema.icon`, and `:69` / `:71` draw it either side of the label per + `iconPosition`, both suppressed while `loading`. + - `object-metric.icon` — `plugin-dashboard/src/index.tsx:161` publishes it as a + designer input; `ObjectMetricWidget.tsx:142` destructures it and forwards it at + `:474` to `MetricWidget`, which resolves it at `MetricWidget.tsx:312-321` and + draws it at `:373-382` in the `colorVariant`-tinted square. + + **The button is the one authorable icon on this surface that does not go through + `LazyIcon`**, and the docblock now says so, because the two paths are not + interchangeable: + + - button: `toPascalCase` (splits on `-` only) → a one-entry rename map + (`Home` → `House`) → `icons[name]` from `lucide-react`. An unknown name + resolves to `undefined` and the button renders with **no icon and no + diagnostic**. + - `LazyIcon` / `getLazyIcon` (`components/src/lib/lazy-icon.tsx:66-92`, the slot + the metric tile and every container icon use): normalises to kebab-case, + validates against Lucide's own name list, and degrades an unknown name to the + `Database` glyph. + + So a spelling that draws an icon in a tab trigger can draw nothing on a button — + previously discoverable only by reading two objectui files. + + **Nothing about what parses changes.** Both keys were already declared and + already optional; no key is widened, narrowed, retired or renamed. What is added + is the prose that makes each liveness verdict readable from the spec side alone, + and the accept-pins that keep it readable: per key, an accept carried through to + the parsed output, an undeclared-sibling refusal so the accept is not vacuous, + and an assertion that the `.describe()` still names its consumer. +- 52db1d1: Correct two stale author-facing contract statements in `Object.enable` / `Object.lifecycle` — text only, no change to what parses. + + - `lifecycle.ttl.onlyWhen` × `archive` (#10526): the refusal's rejection message no longer says "the Archiver moves rows by age alone". Since #10347 the Archiver selects candidates by the declared ttl cutoff, so that reason had gone stale; the reason it states now is the one that holds — the ttl **window** carries over to the Archiver, the `onlyWhen` **filter** does not, so the filtered-out rows would still be archived. The refusal itself is unchanged. + - `enable.files` / `enable.feeds` (#10336): the two `.describe()` strings said the flags reject *creation*. Since #10170 both capability gates are registered on `beforeUpdate` as well, so they refuse any write that makes a row **target** the walled object — a create and an update that re-points/re-threads an existing row alike (403 `FILES_DISABLED` / `FEEDS_DISABLED`). The strings now state that, matching the docblocks above them. `enable.activities` is unaffected and untouched. +- 5649efb: `LifecycleSchema` now refuses the `retention` + `ttl` + `archive` triple at + parse time unless the ttl restates the age bound exactly — `ttl.field: + 'created_at'` with `ttl.expireAfter` equal to `retention.maxAge` (#10527). + + Since #10347 the Archiver selects the rows it moves by the declared ttl cutoff + (`ttl.field` older than `ttl.expireAfter`) whenever `ttl` is declared, and by + `created_at`/`archive.after` only when it is not. On a diverging triple that + leaves `retention.maxAge` (pinned equal to `archive.after` by the existing + alignment refine) declared but enforced by nothing — a row whose `ttl.field` + sits in the future stays hot past `retention.maxAge`, silently. A declared + bound nothing enforces is the class this block already refuses loudly, so the + divergence is now rejected at authoring time with a named message instead of + being resolved by whichever column the sweep happens to read. + + No shipped or example object declares the triple (censused in #10527: + `sys_audit_log` and `sys_metadata_audit` are the only archive-declaring + objects, both `retention` + `archive` pairs) — so no bundled object changes + behaviour, and the ruled-legal shapes are unchanged: `retention` + `archive` + aligned pairs and `ttl` + `archive` pairs parse exactly as before. +- def0d3e: Runtime publish-gate findings for collection-resident write types (`object` / + `permission` / `book`) now key the top-level collection entry in + `issues[].path` / `advisories[].path` by NAME — + `objects.acme_invoice.sharingModel` — instead of by the gate's private + per-write snapshot index (`objects[417].sharingModel`), which no caller could + resolve: that index numbered an in-memory array a Studio / MCP / REST receiver + has never seen. Single-member write types keep their trivially-stable + positional form (`flows[0].nodes[1]…`), and nested positions inside one named + item (`objects.acme_invoice.indexes[1]`) stay positional — they index the + author's own document. An entry with no splice-safe name falls back to the + positional spelling. The accepted metadata set is unchanged; only the spelling + of the emitted finding `path` changes, and `RuntimeAuthoringIssueSchema.path`'s + description now states the convention. CLI (`os validate` / `os lint`) output + is unchanged — there the index resolves against the author's own config file. +- 8d0bb79: **Liveness-ledger verdict:** `app.navigation[].runAction` moves `planned` → `live`, and drops its `authorWarn` (#10068). + + The declared deep-link slot (`ObjectNavItemSchema.runAction`, #4848/#7253) now has a real consumer in a shipped shell, so authoring it changes runtime behaviour. **What changes for authors:** setting `runAction` no longer raises the liveness advisory that told you the auto-run does not fire from this declaration yet. Nothing about the schema, the accept set, or the authoring-time validation changed — `defineStack`'s cross-reference walk and lint's `validate-action-name-refs` nav arm still reject a name that resolves to no defined action, exactly as before. + + The row carries **two** evidence pointers, not one, and the split is the point: + + - **`producer`** — objectui `packages/layout/src/NavigationRenderer.tsx`: defines `NAV_RUN_ACTION_PARAM` (the wire name's one definition) and applies `withRunAction` inside `resolveHref`'s object branch, on the **list landings only** — never the `recordId` branch. It *writes* the deep link and runs nothing. + - **`evidence`** — objectui `packages/app-shell/src/hooks/useNavRunAction.ts`: the single read-once/consume-once consumer, wired generically at `ObjectView.tsx` (every object list) and behind the entitlement gate at `EnvironmentListToolbar.tsx`. + + A renderer-only pointer would have said the slot is live because something *emits* it; what makes the key live is that a shell *consumes* it, and that lives in `app-shell`, not `layout`. Both were read at the `.objectui-sha` pin `9a3daf8`, which postdates the consumer's merge (objectui#5216 via objectui PR #5354). + + ⚠️ **Recorded on the row: enforcement is not consumption.** The published `@objectstack/spec@17.0.0` does **not** enforce the `runAction` × `recordId` exclusivity — the `objectNavTargetExclusivity` refinement exists on `main` but is outside the GA build — and it accepts `runAction: ''`. That is the merged-but-unpublished window, not a defect. The consequence worth carrying: objectui's list-surface-only precedence and its empty-string-is-absent handling are **load-bearing rather than defensive**, because the pinned schema refuses neither input for it. Generalising: merged upstream ≠ published ≠ pinned downstream, and unlike a missing key, a missing **refinement fails silent** — the input is let through and the consumer proceeds. +- 2e3cf95: Name the real per-tier styling primitive in `PageSchema`'s `kind` and `source` + descriptions, replacing the "JSX/HTML+Tailwind" framing that ADR-0080's 2026-06-30 + amendment retracted on styling. + + A page's `source` is runtime metadata, so the console's build-time Tailwind never + scans it — authored utility `className`s silently produce no CSS. The descriptions + now say what each tier actually styles with: `kind:'html'` via the registered + components' structured props plus a JSON `style` object with `hsl(var(--token))` + theme colors, `kind:'react'` via inline `style` with the same token colors, and + neither with Tailwind classes. + + Text-only correction, no schema shape or acceptance change — the accepted page set + is unchanged, and every other claim in the two descriptions survives verbatim + (parse-never-execute, the compiler package per tier, `source` authoritative over + `regions`, the ADR-0081 `OS_PAGE_REACT=off` gating). + + - `packages/spec/src/ui/page.zod.ts` — the `kind` and `source` `.describe()` + strings and the `source` TSDoc block, which regenerate + `content/docs/references/ui/page.mdx`. +- 4c93387: Document the retry/durable-pause boundary on a flow's `errorHandling` block: a durable + pause (`approval`, `screen`, `wait` — ADR-0019) **ends the retry-governed segment**. + `errorHandling.strategy: 'retry'` describes one synchronous dispatch, so a run that pauses + and later resumes gets exactly one attempt for anything that fails after the pause. + + Prose only — no validation change. The accepted flow set is unchanged and every flow that + parsed before parses identically; what changes is that the boundary is now stated where an + author meets it (the `errorHandling` and `strategy` `describe()` text, which is what the + generated reference tables render) instead of having to be inferred from engine behaviour. + + The boundary is deliberate rather than a gap: the retry knobs (`backoffMs`, + `backoffMultiplier`, `jitter`) model an in-process loop, which a pause of arbitrary + duration is not, and the durable continuation carries no attempt counter. To protect the + half of a flow that runs after a pause, give that half its own failure handling in the + flow — a `try_catch` node with its own `retry` around the post-resume work, or a `fault` + edge to a handler node. `content/docs/automation/flows.mdx` carries the recipe. +- 1ec36b7: **Behaviour change (tightening, boot-time only):** a settings write issued before `SettingsService`'s data engine is bound is now **refused loudly** instead of resolving successfully while nothing reaches `sys_setting` (#10159). + + `upsertRow` picks its store on `if (this.engine)`, and the engine is bound in exactly one place — `SettingsServicePlugin` registers a `kernel:ready` hook from its `start()` and calls `bindEngine` inside it. `kernel:ready` handlers run in registration order and every plugin's `init()` runs before any plugin's `start()`, so **every `kernel:ready` hook registered from an `init()` fires inside that window**. A `set()` from there landed in the in-process memory fallback, re-resolved off that same array, and handed the caller a fully resolved value; `sys_setting` received nothing, and neither audit ledger recorded anything (both sinks bind on the same `bindEngine` call). Nothing was logged at any level, because the write did not fail — it succeeded against the wrong store. + + **What an operator will now observe.** A write in that window throws `SettingsEngineNotBoundError` — code `SETTINGS_ENGINE_NOT_BOUND`, status **503** — whose message names the window, the reason, and the fix: move the write to `kernel:bootstrapped` (or later), which fires strictly after every `kernel:ready` handler has settled. Previously that same call returned a resolved value and the setting was silently absent after restart. + + **Nothing outside the window changes.** The refusal is armed only by the new opt-in `SettingsServiceOptions.engineBindPending`, which `SettingsServicePlugin` sets in `init()` and clears on both branches of its `kernel:ready` hook — by `bindEngine` when `objectql` is present, or by the new `SettingsService.settleWithoutEngine()` when it is not. So: + + - a `SettingsService` constructed directly (unit tests, bootstrap, control-plane mock) keeps the in-memory fallback exactly as before — it declares no pending bind, and the guard never arms; + - a lean kernel with no `objectql` keeps the plugin's deliberate degradation: once its `kernel:ready` hook has established that no engine is coming, writes resolve into the memory fallback again (now with a `warn` saying those values are lost on restart); + - reads are untouched in every state, so an ordinary boot-time read of a setting still resolves. + + No shipped caller wrote settings inside the window, so no existing startup sequence becomes an error. + + `SETTINGS_ENGINE_NOT_BOUND` is registered in `ERROR_CODE_LEDGER` per ADR-0112. The status is declared on the error class rather than at an HTTP door because no door can reach it: the window closes at `kernel:ready`, and HTTP servers open their socket at `kernel:listening`, strictly after. +- 5f2e54c: **Docs:** the `skill.tools[]` docblock now states ADR-0109's authoring model instead of its rejected alternative (#10356). + + `SkillSchema.tools`' docblock told authors that "Tools should also be registered as first-class metadata (type: 'tool') unless they are dynamically materialised at runtime" — the shape ADR-0109 explicitly **rejected** ("a required tool record per exposed action": a second authoring step, a second namespace to keep consistent, and a second surface for AI authors to hallucinate into, for zero added capability). It also inverted the exemption, treating the materialised path as the exception when ADR-0109 makes it — together with the platform registry — the rule. The sibling docblock over `stack.zod.ts`'s `tools` already said the opposite, so the package shipped two contradictory answers to the same question. + + The text now mirrors the resolution universe `@objectstack/lint`'s `validate-ai-tool-references` actually implements: a `tool` record is never required and the default third-party path declares none; a `skill.tools[]` name resolves against the stack's own `stack.tools[]` names, `PLATFORM_PROVIDED_TOOL_NAMES`, and the `action_` family the runtime materialises from AI-exposed declarative actions (`ai.exposed` + `ai.description` on a headless action type, per ADR-0011). It also records that `stack.tools` is the optional Phase-2 AI-presentation refinement layer with no runtime reader until that phase lands — so a record authored today is inert, which the old sentence recommended authoring without saying. + + Prose only: no schema shape, no `.describe()` text, no runtime behaviour and no authorable-surface change (`check:authorable-surface` and the whole `check:generated` set are unmoved by this diff). It is graded rather than skipped because the text ships to consumers: `@objectstack/spec`'s `files` list publishes `src/**/*.zod.ts`, so this docblock travels in the npm tarball as source. It does **not** reach `dist/*.d.ts` — property-level comments inside the `z.object({ … })` literal are dropped from the emitted declarations, which is measurable in the built chunk (`tools: z.ZodArray;`, no comment). Published source is the surface that matters here anyway: this is the docblock an AI author reads while writing `skill.tools[]`, the exact surface ADR-0109 was written to keep clean. +- 73d9795: Time-relative sweeps are now idempotent per matched window (#10220). Previously the sweep + held no cross-tick memory, so every re-scan of the same window re-dispatched the same + records — a 5s-interval flow minted 15 duplicate reminders in ~70s, and even under a daily + cron a kernel rebuild re-dispatched the day's window. + + - `@objectstack/service-automation` — new platform object `sys_flow_dispatch`: a persisted + dispatch-claim ledger (ADR-0057 telemetry retention, 30 days), registered alongside + `sys_automation_run` and exposed as `AutomationEngine.claim(key): Promise` on + the automation service surface (check-and-record; a concurrent duplicate insert re-reads + and reports the key as already claimed). When no ObjectQL engine / registration is + available the engine degrades to in-process dedup and logs the weakened guarantee once; + when the ledger errors, the claim falls back to the in-process check for that key so a + store outage never blocks a dispatch (availability over strict-once). + - `@objectstack/trigger-schedule` — the time-relative sweep computes a dispatch key from + the MATCHED WINDOW's identity and claims it before launching: offset mode keys on + `(flowName, recordId, windowDay, offset)` — so a dateField edit that moves the window + legitimately re-fires — and range mode keys on `(flowName, recordId, sweepDay, + rangeSpec)`, preserving the documented `withinDays` semantic ("fires every day the + record stays in range") while never firing twice in one day. The trigger resolves the + claim surface structurally from the automation service; without one it dedups + in-process and warns once. + - `@objectstack/spec` — `sys_flow_dispatch` added to `PLATFORM_OBJECTS_BY_PACKAGE` under + `service-automation` (registry conformance). +- f399618: Retarget four `roles` → `positions` action-session provenance strings from "v11" to + "v16" — the release that actually shipped the `#3280` deprecate → `#3290` remove + session-alias precedent they cite (`content/docs/releases/v16.mdx` is the only release + page citing `#3290`). + + Text-only provenance correction, no schema shape or acceptance change: + + - `ActionSessionSchema`'s `positions` and `roles` `.describe()` strings + (`packages/spec/src/ui/action-params.zod.ts`) — regenerates + `content/docs/references/ui/action-params.mdx`. + - The `action-session-roles-to-positions` migration rationale + (`packages/spec/src/migrations/registry.ts` and + `packages/spec/src/migrations/entries/semantic/17.action-session-roles-to-positions.ts`) + — regenerates `spec-changes.json` and `docs/protocol-upgrade-guide.md`. + ## 17.1.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index 46d9cd00f1..a56fdc8a77 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.1.0", + "version": "17.2.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 79b87809e1..fb684d442d 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/trigger-api +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 036da662ee..51b7049bea 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.1.0", + "version": "17.2.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 e69a6d5ce6..f9e291f839 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/plugin-trigger-record-change +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 4739dccf47..322c07e48b 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.1.0", + "version": "17.2.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 e30a88533a..964048622e 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,66 @@ # @objectstack/plugin-trigger-schedule +## 17.2.0 + +### Minor Changes + +- 73d9795: Time-relative sweeps are now idempotent per matched window (#10220). Previously the sweep + held no cross-tick memory, so every re-scan of the same window re-dispatched the same + records — a 5s-interval flow minted 15 duplicate reminders in ~70s, and even under a daily + cron a kernel rebuild re-dispatched the day's window. + + - `@objectstack/service-automation` — new platform object `sys_flow_dispatch`: a persisted + dispatch-claim ledger (ADR-0057 telemetry retention, 30 days), registered alongside + `sys_automation_run` and exposed as `AutomationEngine.claim(key): Promise` on + the automation service surface (check-and-record; a concurrent duplicate insert re-reads + and reports the key as already claimed). When no ObjectQL engine / registration is + available the engine degrades to in-process dedup and logs the weakened guarantee once; + when the ledger errors, the claim falls back to the in-process check for that key so a + store outage never blocks a dispatch (availability over strict-once). + - `@objectstack/trigger-schedule` — the time-relative sweep computes a dispatch key from + the MATCHED WINDOW's identity and claims it before launching: offset mode keys on + `(flowName, recordId, windowDay, offset)` — so a dateField edit that moves the window + legitimately re-fires — and range mode keys on `(flowName, recordId, sweepDay, + rangeSpec)`, preserving the documented `withinDays` semantic ("fires every day the + record stays in range") while never firing twice in one day. The trigger resolves the + claim surface structurally from the automation service; without one it dedups + in-process and warns once. + - `@objectstack/spec` — `sys_flow_dispatch` added to `PLATFORM_OBJECTS_BY_PACKAGE` under + `service-automation` (registry conformance). + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + - @objectstack/core@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index d2058cd3bd..3909564c30 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.1.0", + "version": "17.2.0", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack — 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 4dd2e1436d..074d241cbd 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,37 @@ # @objectstack/types +## 17.2.0 + +### Patch Changes + +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [2306a76] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [923c424] +- Updated dependencies [1ec36b7] +- Updated dependencies [5f2e54c] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/spec@17.2.0 + ## 17.1.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index b3164bb367..f5d6168cf5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.1.0", + "version": "17.2.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 f34c319600..cf0e6da5a3 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,82 @@ # @objectstack/verify +## 17.2.0 + +### Patch Changes + +- Updated dependencies [8f04d9a] +- Updated dependencies [6936d07] +- Updated dependencies [59eb04d] +- Updated dependencies [9f05b7d] +- Updated dependencies [530c1df] +- Updated dependencies [128684d] +- Updated dependencies [7d2d112] +- Updated dependencies [5fa0d72] +- Updated dependencies [7bf3fb7] +- Updated dependencies [2570ab0] +- Updated dependencies [5886ee6] +- Updated dependencies [b20c8d2] +- Updated dependencies [6ce58a7] +- Updated dependencies [d23e3a0] +- Updated dependencies [9a1ed7a] +- Updated dependencies [f3a8134] +- Updated dependencies [b03a880] +- Updated dependencies [914c413] +- Updated dependencies [55809a0] +- Updated dependencies [5b0af2b] +- Updated dependencies [47cd3ec] +- Updated dependencies [52db1d1] +- Updated dependencies [5649efb] +- Updated dependencies [9d7d2de] +- Updated dependencies [2306a76] +- Updated dependencies [26f3588] +- Updated dependencies [9e04c3e] +- Updated dependencies [67630c4] +- Updated dependencies [a40dcc1] +- Updated dependencies [def0d3e] +- Updated dependencies [8d0bb79] +- Updated dependencies [112a8c6] +- Updated dependencies [a16ff50] +- Updated dependencies [e222a53] +- Updated dependencies [2e3cf95] +- Updated dependencies [4c93387] +- Updated dependencies [d728325] +- Updated dependencies [3ee8ddf] +- Updated dependencies [16cef97] +- Updated dependencies [4389fe9] +- Updated dependencies [9f483d9] +- Updated dependencies [923c424] +- Updated dependencies [b419135] +- Updated dependencies [0ab81d1] +- Updated dependencies [1ec36b7] +- Updated dependencies [bc400af] +- Updated dependencies [5f2e54c] +- Updated dependencies [86a8ec9] +- Updated dependencies [35ad101] +- Updated dependencies [ceb33a9] +- Updated dependencies [dccbcec] +- Updated dependencies [6439f8b] +- Updated dependencies [73d9795] +- Updated dependencies [8012960] +- Updated dependencies [24ba050] +- Updated dependencies [f399618] +- Updated dependencies [75e9301] + - @objectstack/platform-objects@17.2.0 + - @objectstack/plugin-auth@17.2.0 + - @objectstack/spec@17.2.0 + - @objectstack/objectql@17.2.0 + - @objectstack/runtime@17.2.0 + - @objectstack/service-analytics@17.2.0 + - @objectstack/service-automation@17.2.0 + - @objectstack/plugin-security@17.2.0 + - @objectstack/rest@17.2.0 + - @objectstack/plugin-hono-server@17.2.0 + - @objectstack/core@17.2.0 + - @objectstack/plugin-sharing@17.2.0 + - @objectstack/service-settings@17.2.0 + - @objectstack/service-datasource@17.2.0 + - @objectstack/types@17.2.0 + ## 17.1.0 ### Patch Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index ebec2774c6..92adc06cde 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.1.0", + "version": "17.2.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",