diff --git a/.changeset/20102-retire-saved-report-stack.md b/.changeset/20102-retire-saved-report-stack.md
new file mode 100644
index 00000000000..f81b7843e12
--- /dev/null
+++ b/.changeset/20102-retire-saved-report-stack.md
@@ -0,0 +1,82 @@
+---
+'@objectstack/spec': minor
+'@objectstack/platform-objects': minor
+'@objectstack/rest': minor
+'@objectstack/client': minor
+'@objectstack/cli': minor
+'@objectstack/metadata-protocol': patch
+---
+
+feat!: retire the saved-report stack — `sys_saved_report` / `sys_report_schedule`, `/api/v1/reports`, `client.reports`, `IReportService`, the `reports` capability and `@objectstack/plugin-reports` (#20102)
+
+**BREAKING** — the saved-report stack is removed whole, with no deprecation window
+(maintainer ruling 2026-09-25, 「A. 退役」). It persisted a raw object query
+(`object_name` + `{ filter, fields, orderBy, limit, groupBy }`) with a render format
+and an owner, and could e-mail it on a schedule. Measured on the main branch of this
+repository, objectui and cloud before removal: zero callers of the routes, the SDK
+namespace or the service contract outside their own tests, and no app declaring the
+capability.
+
+**NOT affected: the `report` metadata kind.** `ReportSchema`, `defineReport`,
+`/meta/report`, datasets and the analytics service are unchanged. The two shared the
+word "report" and nothing else.
+
+FROM → TO, per surface:
+
+- `requires: ['reports']` → **refused** by `defineStack` (`STACK_CAPABILITY_UNKNOWN`,
+ 422) with the prescription "requires: 'reports' was removed in @objectstack/spec
+ 17.5.0 … Delete the token." Fix: delete the token. `os serve` on an older artifact
+ that still carries it warns with the same prescription and ignores it; `os validate`
+ and `os build` over a plain-object config (no `defineStack` call, so no parse-time
+ vocabulary check) report it as a non-fatal capability advisory carrying the same
+ prescription, never "check for a typo". The token is
+ gone from `PLATFORM_CAPABILITY_TOKENS` and `PLATFORM_CAPABILITY_PROVIDERS`; the new
+ `RETIRED_PLATFORM_CAPABILITY_GUIDANCE` (`@objectstack/spec/kernel`) carries the
+ prescription.
+- `IReportService`, `SavedReport`, `ReportSchedule`, `ReportQuery`, `ReportFormat`,
+ `ReportRunResult`, `SaveReportInput`, `ScheduleReportInput`
+ (`@objectstack/spec/contracts`) → removed, no replacement export. Fix: delete the
+ import.
+- `SysSavedReport`, `SysReportSchedule` (`@objectstack/platform-objects/audit`) and
+ the names `sys_saved_report` / `sys_report_schedule` in
+ `PLATFORM_PROVIDED_OBJECT_NAMES` → removed. A stack referencing either name is now
+ flagged as a probable typo instead of resolving.
+- `GET|POST /api/v1/reports`, `GET|DELETE /api/v1/reports/:id`,
+ `POST /api/v1/reports/:id/run`, `POST /api/v1/reports/:id/schedule`,
+ `GET /api/v1/reports/:id/schedules`, `DELETE /api/v1/reports/schedules/:scheduleId`
+ → unmounted: each answers the standard unmatched-route `404`, byte-identical to a
+ path that never existed. Their nine error codes (`REPORTS_LIST_FAILED`,
+ `REPORT_DELETE_FAILED`, `REPORT_GET_FAILED`, `REPORT_NOT_FOUND`,
+ `REPORT_RUN_FAILED`, `REPORT_SAVE_FAILED`, `REPORT_SCHEDULE_FAILED`,
+ `SCHEDULES_LIST_FAILED`, `SCHEDULE_DELETE_FAILED`) leave `ERROR_CODE_LEDGER` with
+ their only emitter.
+- `client.reports.*` (`list`, `save`, `get`, `delete`, `run`, `schedule`,
+ `listSchedules`, `unschedule`) → removed. Fix: delete the call. A report is `report`
+ metadata, read through `meta.*` and queried through `analytics.*`; a saved ad-hoc
+ object query is a ListView on that object.
+- `RestServer`'s constructor keeps the position of the retired saved-report provider,
+ typed `undefined`, so no later positional argument re-binds. Pass `undefined` there;
+ passing a provider is a compile error.
+- `@objectstack/plugin-reports` → no longer built or published from this repository,
+ and `@objectstack/cli` no longer depends on it or mounts it. Fix: remove the
+ dependency. There is no successor package and no scheduled-delivery replacement.
+
+**Existing databases.** `sys_saved_report` / `sys_report_schedule` tables in a deployed
+database are left in place, untouched — no backfill, no reaper, no drop — under the
+repository's convention for a retired platform object: the platform never drops a
+table that metadata stops declaring, and `os migrate plan` lists such a table in its
+informational unmanaged-tables section so an operator can decide.
+
+`@objectstack/metadata-protocol` (patch): the `INVALID_SORT` hint for a sort node
+spelled `{ field, direction }` no longer names the retired saved-report contract as
+the source of that vocabulary; it names the better-auth adapter's `sortBy`, which
+still uses it. Code and status are unchanged.
+
+Breaking ships as `minor` per the launch-window convention
+(`scripts/check-changeset-no-major.mjs`).
+
+**Clause-②: yes (narrowing)** — a published capability token, a service contract and
+its types, two platform objects, eight routes, nine registered error codes and an SDK
+namespace are removed; nothing previously refused is now accepted.
+
+
diff --git a/.changeset/config.json b/.changeset/config.json
index bd5e166f97c..14d68f959cb 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -44,7 +44,6 @@
"@objectstack/organizations",
"@objectstack/mcp",
"@objectstack/plugin-pinyin-search",
- "@objectstack/plugin-reports",
"@objectstack/plugin-security",
"@objectstack/plugin-sharing",
"@objectstack/service-sms",
diff --git a/README.md b/README.md
index f5f9abd2c40..a8065a6c768 100644
--- a/README.md
+++ b/README.md
@@ -313,7 +313,6 @@ control plane.
| [`@objectstack/plugin-audit`](packages/plugins/plugin-audit) | Audit log object and audit trail |
| [`@objectstack/plugin-email`](packages/plugins/plugin-email) | Pluggable outbound email transport |
| [`@objectstack/plugin-webhooks`](packages/plugins/plugin-webhooks) | Durable, cluster-aware outbound webhook delivery |
-| [`@objectstack/plugin-reports`](packages/plugins/plugin-reports) | Saved reports and scheduled email digests |
| [`@objectstack/plugin-pinyin-search`](packages/plugins/plugin-pinyin-search) | Pinyin recall for CJK search |
| [`@objectstack/plugin-dev`](packages/plugins/plugin-dev) | Zero-config local development assembly |
| [`@objectstack/knowledge-memory`](packages/plugins/knowledge-memory) | In-memory knowledge adapter (dev / test) |
diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx
index 7dc3cbd903d..e900c4f6c57 100644
--- a/content/docs/api/client-sdk.mdx
+++ b/content/docs/api/client-sdk.mdx
@@ -168,7 +168,6 @@ The `@objectstack/client` SDK aims to implement the ObjectStack API protocol spe
| **automation** | ✅ | 18 | Flow CRUD, trigger/execute, runs, screen-flow resume, descriptor/status registries |
| **actions** | ✅ | 2 | Server-registered action handlers (`engine.registerAction`) |
| **approvals** | ✅ | 12 | Approval requests (ADR-0019): inbox, decisions, recall, revise/resubmit (ADR-0044), thread interactions, audit trail |
-| **reports** | ✅ | 8 | Saved reports: definitions, execution, recurring email schedules (501 without `@objectstack/plugin-reports`) |
| **shares** | ✅ | 8 | Per-record sharing grants (list/grant/revoke) + tenant-wide sharing rules (`shares.rules.*`, M10.17) |
| **search** | ✅ | 1 | Global cross-object search (M10.5) |
| **email** | ✅ | 1 | Transactional send via IEmailService (M11.B1) |
diff --git a/content/docs/capabilities/analytics.mdx b/content/docs/capabilities/analytics.mdx
index 387df357439..6bc681a987f 100644
--- a/content/docs/capabilities/analytics.mdx
+++ b/content/docs/capabilities/analytics.mdx
@@ -1,6 +1,6 @@
---
title: Analytics & Dashboards
-description: ~20 chart families, period-over-period comparison, runtime filters, TV display pages, four report shapes, and scheduled email digests
+description: ~20 chart families, period-over-period comparison, runtime filters, TV display pages, and four report shapes
---
@@ -25,7 +25,7 @@ Dashboard extras that matter in practice:
## Reports
-Four shapes — **tabular** (detail rows), **summary** (grouped totals), **matrix** (regions × quarters), and **joined** (several dataset panels) — with runtime filters and export. **Scheduled email digests** deliver a report daily or weekly: the Monday-morning sales report writes itself.
+Four shapes — **tabular** (detail rows), **summary** (grouped totals), **matrix** (regions × quarters), and **joined** (several dataset panels) — with runtime filters and export.
> **In HotCRM**: four dashboards (executive, sales, service, CRM overview) and six reports (lead, opportunity, account, case, churn), fed by six shared datasets covering accounts, contacts, leads, opportunities, products, and cases. The executive dashboard carries quarter and owner filters plus the funnel and trend widgets described above.
diff --git a/content/docs/capabilities/index.mdx b/content/docs/capabilities/index.mdx
index b0548a57383..63ba1e4b5bb 100644
--- a/content/docs/capabilities/index.mdx
+++ b/content/docs/capabilities/index.mdx
@@ -17,7 +17,7 @@ If you are a developer, the [Build sections](/docs/data-modeling) cover the same
-
+
diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx
index f09bcb7c435..7eba4abf5cd 100644
--- a/content/docs/permissions/system-context.mdx
+++ b/content/docs/permissions/system-context.mdx
@@ -9,8 +9,8 @@ the seed loader replaying package fixtures, a plugin's boot reconciler, a
service self-write, a migration.
This page is **the authority** for what that flag actually does. It exists
-because the flag is not one concept: it is a single boolean read at **112
-distinct sites across 20 packages**, and knowing three of those behaviours gives
+because the flag is not one concept: it is a single boolean read at **105
+distinct sites across 19 packages**, and knowing three of those behaviours gives
no hint that the other hundred-and-four exist. Every documented app-side bug
traced to `isSystem` had the same shape — the metadata was complete and correct,
and the gap was observable only by querying the resulting rows.
@@ -135,7 +135,7 @@ that silently does not happen.
### 3. Sharing (`plugin-sharing`)
-The largest single consumer — **17 of the 112 sites**.
+The largest single consumer — **17 of the 105 sites**.
| # | Behaviour when `isSystem` | What you get / what you lose | Anchor |
|:--|:---|:---|:---|
@@ -150,51 +150,48 @@ The largest single consumer — **17 of the 112 sites**.
| 39 | Sharing-rule provenance stamp skipped | Lose: the row is not marked as an admin customization — seeder / `defineRule` / boot reconcilers are "the package door" | `packages/plugins/plugin-sharing/src/sharing-rule-provenance.ts#bindRuleProvenanceStamp` |
| 40 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, and the platform-global-rule delete guard | `packages/plugins/plugin-sharing/src/sharing-rule-service.ts#assertCanManageRules`, `#assertCanDeletePlatformGlobalRule` |
-### 4. Approvals, reports, attachments, comments, knowledge
+### 4. Approvals, attachments, comments, knowledge
| # | Behaviour when `isSystem` | Package | What you get / what you lose | Anchor |
|:--|:---|:---|:---|:---|
| 41 | **Approval record lock released** — a locked record is writable | plugin-approvals | Get: engine self-writes (the status mirror) pass. Lose: the lock that stops edits while an approval is live. Note there is deliberately **no admin exemption** here — only `isSystem` | `packages/plugins/plugin-approvals/src/lifecycle-hooks.ts#bindApprovalLockHook` |
| 42 | Delegation write guard bypassed | plugin-approvals | Get: service / seed / import may write delegation rows naming another delegator | `packages/plugins/plugin-approvals/src/lifecycle-hooks.ts#bindDelegationWriteGuard` |
| 43 | Approval actor / submitter / pending-approver checks bypassed (8 sites) | plugin-approvals | Get: approve, reject, recall, reassign without being a pending approver or the submitter | `packages/plugins/plugin-approvals/src/approval-service.ts#isOverrideActor`, `#resolveActor`, `#sendBack`, `#resubmit`, `#reassign`, `#remind`, `#requestInfo`, `#comment` |
-| 44 | Saved-report ownership is **assignable**, and an update may reassign it | plugin-reports | Get: `ownerId` from input is honoured. A non-system caller always owns what it creates and can never reassign | `packages/plugins/plugin-reports/src/report-service.ts#saveReport` |
-| 45 | Saved-report access / export / mutation gates bypassed | plugin-reports | Get: read, bulk-export and overwrite any report | `packages/plugins/plugin-reports/src/report-service.ts#assertExportAllowed`, `#canAccessReport`, `#listReports`, `#listSchedules` |
-| 46 | Attachment access hooks return early (insert + update + delete, and the read AST) | service-storage | Lose: attachment visibility scoping | `packages/services/service-storage/src/attachment-access-hooks.ts#installAttachmentAccessHooks`, `#installAttachmentReadVisibility` |
-| 47 | Comment access hooks return early (insert + update + delete, and the read AST) | plugin-audit | Lose: comment visibility scoping | `packages/plugins/plugin-audit/src/comment-access-hooks.ts#installCommentAccessHooks`, `#installCommentReadVisibility` |
-| 48 | Knowledge search returns hits unfiltered | service-knowledge | Lose: the permission filter over search results | `packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter` |
+| 44 | Attachment access hooks return early (insert + update + delete, and the read AST) | service-storage | Lose: attachment visibility scoping | `packages/services/service-storage/src/attachment-access-hooks.ts#installAttachmentAccessHooks`, `#installAttachmentReadVisibility` |
+| 45 | Comment access hooks return early (insert + update + delete, and the read AST) | plugin-audit | Lose: comment visibility scoping | `packages/plugins/plugin-audit/src/comment-access-hooks.ts#installCommentAccessHooks`, `#installCommentReadVisibility` |
+| 46 | Knowledge search returns hits unfiltered | service-knowledge | Lose: the permission filter over search results | `packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter` |
### 5. Actions, metadata plane, provenance, the organization wall
| # | Behaviour when `isSystem` | Package | What you get / what you lose | Anchor |
|:--|:---|:---|:---|:---|
-| 49 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `packages/runtime/src/action-execution.ts#callData` |
-| 50 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `packages/runtime/src/action-execution.ts#actionPermissionError` |
-| 51 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` |
-| 52 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 51's doors consult answers yes before any capability is examined | `packages/metadata-core/src/meta-write-capability.ts#metaWriteCapabilityVerdict` |
-| 53 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `packages/runtime/src/domains/actions.ts#handleActionsRequest`, `packages/runtime/src/domains/ai.ts#handleAIRequest`, `packages/runtime/src/domains/automation.ts#handleAutomationRequest`, `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/runtime/src/domains/security.ts#handleSecurityRequest`, `packages/runtime/src/domains/packages.ts#handlePackagesRequest`, `packages/rest/src/external-datasource-routes.ts#registerExternalDatasourceRoutes`, `packages/rest/src/package-routes.ts#refusePackageRequest` |
-| 54 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `packages/runtime/src/domains/mcp.ts#handleMcpRequest` |
-| 55 | Package REST route capability gate bypassed | rest | Get: a marketplace publish over REST (`POST /packages/publish`, the one route the REST registrar mounts since #14503) without `manage_metadata`; the package read cohort (`studio.access` / `setup.access`) is enforced by the dispatcher `/packages` domain's own read gate, where the reads are served | `packages/rest/src/package-routes.ts#refusePackageRequest` |
-| 56 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability` |
-| 57 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring` |
-| 58 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume) and the two operator run-lifecycle writes all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite` |
-| 59 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertTenantAdmin` |
-| 60 | Email-template / webhook provenance stamps skipped | plugin-email, plugin-webhooks | Lose: the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-provenance.ts#bindEmailTemplateProvenanceStamp`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
-| 61 | **Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | `packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes` |
-| 62 | Inbox caller refusal names `isSystem` as what was carried | service-messaging | Get: nothing — the refusal still fires. The flag only shapes the diagnostic, because privilege is not an authorization subject | `packages/services/service-messaging/src/inbox-caller.ts#resolveInboxRecipient` |
-| 63 | **`organization_id` is not auto-stamped on INSERT** — the organization-axis twin of the `owner_id` gap above | organizations | Get: an elevated write may name another organization deliberately, which is what the per-organization seed replay, the orphan-row claim, imports and migrations all rely on. Lose: the authoritative stamp, so an elevated insert that names no organization lands `organization_id = NULL` and the wall hides it. ⛔ This is why a forged `organization_id` is overwritten on the non-elevated path and not here: elevation is the seam the legitimate cross-organization writers use | `packages/plugins/organizations/src/organizations-plugin.ts#start` |
+| 47 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `packages/runtime/src/action-execution.ts#callData` |
+| 48 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `packages/runtime/src/action-execution.ts#actionPermissionError` |
+| 49 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` |
+| 50 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 49's doors consult answers yes before any capability is examined | `packages/metadata-core/src/meta-write-capability.ts#metaWriteCapabilityVerdict` |
+| 51 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `packages/runtime/src/domains/actions.ts#handleActionsRequest`, `packages/runtime/src/domains/ai.ts#handleAIRequest`, `packages/runtime/src/domains/automation.ts#handleAutomationRequest`, `packages/runtime/src/domains/meta.ts#handleMetadataRequest`, `packages/runtime/src/domains/security.ts#handleSecurityRequest`, `packages/runtime/src/domains/packages.ts#handlePackagesRequest`, `packages/rest/src/external-datasource-routes.ts#registerExternalDatasourceRoutes`, `packages/rest/src/package-routes.ts#refusePackageRequest` |
+| 52 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `packages/runtime/src/domains/mcp.ts#handleMcpRequest` |
+| 53 | Package REST route capability gate bypassed | rest | Get: a marketplace publish over REST (`POST /packages/publish`, the one route the REST registrar mounts since #14503) without `manage_metadata`; the package read cohort (`studio.access` / `setup.access`) is enforced by the dispatcher `/packages` domain's own read gate, where the reads are served | `packages/rest/src/package-routes.ts#refusePackageRequest` |
+| 54 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability` |
+| 55 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring` |
+| 56 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume) and the two operator run-lifecycle writes all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite` |
+| 57 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertTenantAdmin` |
+| 58 | Email-template / webhook provenance stamps skipped | plugin-email, plugin-webhooks | Lose: the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-provenance.ts#bindEmailTemplateProvenanceStamp`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
+| 59 | **Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | `packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes` |
+| 60 | Inbox caller refusal names `isSystem` as what was carried | service-messaging | Get: nothing — the refusal still fires. The flag only shapes the diagnostic, because privilege is not an authorization subject | `packages/services/service-messaging/src/inbox-caller.ts#resolveInboxRecipient` |
+| 61 | **`organization_id` is not auto-stamped on INSERT** — the organization-axis twin of the `owner_id` gap above | organizations | Get: an elevated write may name another organization deliberately, which is what the per-organization seed replay, the orphan-row claim, imports and migrations all rely on. Lose: the authoritative stamp, so an elevated insert that names no organization lands `organization_id = NULL` and the wall hides it. ⛔ This is why a forged `organization_id` is overwritten on the non-elevated path and not here: elevation is the seam the legitimate cross-organization writers use | `packages/plugins/organizations/src/organizations-plugin.ts#start` |
### 6. Reads that only carry the flag onward
-Not behaviours: these four sites read the flag solely to copy it into another
+Not behaviours: these three sites read the flag solely to copy it into another
context or envelope. They are listed because the census counts them, and because
a reader tracing where elevation travels needs them.
| # | Site | Package | What it does |
|:--|:---|:---|:---|
-| 64 | `packages/objectql/src/engine.ts#buildSession` | objectql | Propagates `isSystem` into the hook session so hooks can tell engine self-writes from user writes |
-| 65 | `packages/objectql/src/engine.ts#isSystem` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag |
-| 66 | `packages/plugins/plugin-reports/src/report-service.ts#executeReport` | plugin-reports | Threads the flag into the engine call that runs a report |
-| 67 | `packages/runtime/src/sandbox/body-runner.ts#executionContextFromHook` | runtime | Rebuilds an `ExecutionContext` from a hook session, carrying the flag across |
+| 62 | `packages/objectql/src/engine.ts#buildSession` | objectql | Propagates `isSystem` into the hook session so hooks can tell engine self-writes from user writes |
+| 63 | `packages/objectql/src/engine.ts#isSystem` | objectql | `ScopedContext.isSystem` getter — re-exposes the underlying execution context's flag |
+| 64 | `packages/runtime/src/sandbox/body-runner.ts#executionContextFromHook` | runtime | Rebuilds an `ExecutionContext` from a hook session, carrying the flag across |
---
@@ -281,8 +278,8 @@ Ownership injection, `readonly` bypass and sharing materialisation are
independent decisions, and a seed loader plausibly wants the first two but not
the third. The concept is nevertheless **staying as one boolean**:
-- **Shipped semantics.** `isSystem` is a published contract with 112 read sites
- in 20 packages. Splitting it is a breaking contract change across all of them.
+- **Shipped semantics.** `isSystem` is a published contract with 105 read sites
+ in 19 packages. Splitting it is a breaking contract change across all of them.
(The ruling was taken when the census read 80 sites in 18 packages; the count
has grown, which strengthens rather than weakens the argument.)
- **No business pull.** No app has asked for the combinations a split would
@@ -335,7 +332,7 @@ one ends: it returns prose inside comments and strings, the three unrelated
metadata fields, and the `isSystemObject` / `isSystemObjectName` /
`isSystemLedgerObject` name helpers. Worse, it loses real sites — a regex pass
over this same corpus silently dropped **6** reads in
-`packages/plugins/plugin-reports/src/report-service.ts` to a quoting desync and **11** more to
+the saved-report service (retired since, #20102) to a quoting desync and **11** more to
`(ctx?.session as any)?.isSystem` casts. So the census walks the TypeScript AST
and classifies each appearance of the identifier by where the parser puts it:
a **read** (a table row here), a **declaration**, an object-literal or type
@@ -353,19 +350,19 @@ still holds equal to the census on every pull request:
| — in tests | 1013 | — |
| — in non-test sources | 798 | — |
| Appearances of the bare identifier `isSystem` in non-test sources | 813 | — |
-| — parsed as a declaration | 23 | ✅ |
+| — parsed as a declaration | 22 | ✅ |
| — parsed as an object-literal / type key (producers and option objects) | 310 | — |
-| — parsed as a property **read** | 118 | ✅ |
+| — parsed as a property **read** | 111 | ✅ |
| — parsed in some other syntactic position (a local, a cast, a conditional) | 9 | ✅ |
| — the remainder: text inside comments and string literals | 358 | — |
| Of those reads: reads of one of the unrelated metadata fields | 6 | ✅ |
-| Of those reads: reads of `ExecutionContext.isSystem` | **112** | ✅ |
-| — behaviour-bearing (rows 1–63 above) | 108 | ✅ |
-| — carry the flag onward only (rows 64–67 above) | 4 | ✅ |
-| Packages containing at least one elevation read | **20** | ✅ |
-| Files containing at least one elevation read | 45 | ✅ |
-| — the distinct symbols those reads live in — what this page anchors | 93 | ✅ |
-| — of those files, the ones holding more than one read in one symbol | 9 | ✅ |
+| Of those reads: reads of `ExecutionContext.isSystem` | **105** | ✅ |
+| — behaviour-bearing (rows 1–61 above) | 102 | ✅ |
+| — carry the flag onward only (rows 62–64 above) | 3 | ✅ |
+| Packages containing at least one elevation read | **19** | ✅ |
+| Files containing at least one elevation read | 44 | ✅ |
+| — the distinct symbols those reads live in — what this page anchors | 87 | ✅ |
+| — of those files, the ones holding more than one read in one symbol | 8 | ✅ |
The six rows marked — are a **dated decomposition, not a live claim**: they were
measured on 2026-08-29 at `ca1965f2b5` and CI does not re-derive them. They count
@@ -428,7 +425,7 @@ same resolver, and the same registration shape, that holds `docs/adr/**`.
Renaming a symbol is now a loud red instead of a silent misdirection.
⚠️ **The precision that costs, priced here rather than buried.** A symbol anchor
-cannot say WHICH read inside a function it means, and **9** of the **45**
+cannot say WHICH read inside a function it means, and **8** of the **44**
anchored files hold more than one read inside a single symbol. So the population
check runs per file at symbol granularity: every file the census finds a read in
must be anchored, and the set of symbols this page cites into that file must
diff --git a/content/docs/permissions/tenant-audit-census.mdx b/content/docs/permissions/tenant-audit-census.mdx
index 3727f95eb56..7c7775542bf 100644
--- a/content/docs/permissions/tenant-audit-census.mdx
+++ b/content/docs/permissions/tenant-audit-census.mdx
@@ -121,7 +121,7 @@ are reported as `undecidable` rather than assumed either way.
The same holds twice over for the context. An options argument spelled as a
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
-forwarding shim cannot, and **67 of the 227 sites are spelled that way**. A
+forwarding shim cannot, and **67 of the 218 sites are spelled that way**. A
context resolved from an inline literal or a local `const` can be tested for
`isSystem`; one arriving from a helper call cannot.
@@ -186,10 +186,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
| carried figure | where it survives | this census |
| :--- | :--- | ---: |
-| 175 write call sites | quoted in the merged changeset | **227** |
+| 175 write call sites | quoted in the merged changeset | **218** |
| 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **34** more whose options argument is unreadable |
-| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **153 of 227** decidable, **74** undecidable |
-| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 108 decidably elevated, 0 decidably not, 102 undecidable |
+| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **144 of 218** decidable, **74** undecidable |
+| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 99 decidably elevated, 0 decidably not, 102 undecidable |
| 141 and 132, two independent re-derivations | the card that filed this work | — |
**The differences are not reconciled, and deliberately so.** The old census's
@@ -206,11 +206,11 @@ would report a smaller number and would not say so.
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
figure has no surviving corroboration anywhere in the tree.** This census reads
-108 of 227 (48%) as decidably elevated, with 102 more whose elevation is a
+99 of 218 (45%) as decidably elevated, with 102 more whose elevation is a
run-time fact — so the claim is neither confirmed nor refuted, and the honest
answer is that a static reading cannot settle it.
-⇒ **Cite `9 / 227`, and say what it is**: the sites whose options argument was
+⇒ **Cite `9 / 218`, and say what it is**: the sites whose options argument was
READ and holds no tenant context, against a decidably tenancy-enabled object.
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
without tenant context" — **34 further sites** have an options argument this
@@ -222,28 +222,28 @@ cannot read, and they are neither in nor out.
| what | count |
| :--- | ---: |
-| write call sites on the application surface | **227** |
-| …whose object name is statically decidable | 153 |
+| write call sites on the application surface | **218** |
+| …whose object name is statically decidable | 144 |
| …whose object name is chosen at run time | 74 |
-| …against an object with tenancy ENABLED | 152 |
+| …against an object with tenancy ENABLED | 143 |
| …against an object that declares tenancy off | 1 |
-| threading a tenant context | 143 |
+| threading a tenant context | 134 |
| PROVABLY carrying none (options read, no context key) | **17** |
| …of those, against a decidably tenancy-enabled object | **9** |
| options argument UNREADABLE — may or may not carry one | 67 |
| …of those, against a decidably tenancy-enabled object | 34 |
-| threading a decidably ELEVATED (`isSystem`) context | 108 |
+| threading a decidably ELEVATED (`isSystem`) context | 99 |
| threading a context that is decidably NOT elevated | 0 |
| threading a context whose elevation is a run-time fact | 102 |
| how the instrument reached the site | count |
| :--- | ---: |
-| receiver carried a readable engine type | 183 |
+| receiver carried a readable engine type | 174 |
| receiver erased, placed by the object NAME | 24 |
| receiver erased, placed by an `object: string` PARAMETER | 15 |
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
-| object name spelled inline | 110 |
+| object name spelled inline | 101 |
| object name spelled through a `const` | 43 |
| object name is an `object: string` parameter | 17 |
| object name is some other run-time expression | 57 |
@@ -296,13 +296,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
their values are not compared. The reasoning, and the measurement behind it,
are in `scripts/check-tenant-audit-census.mjs`.
-Measured on 2026-09-23 at `8f8828bf6`.
+Measured on 2026-09-25 at `62171c25e`.
| corpus scale (not enforced) | count |
| :--- | ---: |
-| tracked non-test sources scanned | 576 |
-| engine-shaped types recognised | 63 |
-| declared objects in the registry | 117 |
+| tracked non-test sources scanned | 572 |
+| engine-shaped types recognised | 61 |
+| declared objects in the registry | 115 |
| same-named calls subtracted as non-engine | 144 |
{/* END GENERATED: tenant-audit-census */}
diff --git a/content/docs/plugins/packages.mdx b/content/docs/plugins/packages.mdx
index ff424246602..29220f182f0 100644
--- a/content/docs/plugins/packages.mdx
+++ b/content/docs/plugins/packages.mdx
@@ -13,7 +13,7 @@ ObjectStack is organized into **72 package manifests** across multiple categorie
| **Client / DX** | 4 | `client`, `client-react`, `cli`, `create-objectstack` |
| **Framework adapters** | 1 | `hono` (other frameworks: build a thin adapter on `HttpDispatcher` — see below) |
| **Drivers** | 5 | `driver-memory`, `driver-sql`, `driver-sqlite-wasm`, `driver-mongodb`, `driver-turso` |
-| **Plugins** | 18 | `plugin-auth`, `plugin-security`, `plugin-audit`, `plugin-approvals`, `plugin-sharing`, `plugin-email`, `plugin-webhooks`, `plugin-reports`, `plugin-hono-server`, `plugin-dev`, `plugin-pinyin-search`, `mcp`, trigger plugins (`trigger-api`, `trigger-record-change`, `trigger-schedule`), and knowledge/embedder plugins (`knowledge-memory`, `knowledge-ragflow`, `embedder-openai`) |
+| **Plugins** | 17 | `plugin-auth`, `plugin-security`, `plugin-audit`, `plugin-approvals`, `plugin-sharing`, `plugin-email`, `plugin-webhooks`, `plugin-hono-server`, `plugin-dev`, `plugin-pinyin-search`, `mcp`, trigger plugins (`trigger-api`, `trigger-record-change`, `trigger-schedule`), and knowledge/embedder plugins (`knowledge-memory`, `knowledge-ragflow`, `embedder-openai`) |
| **Platform services** | 16 | `service-analytics`, `service-automation`, `service-cache`, `service-cluster`, `service-cluster-redis`, `service-datasource`, `service-i18n`, `service-job`, `service-knowledge`, `service-messaging`, `service-package`, `service-queue`, `service-realtime`, `service-settings`, `service-sms`, `service-storage` |
@@ -440,13 +440,6 @@ metadata it names.
- **Features**: Subscriptions, retries, signed payloads, delivery logs
- **When to use**: Integrating ObjectStack events with external systems
-### @objectstack/plugin-reports
-
-**Reports Plugin** — Metadata-driven report rendering and scheduling.
-
-- **Features**: Tabular/aggregate reports, scheduled delivery, exports
-- **When to use**: Operational reporting on top of business objects
-
---
## Framework Adapters
diff --git a/content/docs/protocol/backward-compatibility.mdx b/content/docs/protocol/backward-compatibility.mdx
index 7efdd4aafce..7a5986edd3b 100644
--- a/content/docs/protocol/backward-compatibility.mdx
+++ b/content/docs/protocol/backward-compatibility.mdx
@@ -111,7 +111,7 @@ Read the **Breaking?** column, not the version number: during the launch window
1. **RFC (Request for Comments)** — Breaking changes are proposed as GitHub issues with the `protocol:breaking` label.
2. **Deprecation** — The old behavior is deprecated in a MINOR release (see timeline above).
3. **Migration Guide** — A detailed migration guide is published before the removal lands, in the release notes for the version that carries it.
-4. **Release** — During the launch window the breaking change ships in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`. `scripts/check-changeset-no-major.mjs` fails any pull request that declares a `major` bump, because under lockstep one `major` would promote all 70 published packages.
+4. **Release** — During the launch window the breaking change ships in the next **MINOR** version, carrying a changeset entry marked `**BREAKING**`. `scripts/check-changeset-no-major.mjs` fails any pull request that declares a `major` bump, because under lockstep one `major` would promote all 69 published packages.
---
@@ -189,7 +189,7 @@ The `@objectstack/spec` package provides additional stability guarantees:
### Which surfaces this covers
-**All of them.** This is not scoped to an experimental corner or a pre-release channel: all **70** packages published from this repository belong to a single Changesets `fixed` group, so they share one version number and one policy. No published surface is exempt.
+**All of them.** This is not scoped to an experimental corner or a pre-release channel: all **69** packages published from this repository belong to a single Changesets `fixed` group, so they share one version number and one policy. No published surface is exempt.
The convention is enforced rather than informal — `scripts/check-changeset-no-major.mjs` fails any pull request that introduces a `major` bump, because under lockstep a single `major` on one package would promote the entire stack.
diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx
index 63d9431349f..7ab0fbd089e 100644
--- a/content/docs/protocol/objectql/query-syntax.mdx
+++ b/content/docs/protocol/objectql/query-syntax.mdx
@@ -1095,7 +1095,7 @@ label→value mapping in the expansion rather than by the operator.
because of a fold, and `FILTER_TEXT_CASES` holds all five drivers to that rule.
⚠️ **One axis is NOT aligned, it is not a case question, and which answer you see
depends on how you reach the data.** **Through the engine** — `ql.find()` and every
- surface above it (REST `where` / `$filter`, saved reports, list views) — a text
+ surface above it (REST `where` / `$filter`, flow node filters, list views) — a text
operator aimed at a field whose DECLARED type can never store a string is REFUSED
with `400 INVALID_FILTER` before any driver dispatch, naming the field and its
declared type, by the maintainer ruling on
diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx
index 27848a09d9a..ffadb96627e 100644
--- a/content/docs/references/api/contract.mdx
+++ b/content/docs/references/api/contract.mdx
@@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
-| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +331 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
+| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +322 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| **message** | `string` | ✅ | Readable error message |
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -291,13 +291,6 @@ const result = ApiErrorSchema.parse(data);
* `RECORD_LOCKED`
* `RECORD_NOT_ELIGIBLE`
* `REGISTRY_TYPE_NOT_CANONICAL`
-* `REPORTS_LIST_FAILED`
-* `REPORT_DELETE_FAILED`
-* `REPORT_GET_FAILED`
-* `REPORT_NOT_FOUND`
-* `REPORT_RUN_FAILED`
-* `REPORT_SAVE_FAILED`
-* `REPORT_SCHEDULE_FAILED`
* `REQUEST_NOT_FOUND`
* `RESEED_NO_ROWS`
* `RESEED_SKIPPED`
@@ -314,8 +307,6 @@ const result = ApiErrorSchema.parse(data);
* `RULE_NOT_FOUND`
* `RUN_NOT_FOUND`
* `SAML_REGISTER_FAILED`
-* `SCHEDULES_LIST_FAILED`
-* `SCHEDULE_DELETE_FAILED`
* `SELF_REGISTRATION_CLOSED`
* `SERVICE_NOT_REGISTERED`
* `SETTINGS_ACTION_FAILED`
diff --git a/content/docs/references/api/error-code-ledger.mdx b/content/docs/references/api/error-code-ledger.mdx
index 9cafe7575d8..8d1eb46d304 100644
--- a/content/docs/references/api/error-code-ledger.mdx
+++ b/content/docs/references/api/error-code-ledger.mdx
@@ -64,7 +64,7 @@ a framework package; see the scope rule above if yours ships from another
repo), SCREAMING_SNAKE (`^[A-Z][A-Z0-9_]*$` — lint-enforced by
`error-code-ledger.test.ts`), with a trailing `//` comment when the name
alone doesn't carry the meaning. Prefer a domain prefix for anything not
-self-evidently global (`ATTACHMENT_*`, `REPORT_*`, `SETTINGS_*`). If the
+self-evidently global (`ATTACHMENT_*`, `APPROVAL_*`, `SETTINGS_*`). If the
condition is generic (not found / permission / validation / rate limit),
use the standard catalog instead of registering a synonym.
@@ -458,13 +458,6 @@ const result = ErrorCode.parse(data);
* `RECORD_LOCKED`
* `RECORD_NOT_ELIGIBLE`
* `REGISTRY_TYPE_NOT_CANONICAL`
-* `REPORTS_LIST_FAILED`
-* `REPORT_DELETE_FAILED`
-* `REPORT_GET_FAILED`
-* `REPORT_NOT_FOUND`
-* `REPORT_RUN_FAILED`
-* `REPORT_SAVE_FAILED`
-* `REPORT_SCHEDULE_FAILED`
* `REQUEST_NOT_FOUND`
* `RESEED_NO_ROWS`
* `RESEED_SKIPPED`
@@ -481,8 +474,6 @@ const result = ErrorCode.parse(data);
* `RULE_NOT_FOUND`
* `RUN_NOT_FOUND`
* `SAML_REGISTER_FAILED`
-* `SCHEDULES_LIST_FAILED`
-* `SCHEDULE_DELETE_FAILED`
* `SELF_REGISTRATION_CLOSED`
* `SERVICE_NOT_REGISTERED`
* `SETTINGS_ACTION_FAILED`
diff --git a/content/docs/ui/apps.mdx b/content/docs/ui/apps.mdx
index a569a404d25..f2c7dc276b8 100644
--- a/content/docs/ui/apps.mdx
+++ b/content/docs/ui/apps.mdx
@@ -161,7 +161,7 @@ Groups items into collapsible sections with children:
### Report Navigation
-Links to a saved report:
+Links to a report — a `report` metadata item, named by `reportName`:
```typescript
{ id: 'nav_pipeline', type: 'report', label: 'Pipeline Report', reportName: 'sales_pipeline', icon: 'file-bar-chart' }
diff --git a/docs/adr/0096-execution-surface-identity-admission.md b/docs/adr/0096-execution-surface-identity-admission.md
index 5387eb30359..d956e18bef7 100644
--- a/docs/adr/0096-execution-surface-identity-admission.md
+++ b/docs/adr/0096-execution-surface-identity-admission.md
@@ -191,7 +191,7 @@ All trace to the same `positions==0 && permissions==0 && !userId → skip` predi
| Instance | Verdict | Issue |
|:---|:---|:---|
| Reports `getReport`/`deleteReport`/`listReports` discard the caller and query with `SYSTEM_CTX`; routes only `enforceAuth` → cross-user/cross-tenant IDOR (read+delete any report) | CONFIRMED exploitable | #2980 |
-| Scheduled reports (`packages/plugins/plugin-reports/src/report-service.ts#isSystem` `dispatchDue`) run `executeReport(..., {isSystem:true})` → a member-owned schedule emails the target object's **entire** table, RLS bypassed | CONFIRMED exploitable | #2980 |
+| Scheduled reports (`packages/plugins/plugin-reports/src/report-service.ts` `dispatchDue` — historical and deliberately unlinked: the saved-report stack was retired whole by #20102) run `executeReport(..., {isSystem:true})` → a member-owned schedule emails the target object's **entire** table, RLS bypassed | CONFIRMED exploitable | #2980 |
| Knowledge/RAG `applyPermissionFilter` (`packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter`) returns **all** hits when `ctx` is missing/system; `chatWithTools`'s `ToolExecutionContext.actor` is optional with a system fallback → agent retrieval escapes the data ceiling | CONFIRMED (framework); exposure gated on cloud impl | #2981 |
These are *not* the `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` fall-open (they use an unconditional `SYSTEM_CTX`), but they are exactly what a D4 conformance row (`caller-scoped?` proof) + the D2 audit would have flagged. Fixed independently of the mechanism, tracked as the mechanism's motivating evidence.
diff --git a/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md b/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
index 09a18668800..26f9615b87e 100644
--- a/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
+++ b/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
@@ -33,17 +33,17 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
| Measure | Value |
|---|---:|
-| Write call sites | 227 |
-| Object name statically decidable | 153 |
+| Write call sites | 218 |
+| Object name statically decidable | 144 |
| Object name chosen at run time | 74 |
-| Against a tenancy-enabled object | 152 |
+| Against a tenancy-enabled object | 143 |
| Against an object declaring tenancy off | 1 |
-| Threading a tenant context | 143 |
+| Threading a tenant context | 134 |
| Provably carrying none | 17 |
| …and decidably tenancy-enabled | 9 |
| Options argument unreadable | 67 |
| …and decidably tenancy-enabled | 34 |
-| Threading a decidably elevated context | 108 |
+| Threading a decidably elevated context | 99 |
| Threading a decidably non-elevated context | 0 |
| Threading a context of undecidable elevation | 102 |
@@ -90,13 +90,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
their values are not compared. The reasoning, and the measurement behind it,
are in `scripts/check-tenant-audit-census.mjs`.
-Measured on 2026-09-23 at `8f8828bf6`.
+Measured on 2026-09-25 at `62171c25e`.
| corpus scale (not enforced) | count |
| :--- | ---: |
-| tracked non-test sources scanned | 576 |
-| engine-shaped types recognised | 63 |
-| declared objects in the registry | 117 |
+| tracked non-test sources scanned | 572 |
+| engine-shaped types recognised | 61 |
+| declared objects in the registry | 115 |
| same-named calls subtracted as non-engine | 144 |
## Every site
@@ -153,12 +153,6 @@ Measured on 2026-09-23 at `8f8828bf6`.
| `packages/plugins/plugin-email/src/email-plugin.ts` | `insert` | `sys_email_template` | enabled | elevated | 1 |
| `packages/plugins/plugin-email/src/email-plugin.ts` | `update` | `sys_email_template` | enabled | elevated | 1 |
| `packages/plugins/plugin-pinyin-search/src/companion-projection.ts` | `update` | `schema.name` | undecidable | elevated | 1 |
-| `packages/plugins/plugin-reports/src/report-service.ts` | `delete` | `sys_report_schedule` | enabled | elevated | 2 |
-| `packages/plugins/plugin-reports/src/report-service.ts` | `insert` | `sys_report_schedule` | enabled | elevated | 1 |
-| `packages/plugins/plugin-reports/src/report-service.ts` | `update` | `sys_report_schedule` | enabled | elevated | 2 |
-| `packages/plugins/plugin-reports/src/report-service.ts` | `delete` | `sys_saved_report` | enabled | elevated | 1 |
-| `packages/plugins/plugin-reports/src/report-service.ts` | `insert` | `sys_saved_report` | enabled | elevated | 1 |
-| `packages/plugins/plugin-reports/src/report-service.ts` | `update` | `sys_saved_report` | enabled | elevated | 2 |
| `packages/plugins/plugin-security/src/auto-org-admin-grant.ts` | `delete` | `object` | undecidable | elevated | 1 |
| `packages/plugins/plugin-security/src/auto-org-admin-grant.ts` | `insert` | `object` | undecidable | elevated | 1 |
| `packages/plugins/plugin-security/src/bootstrap-builtin-positions.ts` | `insert` | `object` | undecidable | context, elevation undecidable | 1 |
diff --git a/docs/qa/platform-checklist/areas/dashboards.json b/docs/qa/platform-checklist/areas/dashboards.json
index ada007d90ec..be4d647630b 100644
--- a/docs/qa/platform-checklist/areas/dashboards.json
+++ b/docs/qa/platform-checklist/areas/dashboards.json
@@ -774,8 +774,8 @@
"id": "dashboards.saved-report-ownership",
"title": "Saved reports are owner-isolated: cross-owner read/run/delete deny as 404, schedules included",
"since": "v15.1",
- "status": "active",
- "revision": 2,
+ "status": "retired",
+ "revision": 3,
"priority": "P1",
"surface": "api",
"personas": [
@@ -836,9 +836,10 @@
"wrong-persona"
],
"source": [
- "packages/rest/src/rest-route-ledger.ts (reports family)",
- "packages/plugins/plugin-reports/src/report-service.ts#canAccessReport (canAccessReport #2980; unscheduleReport/listSchedules owner-gated in #6683)",
- "packages/platform-objects/src/audit/sys-saved-report.object.ts",
+ "packages/rest/src/reports-routes-retired.test.ts (the retirement pin: every former /api/v1/reports path answers the standard unmounted-route 404, byte-identical to a path never mounted)",
+ "packages/spec/src/migrations/entries/semantic/18.saved-report-stack-retired.ts#entry (the ADR-0087 record of the retirement — surface, replacement, and what happens to existing tables)",
+ "packages/spec/src/kernel/platform-capabilities.ts#RETIRED_PLATFORM_CAPABILITY_GUIDANCE (requires: ['reports'] is refused with the retirement prescription)",
+ "HISTORICAL (deleted by #20102): the ownership gate lived in the saved-report service's canAccessReport (#2980; schedule routes owner-gated in #6683) over the sys_saved_report object",
"docs/plans/release-15.1-test-plan.md §A10 (#2980/#2981/#2975)"
],
"history": [
@@ -853,8 +854,15 @@
"date": "2026-08-08",
"change": "D1 fixed in #6683 — unscheduleReport/listSchedules now owner-gated; flipped the known-gap probe clause to a positive deny-as-404 assertion and dropped the expected-FAIL language in negative/source",
"ref": "#6683"
+ },
+ {
+ "revision": 3,
+ "date": "2026-09-25",
+ "change": "retired — the saved-report stack it exercised was removed (#20102); see retiredReason",
+ "ref": "#20102"
}
- ]
+ ],
+ "retiredReason": "Capability removed (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-25, #20102): the saved-report stack — the /api/v1/reports routes this item drove, the sys_saved_report / sys_report_schedule objects, IReportService and @objectstack/plugin-reports — was retired whole, with zero consumers measured across objectstack, objectui and cloud. Every path this item called now answers the standard unmounted-route 404, pinned by packages/rest/src/reports-routes-retired.test.ts. No successor: a report is `report` metadata (dashboards.dataset-report-authoring), and a saved ad-hoc object query is a ListView."
},
{
"id": "dashboards.system-overview-live-counts",
@@ -1054,8 +1062,8 @@
"id": "dashboards.report-schedule-dispatch-delivery",
"title": "Scheduled report dispatch: the dispatcher ticks on a stock boot, a due schedule runs under the OWNER's authority or FAILS CLOSED with the refusal recorded on the row — never an RLS-bypassed run — and the delivery/bookkeeping contract (format fork, subject template, next-run advance) holds",
"since": "v15.1",
- "status": "active",
- "revision": 1,
+ "status": "retired",
+ "revision": 2,
"priority": "P1",
"surface": "api",
"personas": [
@@ -1133,18 +1141,13 @@
"clock-skew",
"wrong-persona"
],
- "automated": {
- "kind": "test",
- "ref": "packages/plugins/plugin-reports/src/report-service.test.ts — the dispatch describes: owner-scoped execution + fails-closed-without-resolver (#2980), csv attachment, subject template, cron/interval advance; the LIVE halves (dispatcher registration, the fail-closed row write on a real boot, the log arms) still need the run"
- },
"source": [
- "packages/plugins/plugin-reports/src/report-service.ts#dispatchDue (dispatchDue: due filter on next_run_at; owner fail-closed with the verbatim #2849/#2980 refusal; format fork + CJK-safe filename; renderSubject; skipped arm), (nextRunAt — cron wins, schedule timezone, warned fallback), (advanceSchedule 'ok'/markSchedule), (scheduleReport — first next_run_at, eager cron validation, recipients required)",
- "packages/plugins/plugin-reports/src/reports-plugin.ts#resolveOwnerContext (optional email + the no-email warn), (resolveOwnerContext: undefined — the recorded fail-closed posture pending ADR-0073 M2), (dispatcher: job-service arm 'reports.dispatch' or setInterval fallback, min 5s / default 60s)",
- "packages/plugins/plugin-reports/src/report-service.test.ts (the unit pins cited by clauses 3-5)",
- "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS (CAPABILITY_PROVIDERS.reports → @objectstack/plugin-reports — the dispatcher runs on stock boots)",
- "packages/plugins/plugin-email/src/email-plugin.ts#provider (stock provider 'log' → LogTransport, 'mail will NOT be sent' — why delivery evidence is a log line)",
- "docs/plans/release-15.1-test-plan.md §A10 (reports IDOR / 定时报表 RLS — #2980/#2981/#2975; the ownership half was imported as dashboards.saved-report-ownership, this item is the 定时报表-RLS dispatch leg that never was)",
- "cross-ref: dashboards.saved-report-ownership (the /reports* owner-isolation surfaces, including schedule routes #6683)"
+ "packages/spec/src/stack.zod.ts#validateKnownCapabilities (a stack still declaring requires: ['reports'] is refused — nothing mounts the retired dispatcher)",
+ "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS (no `reports` row since #20102 — the stock boot mounts no saved-report dispatcher)",
+ "packages/plugins/plugin-email/src/email-plugin.ts#provider (stock provider 'log' → LogTransport — unchanged; cited when this item was active)",
+ "HISTORICAL (deleted by #20102): the dispatch loop lived in the saved-report service's dispatchDue / nextRunAt / scheduleReport and the plugin's resolveOwnerContext, with unit pins in its own test file",
+ "docs/plans/release-15.1-test-plan.md §A10 (reports IDOR / 定时报表 RLS — #2980/#2981/#2975)",
+ "cross-ref: dashboards.saved-report-ownership (retired with this item)"
],
"history": [
{
@@ -1152,8 +1155,15 @@
"date": "2026-08-30",
"change": "new item (2026-08-30 sweep, angle 5): the dispatch loop was the un-imported leg of release-15.1 §A10 (定时报表 RLS) — nothing asserted the dispatcher runs on a real boot or that a due schedule cannot run RLS-bypassed. Grounded against source with one major correction to the register hypothesis: the register's positive framing ('a due schedule fires under the OWNER's RLS … mail lands at dev transport') is unreachable live at head, because reports-plugin.ts wires resolveOwnerContext: undefined (fail-closed pending ADR-0073 M2) and stock mail is the LogTransport — so the LIVE assertion is the fail-closed row write with the verbatim #2849/#2980 refusal (an 'ok' at head is the FAIL), and the owner-scoped/delivery/bookkeeping clauses are pinned at the unit layer with an explicit flip-to-live tripwire for when the resolver lands. Also recorded: no on-demand door drives dispatchDue (tick-wait protocol), and the skipped arm is unreachable through the owner API (deleteReport cascades schedules)",
"ref": "#sweep-2026-08-30"
+ },
+ {
+ "revision": 2,
+ "date": "2026-09-25",
+ "change": "retired — the saved-report stack it exercised was removed (#20102); see retiredReason",
+ "ref": "#20102"
}
- ]
+ ],
+ "retiredReason": "Capability removed (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-25, #20102): the saved-report schedule dispatcher this item drove left with the rest of the saved-report stack (sys_report_schedule, the /api/v1/reports schedule routes, @objectstack/plugin-reports), which had zero consumers. The platform has no scheduled delivery of a report today; scheduled delivery for `report` metadata is explicitly NOT built by that ruling — a real need is a new card on the metadata kind, and a new item with it."
}
]
}
\ No newline at end of file
diff --git a/docs/qa/platform-checklist/coverage.json b/docs/qa/platform-checklist/coverage.json
index 690af861c72..7f79565cb27 100644
--- a/docs/qa/platform-checklist/coverage.json
+++ b/docs/qa/platform-checklist/coverage.json
@@ -252,8 +252,6 @@
"items": [
"dashboards.dataset-report-authoring",
"dashboards.drill-through-range",
- "dashboards.saved-report-ownership",
- "dashboards.report-schedule-dispatch-delivery",
"search.command-palette-navigation"
]
},
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 20857858328..00734966e81 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -292,7 +292,7 @@ const slotLookupPlugin = {
// nothing anywhere reports.
//
// #4674 is the bill: two internal queries spelled their sort
-// `{ field, direction: 'desc' }` — `IReportService`'s vocabulary — where the
+// `{ field, direction: 'desc' }` — another contract's vocabulary — where the
// QueryAST shape is `SortNodeSchema` = `{ field, order }`. Both drivers
// normalize off `.order` with no fallback, so both queries ran ASCENDING, and
// because both carried a `limit` the wrong direction changed WHICH ROWS came
@@ -352,7 +352,7 @@ export const QUERY_OPTIONS_ANY_MESSAGE =
'caller `tsc` is the ONLY channel that enforces them: the protocol\'s ingress ' +
'normalizer does not run on a direct engine call, and the options schemas are not ' +
'`.strict()`, so an unknown key is silently DROPPED, never rejected. That is #4674 ' +
- '— two queries sorted by `direction` (IReportService\'s vocabulary) instead of ' +
+ '— two queries sorted by `direction` (another contract\'s vocabulary) instead of ' +
'`order` (SortNodeSchema\'s), both with a `limit`, so both quietly returned the ' +
'OLDEST rows: audit history that never showed an object\'s recent changes, and a ' +
'global search that truncated away the freshly-edited records. The declared type ' +
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 0d8330cba3a..176024d636b 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -83,7 +83,6 @@
"@objectstack/plugin-auth": "workspace:*",
"@objectstack/plugin-email": "workspace:*",
"@objectstack/plugin-hono-server": "workspace:*",
- "@objectstack/plugin-reports": "workspace:*",
"@objectstack/plugin-security": "workspace:*",
"@objectstack/plugin-pinyin-search": "workspace:*",
"@objectstack/plugin-sharing": "workspace:*",
diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts
index 6b1c0140a0c..5ed005c90b6 100644
--- a/packages/cli/src/commands/serve.ts
+++ b/packages/cli/src/commands/serve.ts
@@ -23,7 +23,7 @@ import {
// the legacy boolean in as its unset-fallback. serve's last direct reader of the
// boolean was the banner, and that was exactly the drift #4801 fixed.
import { readEnvWithDeprecation, resolveTenancyPosture, resolveAllowDegradedTenancy, isMcpServerEnabled, stampSearchPinyinEnabled, isModuleNotFoundError } from '@objectstack/types';
-import { PLATFORM_CAPABILITY_TOKENS, PLATFORM_ALWAYS_ON_CAPABILITIES } from '@objectstack/spec/kernel';
+import { PLATFORM_CAPABILITY_TOKENS, PLATFORM_ALWAYS_ON_CAPABILITIES, RETIRED_PLATFORM_CAPABILITY_GUIDANCE } from '@objectstack/spec/kernel';
// The posture vocabulary, read from the package that DEFINES it (#5359) — the
// boot gate's fix list enumerates the accepted values, and a second literal
// list would be free to drift the day a posture is added.
@@ -2001,11 +2001,6 @@ export default class Serve extends Command {
export: 'PinyinSearchPlugin',
identities: ['com.objectstack.plugin.pinyin-search', 'PinyinSearchPlugin'],
},
- reports: {
- pkg: '@objectstack/plugin-reports',
- export: 'ReportsServicePlugin',
- identities: ['com.objectstack.service.reports', 'ReportsServicePlugin'],
- },
approvals: {
pkg: '@objectstack/plugin-approvals',
export: 'ApprovalsServicePlugin',
@@ -2818,13 +2813,13 @@ export default class Serve extends Command {
if (!requires.includes(cap)) requires.push(cap);
}
}
- // The email + approvals + reports services schedule background work
- // (durable retries, SLA escalation, scheduled digests). Auto-pull
+ // The email + approvals services schedule background work
+ // (durable retries, SLA escalation). Auto-pull
// 'job' and 'queue' so plugins can opt into durable scheduling.
// IMPORTANT: prepend, so their plugins load (and their kernel:ready
// hooks fire) BEFORE consumers like email/approvals that subscribe
// to queues during their own kernel:ready phase.
- const NEEDS_JOB_OR_QUEUE = ['email', 'approvals', 'reports', 'auth'];
+ const NEEDS_JOB_OR_QUEUE = ['email', 'approvals', 'auth'];
if (NEEDS_JOB_OR_QUEUE.some((c) => requires.includes(c))) {
if (!requires.includes('queue')) requires.unshift('queue');
if (!requires.includes('job')) requires.unshift('job');
@@ -4617,8 +4612,15 @@ export default class Serve extends Command {
// (`serve-capability-vocabulary.test.ts`), and `@objectstack/spec` and
// this package release in one fixed version group.
if (declaredRequires.has(cap) && !PLATFORM_CAPABILITY_TOKENS.includes(cap)) {
+ // A RETIRED token (e.g. `reports`, #20102) is not a typo: say what
+ // replaced it, in the same words `defineStack` refuses it with.
+ const retired = Object.prototype.hasOwnProperty.call(RETIRED_PLATFORM_CAPABILITY_GUIDANCE, cap)
+ ? RETIRED_PLATFORM_CAPABILITY_GUIDANCE[cap]
+ : undefined;
console.warn(chalk.yellow(
- ` ⚠ requires: "${cap}" is not a known platform capability — check for a typo. It was ignored.`,
+ retired
+ ? ` ⚠ ${retired} It was ignored.`
+ : ` ⚠ requires: "${cap}" is not a known platform capability — check for a typo. It was ignored.`,
));
}
continue;
diff --git a/packages/cli/src/utils/capability-preflight.ts b/packages/cli/src/utils/capability-preflight.ts
index 04d9f599ec9..355e5a2e60e 100644
--- a/packages/cli/src/utils/capability-preflight.ts
+++ b/packages/cli/src/utils/capability-preflight.ts
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
import path from 'node:path';
import {
classifyRequiredCapability,
+ RETIRED_PLATFORM_CAPABILITY_GUIDANCE,
type CapabilityClassification,
} from '@objectstack/spec/kernel';
@@ -93,6 +94,12 @@ export function renderCapabilityMessage(c: CapabilityClassification): string {
);
}
case 'unknown':
+ // A RETIRED token is not a typo — the word used to be right — so it gets
+ // its retirement prescription, the same text `defineStack` refuses it
+ // with and `os serve` warns with, never "check for a typo".
+ if (Object.prototype.hasOwnProperty.call(RETIRED_PLATFORM_CAPABILITY_GUIDANCE, c.token)) {
+ return RETIRED_PLATFORM_CAPABILITY_GUIDANCE[c.token];
+ }
return `requires: "${c.token}" is not a known platform capability — check for a typo.`;
case 'ok':
default:
@@ -110,7 +117,8 @@ export interface CapabilityPreflightResult {
readonly errors: CapabilityClassification[];
/**
* Advisory findings — `installable` (absent but addable → `pnpm add` hint) and
- * `unknown` (a typo). Never fatal.
+ * `unknown` (a typo, or a retired token, which renders its retirement
+ * prescription). Never fatal.
*/
readonly warnings: CapabilityClassification[];
}
diff --git a/packages/cli/test/capability-preflight.test.ts b/packages/cli/test/capability-preflight.test.ts
index e310665ec3c..beeec6663a9 100644
--- a/packages/cli/test/capability-preflight.test.ts
+++ b/packages/cli/test/capability-preflight.test.ts
@@ -8,6 +8,7 @@ import {
import {
classifyRequiredCapability,
PLATFORM_CAPABILITY_PROVIDERS,
+ RETIRED_PLATFORM_CAPABILITY_GUIDANCE,
} from '@objectstack/spec/kernel';
// framework#3366 — the CLI-side resolution + message layer over the spec-owned
@@ -41,6 +42,12 @@ describe('preflightRequiredCapabilities (#3366)', () => {
expect(r.warnings.map((c) => c.status)).toEqual(['unknown']);
});
+ it('a retired token keeps the unknown-token posture: advisory, never fatal', () => {
+ const r = call(['reports'], () => true);
+ expect(r.errors).toHaveLength(0);
+ expect(r.warnings.map((c) => [c.token, c.status])).toEqual([['reports', 'unknown']]);
+ });
+
it('dedupes and ignores non-string entries', () => {
const r = call(['ai', 'ai', 42, null, 'ai'], () => false);
expect(r.errors).toHaveLength(1);
@@ -91,6 +98,25 @@ describe('renderCapabilityMessage (#3366)', () => {
it('unknown token reads as a typo hint', () => {
expect(msgFor('automations', () => true)).toContain('not a known platform capability');
});
+
+ it('a RETIRED token renders its retirement prescription, never the typo hint', () => {
+ // `os validate` / `os build` parse a plain-object config without the
+ // `defineStack` vocabulary check, so this renderer is the only text an
+ // author at those doors sees. It must be the spec-owned prescription
+ // verbatim — the same words `defineStack` refuses the token with.
+ const c = classifyRequiredCapability('reports', () => true);
+ expect(c.status).toBe('unknown');
+ const prescription = RETIRED_PLATFORM_CAPABILITY_GUIDANCE.reports;
+ expect(prescription, 'the retirement map must carry a `reports` row').toBeTruthy();
+ const m = renderCapabilityMessage(c);
+ expect(m).toBe(prescription);
+ expect(m).not.toContain('check for a typo');
+ // Control: an ordinary misspelling still gets the typo hint, and the
+ // retired-map lookup is an own-property read (an inherited name such as
+ // `constructor` is not a retired token).
+ expect(msgFor('reportz', () => true)).toContain('check for a typo');
+ expect(renderCapabilityMessage({ token: 'constructor', status: 'unknown' })).toContain('check for a typo');
+ });
});
describe('missingProviderMessage — serve reads identically to the preflight (#3366)', () => {
diff --git a/packages/cli/test/requires-retired-capability.e2e.test.ts b/packages/cli/test/requires-retired-capability.e2e.test.ts
new file mode 100644
index 00000000000..13f2bb40486
--- /dev/null
+++ b/packages/cli/test/requires-retired-capability.e2e.test.ts
@@ -0,0 +1,127 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * A RETIRED `requires` token reads as its retirement prescription at the
+ * `os validate` / `os build` door — never as "check for a typo".
+ *
+ * Both commands accept a PLAIN-OBJECT config (`export default { … }`, no
+ * `defineStack` call). That config is parsed with
+ * `ObjectStackDefinitionSchema.safeParse`, whose `requires` is a bare string
+ * array — the `defineStack` vocabulary check never runs — and the only text
+ * the author sees for a `requires` token is the capability preflight's
+ * `renderCapabilityMessage`. For a word that USED to be a capability (the
+ * saved-report stack's `reports`), the typo advice is wrong in a way that
+ * sends the author hunting for a spelling instead of deleting the token.
+ *
+ * Posture is unchanged: an unknown token is an ADVISORY at this door (exit 0,
+ * a `{ token, message }` record in `warnings`); only its TEXT is asserted
+ * here, and it is asserted EQUAL to the spec-owned prescription — the same
+ * string `defineStack` refuses the token with and `os serve` warns with.
+ *
+ * A misspelled token rides in the same fixture as the control, so "the
+ * prescription is shown" cannot pass against a renderer that shows it for
+ * every unknown token.
+ */
+
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { execFile } from 'node:child_process';
+import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { RETIRED_PLATFORM_CAPABILITY_GUIDANCE } from '@objectstack/spec/kernel';
+import { childEnv } from './helpers/serve-process.js';
+
+const HERE = resolve(fileURLToPath(import.meta.url), '..');
+const CLI = resolve(HERE, '../bin/run-dev.js');
+const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
+
+interface Run {
+ code: number;
+ stdout: string;
+ stderr: string;
+}
+
+function runCli(args: string[], cwd: string): Promise {
+ return new Promise((resolvePromise) => {
+ execFile(
+ TSX,
+ [CLI, ...args],
+ { cwd, maxBuffer: 16 * 1024 * 1024, env: childEnv({ NO_COLOR: '1' }) },
+ (err, stdout, stderr) => {
+ resolvePromise({
+ code: err ? (typeof (err as { code?: unknown }).code === 'number' ? (err as unknown as { code: number }).code : 1) : 0,
+ stdout: String(stdout),
+ stderr: String(stderr),
+ });
+ },
+ );
+ });
+}
+
+function capabilityHints(run: Run, label: string): Map {
+ let payload: { warnings?: unknown };
+ try {
+ payload = JSON.parse(run.stdout) as { warnings?: unknown };
+ } catch {
+ throw new Error(`${label}: stdout was not one JSON document (exit ${run.code})\n${run.stdout}\n${run.stderr}`);
+ }
+ const hints = new Map();
+ for (const w of Array.isArray(payload.warnings) ? payload.warnings : []) {
+ if (typeof w === 'object' && w !== null && 'token' in w) {
+ const r = w as { token: unknown; message: unknown };
+ hints.set(String(r.token), String(r.message));
+ }
+ }
+ return hints;
+}
+
+const RETIRED_TOKEN = 'reports';
+const TYPO_TOKEN = 'reportz';
+
+/** A plain-object config: no `defineStack`, so no parse-time vocabulary check. */
+const CONFIG = `
+export default {
+ manifest: { id: 'com.example.retiredcap', name: 'retiredcap', version: '1.0.0', type: 'app', namespace: 'retiredcap' },
+ requires: ['${RETIRED_TOKEN}', '${TYPO_TOKEN}'],
+ objects: [
+ {
+ name: 'rc_thing',
+ label: 'Thing',
+ sharingModel: 'private',
+ fields: { title: { type: 'text', label: 'Title' } },
+ },
+ ],
+};
+`;
+
+let dir = '';
+
+beforeAll(() => {
+ dir = mkdtempSync(join(tmpdir(), 'os-retired-cap-'));
+ writeFileSync(join(dir, 'objectstack.config.ts'), CONFIG);
+});
+
+afterAll(() => {
+ if (dir) rmSync(dir, { recursive: true, force: true });
+});
+
+describe('a retired `requires` token at the `os validate` / `os build` door (plain-object config)', () => {
+ const prescription = RETIRED_PLATFORM_CAPABILITY_GUIDANCE[RETIRED_TOKEN];
+
+ it('the spec carries a prescription for the token under test', () => {
+ expect(prescription, 'RETIRED_PLATFORM_CAPABILITY_GUIDANCE lost its `reports` row').toBeTruthy();
+ });
+
+ for (const command of ['validate', 'build'] as const) {
+ it(`os ${command} --json: advisory posture, the retired token carries the prescription, the typo keeps the typo hint`, async () => {
+ const run = await runCli([command, '--json'], dir);
+ expect(run.code, `os ${command} --json failed:\n${run.stdout}${run.stderr}`).toBe(0);
+ const hints = capabilityHints(run, `os ${command} --json`);
+ expect([...hints.keys()].sort()).toEqual([RETIRED_TOKEN, TYPO_TOKEN].sort());
+ expect(hints.get(RETIRED_TOKEN)).toBe(prescription);
+ expect(hints.get(RETIRED_TOKEN)).not.toContain('check for a typo');
+ expect(hints.get(TYPO_TOKEN)).toContain('check for a typo');
+ }, 180_000);
+ }
+});
diff --git a/packages/cli/test/serve-capability-identity.test.ts b/packages/cli/test/serve-capability-identity.test.ts
index ccdd30a5d5d..1fe47ad9cfd 100644
--- a/packages/cli/test/serve-capability-identity.test.ts
+++ b/packages/cli/test/serve-capability-identity.test.ts
@@ -113,7 +113,6 @@ const EXPECTED_PROVIDER_NAME: Record = {
sms: 'com.objectstack.service.sms',
sharing: 'com.objectstack.service.sharing',
'pinyin-search': 'com.objectstack.plugin.pinyin-search',
- reports: 'com.objectstack.service.reports',
approvals: 'com.objectstack.service.approvals',
settings: 'com.objectstack.service.settings',
webhooks: 'com.objectstack.plugin-webhook-outbox',
diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts
index daf3312c6a4..91feb61557f 100644
--- a/packages/client/src/client.test.ts
+++ b/packages/client/src/client.test.ts
@@ -390,93 +390,21 @@ describe('ObjectStackClient', () => {
});
});
-describe('Reports namespace (#3587 gap closure)', () => {
- it('reports.list pins GET /reports with filters and unwraps {data}', async () => {
- const { client, fetchMock } = createMockClient({ data: [{ id: 'r1' }] });
- const rows = await client.reports.list({ object: 'lead', ownerId: 'u1' });
- expect(String(fetchMock.mock.calls[0][0])).toBe(
- 'http://localhost:3000/api/v1/reports?object=lead&ownerId=u1',
- );
- expect(rows).toEqual([{ id: 'r1' }]);
- });
-
- it('reports.save pins POST /reports', async () => {
- const { client, fetchMock } = createMockClient({ id: 'r1' });
- await client.reports.save({ name: 'Pipeline', object: 'lead', query: { fields: ['id'] } });
- const [url, init] = fetchMock.mock.calls[0];
- expect(String(url)).toBe('http://localhost:3000/api/v1/reports');
- expect(init.method).toBe('POST');
- expect(JSON.parse(init.body)).toEqual({ name: 'Pipeline', object: 'lead', query: { fields: ['id'] } });
- });
-
- // [#11926] The literal below is the ORIGINAL fixture of the test above,
- // preserved verbatim rather than repaired. For as long as `reports.save`
- // took `any` it sat there constructing an input the service contract
- // REFUSES — `SaveReportInput.query` is required — against a mock transport
- // that never reaches a service, so no run could ever have failed on it. It
- // is evidence, and giving it a `query` would have silenced the evidence
- // without closing anything. So it moves here, and the compiler asserts the
- // refusal instead.
- //
- // This is a bidirectional pin, not a comment. `client.test.ts` is compiled
- // by `tsconfig.test.json` — named by this package's `typecheck` script —
- // and holds no `test-typecheck-debt.json` entry, so an unlisted file must
- // have zero errors. Widen the parameter back to `any` and the directive
- // below stops matching an error: tsc reds with TS2578, "unused
- // '@ts-expect-error' directive". It cannot rot into a phantom check.
- it('[#11926] reports.save refuses a query-less report at the type level', async () => {
- const { client, fetchMock } = createMockClient({ id: 'r1' });
- // @ts-expect-error — `query` is required by `SaveReportInput`.
- await client.reports.save({ name: 'Pipeline', object: 'lead' });
- // The SDK is a transport, not a second validator: the request still
- // goes out unaltered. The refusal ON THE WIRE belongs to the route and
- // is pinned in packages/rest/src/rest.test.ts — see
- // 'POST /reports refuses a body the service contract requires more of'.
- expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ name: 'Pipeline', object: 'lead' });
- });
-
- it('reports.get / delete pin /reports/:id and delete tolerates 204', async () => {
- const { client, fetchMock } = createMockClient({ id: 'r1' });
- await client.reports.get('r1');
- expect(String(fetchMock.mock.calls[0][0])).toBe('http://localhost:3000/api/v1/reports/r1');
-
- const del = createMockClient(undefined, 204);
- // A 204 has no JSON body — the method must not try to parse one.
- del.fetchMock.mockResolvedValue({ ok: true, status: 204, statusText: 'No Content', json: async () => { throw new Error('no body'); }, headers: new Headers() });
- const out = await del.client.reports.delete('r1');
- expect(String(del.fetchMock.mock.calls[0][0])).toBe('http://localhost:3000/api/v1/reports/r1');
- expect(del.fetchMock.mock.calls[0][1].method).toBe('DELETE');
- expect(out).toEqual({ deleted: true });
- });
-
- it('reports.run pins POST /reports/:id/run', async () => {
- const { client, fetchMock } = createMockClient({ rows: [] });
- await client.reports.run('r1');
- const [url, init] = fetchMock.mock.calls[0];
- expect(String(url)).toBe('http://localhost:3000/api/v1/reports/r1/run');
- expect(init.method).toBe('POST');
- });
-
- it('reports.schedule pins POST /reports/:id/schedule with the schedule body', async () => {
- const { client, fetchMock } = createMockClient({ id: 's1' });
- await client.reports.schedule('r1', { recipients: ['a@example.com'], cronExpression: '0 8 * * 1' });
- const [url, init] = fetchMock.mock.calls[0];
- expect(String(url)).toBe('http://localhost:3000/api/v1/reports/r1/schedule');
- expect(JSON.parse(init.body)).toEqual({ recipients: ['a@example.com'], cronExpression: '0 8 * * 1' });
- });
-
- it('reports.listSchedules / unschedule pin the schedule routes', async () => {
- const { client, fetchMock } = createMockClient({ data: [{ id: 's1' }] });
- const rows = await client.reports.listSchedules('r1');
- expect(String(fetchMock.mock.calls[0][0])).toBe('http://localhost:3000/api/v1/reports/r1/schedules');
- expect(rows).toEqual([{ id: 's1' }]);
-
- const del = createMockClient(undefined, 204);
- del.fetchMock.mockResolvedValue({ ok: true, status: 204, statusText: 'No Content', json: async () => { throw new Error('no body'); }, headers: new Headers() });
- const out = await del.client.reports.unschedule('s1');
- expect(String(del.fetchMock.mock.calls[0][0])).toBe('http://localhost:3000/api/v1/reports/schedules/s1');
- expect(del.fetchMock.mock.calls[0][1].method).toBe('DELETE');
- expect(out).toEqual({ deleted: true });
+describe('[#20102] the saved-report namespace is retired', () => {
+ // The `reports` namespace left with the `/api/v1/reports` routes it
+ // called. Both halves are pinned: a caller that still writes
+ // `client.reports` is a compile error (tsc checks this file through
+ // `tsconfig.test.json`, so an unused directive reds as TS2578 the moment
+ // the namespace comes back), and at runtime there is no such member to
+ // reach for.
+ it('has no `reports` member, at the type level or at runtime', () => {
+ const { client, fetchMock } = createMockClient({ data: [] });
+ // @ts-expect-error — `reports` was retired; a report is `report` metadata.
+ const retired = client.reports;
+ expect(retired).toBeUndefined();
+ expect('reports' in client).toBe(false);
+ // Nothing was sent: the member is absent, not a stub that still calls out.
+ expect(fetchMock).not.toHaveBeenCalled();
});
});
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 6f056822de8..4f334d3148a 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -165,10 +165,6 @@ import type {
ObjectDraft,
RecordShare,
RemoteTable,
- ReportRunResult,
- ReportSchedule,
- SaveReportInput,
- SavedReport,
SchemaValidationReport,
ScreenSpec,
SendEmailResult,
@@ -6353,109 +6349,11 @@ export class ObjectStackClient {
return this.unwrapResponse(res);
};
- /**
- * Saved reports (#3587 gap closure)
- *
- * Tenant-wide report definitions, execution, and recurring email
- * schedules, served by `@objectstack/plugin-reports` behind the REST
- * surface. Every route 501s [NOT_IMPLEMENTED] on deployments without the
- * reports service. Fixed path — `reports` is not in `ApiRoutesSchema`.
- */
- reports = {
- /** List saved reports, optionally filtered by object or owner. */
- list: async (opts?: { object?: string; ownerId?: string }): Promise => {
- const params = new URLSearchParams();
- if (opts?.object) params.set('object', opts.object);
- if (opts?.ownerId) params.set('ownerId', opts.ownerId);
- const qs = params.toString();
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports${qs ? `?${qs}` : ''}`);
- const body = await this.unwrapResponse<{ data?: SavedReport[] } | SavedReport[]>(res);
- return Array.isArray(body) ? body : (body?.data ?? []);
- },
-
- /**
- * Create or update a saved report definition.
- *
- * [#11926] The parameter is the service contract's own `SaveReportInput`,
- * not `any`: `name`, `object` and `query` are required, and omitting one is
- * a compile error here rather than a surprise from whichever reports
- * implementation the deployment mounts. The wire refusal is the route's —
- * `POST /reports` answers 400 [VALIDATION_FAILED] for the same three keys,
- * so a JavaScript caller that never sees this type is refused too.
- */
- save: async (report: SaveReportInput): Promise => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports`, {
- method: 'POST',
- body: JSON.stringify(report ?? {}),
- });
- return this.unwrapResponse(res);
- },
-
- /** Get a saved report by id. 404 [REPORT_NOT_FOUND] when absent. */
- get: async (id: string): Promise => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports/${encodeURIComponent(id)}`);
- return this.unwrapResponse(res);
- },
-
- /** Delete a saved report; its schedules cascade. */
- delete: async (id: string): Promise<{ deleted: boolean }> => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports/${encodeURIComponent(id)}`, {
- method: 'DELETE',
- });
- if (res.status === 204) return { deleted: true };
- return this.unwrapResponse<{ deleted: boolean }>(res);
- },
-
- /** Execute a saved report and return its rendered output. */
- run: async (id: string): Promise => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports/${encodeURIComponent(id)}/run`, {
- method: 'POST',
- body: JSON.stringify({}),
- });
- return this.unwrapResponse(res);
- },
-
- /**
- * Create a recurring email schedule for a report. Provide either
- * `intervalMinutes` or `cronExpression`; `recipients` is required.
- */
- schedule: async (
- id: string,
- opts: {
- recipients: string[];
- name?: string;
- intervalMinutes?: number;
- cronExpression?: string;
- timezone?: string;
- format?: string;
- subjectTemplate?: string;
- ownerId?: string;
- active?: boolean;
- },
- ): Promise => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports/${encodeURIComponent(id)}/schedule`, {
- method: 'POST',
- body: JSON.stringify(opts),
- });
- return this.unwrapResponse(res);
- },
-
- /** List the recurring schedules attached to a report. */
- listSchedules: async (id: string): Promise => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports/${encodeURIComponent(id)}/schedules`);
- const body = await this.unwrapResponse<{ data?: ReportSchedule[] } | ReportSchedule[]>(res);
- return Array.isArray(body) ? body : (body?.data ?? []);
- },
-
- /** Delete a schedule by its id (report-independent path). */
- unschedule: async (scheduleId: string): Promise<{ deleted: boolean }> => {
- const res = await this.fetch(`${this.baseUrl}/api/v1/reports/schedules/${encodeURIComponent(scheduleId)}`, {
- method: 'DELETE',
- });
- if (res.status === 204) return { deleted: true };
- return this.unwrapResponse<{ deleted: boolean }>(res);
- },
- };
+ // The former `reports` namespace (saved-report definitions, runs and
+ // e-mail schedules) was retired with its server routes in #20102 — the
+ // saved-report stack had no consumer. A report is `report` metadata, read
+ // through `meta.*` and run through `analytics.*`; a saved ad-hoc object
+ // query is a ListView.
// The former `views` CRUD namespace was removed in #3612 — no server
// surface mounts /ui/views (both surfaces serve only /ui/view/:object…).
diff --git a/packages/client/src/return-type-precision.test.ts b/packages/client/src/return-type-precision.test.ts
index 8439bd817db..b204de42bc8 100644
--- a/packages/client/src/return-type-precision.test.ts
+++ b/packages/client/src/return-type-precision.test.ts
@@ -87,8 +87,6 @@ import type {
ImportObjectResult,
RecordShare,
RemoteTable,
- ReportSchedule,
- SavedReport,
SearchResult,
ShareLink,
SharingRuleRow,
@@ -154,8 +152,6 @@ export async function returnTypePrecisionPins(): Promise {
// ── shape class 3: array ELEMENT typed through the client's own unwrap ─
// These routes answer `{ data: rows }` with no `success` flag, so
// `unwrapResponse` passes it through and the method folds it to an array.
- expectTypeOf(await client.reports.list()).toEqualTypeOf();
- expectTypeOf(await client.reports.listSchedules('rep_1')).toEqualTypeOf();
expectTypeOf(await client.shares.list('lead', 'rec_1')).toEqualTypeOf();
expectTypeOf(await client.shares.rules.list()).toEqualTypeOf();
expectTypeOf(await client.shareLinks.list()).toEqualTypeOf();
@@ -239,9 +235,6 @@ export async function returnTypePrecisionPins(): Promise {
// @ts-expect-error the route answers `{ tables }`, not a bare array
const wrongTables: RemoteTable[] = await client.datasources.external.listTables('ds');
- // @ts-expect-error `reports.list` answers SavedReport[], not a single row
- const wrongReport: SavedReport = await client.reports.list();
-
// @ts-expect-error a flow definition is not an execution log
const wrongFlow: ExecutionLog = await client.automation.getFlow('flow_a');
@@ -259,7 +252,6 @@ export async function returnTypePrecisionPins(): Promise {
void wrongScope;
void wrongTables;
- void wrongReport;
void wrongFlow;
void wrongSearch;
void wrongClone;
diff --git a/packages/core/src/fallbacks/index.ts b/packages/core/src/fallbacks/index.ts
index 114db6567c8..db631c9fcff 100644
--- a/packages/core/src/fallbacks/index.ts
+++ b/packages/core/src/fallbacks/index.ts
@@ -25,9 +25,10 @@ export {
* (maintainer ruling 2026-08-22). `createMemoryJob()`'s `schedule()` records a
* job and never fires it, so pre-injecting it made every "prefer the platform
* job service, else own a timer" consumer take the job-service branch and then
- * silently never run: `plugin-reports` logged `dispatcher registered with job
- * service` and dispatched nothing, ever (measured: 0 reads of
- * `sys_report_schedule` in 5600 ms with the success line present). With no
+ * silently never run: the saved-report plugin (since retired, #20102) logged
+ * `dispatcher registered with job service` and dispatched nothing, ever
+ * (measured: 0 reads of its schedule table in 5600 ms with the success line
+ * present). With no
* entry here, `getService('job')` throws when no job plugin is installed,
* every consumer's documented no-job-service path becomes reachable (they all
* already run on `LiteKernel`, which injects no fallbacks), and the kernel
diff --git a/packages/core/src/security/operation-private-keys.pin.test.ts b/packages/core/src/security/operation-private-keys.pin.test.ts
index 1aa9592225a..ff11fcf8ba0 100644
--- a/packages/core/src/security/operation-private-keys.pin.test.ts
+++ b/packages/core/src/security/operation-private-keys.pin.test.ts
@@ -331,14 +331,14 @@ describe('the `__` operation-private-key convention has one owner (#7284)', () =
'the scan reaches the packages that used to hold the copies',
() => {
// The second half of the same anti-vacuity guard: prove the scan actually
- // descends into the three consumer packages, so a future refactor of the
- // scan surface cannot silently narrow it to `packages/core`.
+ // descends into the consumer packages, so a future refactor of the scan
+ // surface cannot silently narrow it to `packages/core`. (A third consumer,
+ // the saved-report service, was retired whole in #20102.)
const scanned = scannedFiles();
for (const consumer of [
'packages/plugins/plugin-audit/src/comment-access-hooks.ts',
'packages/services/service-storage/src/attachment-access-hooks.ts',
- 'packages/plugins/plugin-reports/src/report-service.ts',
]) {
expect(scanned).toContain(consumer);
}
diff --git a/packages/core/src/security/operation-private-keys.ts b/packages/core/src/security/operation-private-keys.ts
index e382e5d8285..82cce21c75b 100644
--- a/packages/core/src/security/operation-private-keys.ts
+++ b/packages/core/src/security/operation-private-keys.ts
@@ -52,7 +52,7 @@
* permission sets for the new object (`if (permissionSets.length > 0)`), so a
* stale depth SURVIVES into a question it was never resolved for whenever that
* branch does not fire. A REST request that touched another object before
- * reaching, say, `/reports/:id/run` hands over an envelope the middleware has
+ * reaching, say, an attachment download hands over an envelope the middleware has
* already written into.
*
* Dropping is safe in the one direction that matters: the middleware re-stamps
@@ -74,12 +74,12 @@
*
* ## Known consumers
*
- * `plugin-audit` (`comment-access-hooks.ts`, #7141), `service-storage`
- * (`attachment-access-hooks.ts`, #7145) and `plugin-reports`
- * (`report-service.ts`, #7204) — each forwarding a caller envelope to a gate or
- * a read that asks about a parent/target object rather than about the object the
- * middleware resolved for. Each of the three grew its own byte-equivalent copy
- * of this file by hand before #7284 gave the rule a home; `operation-private-
+ * `plugin-audit` (`comment-access-hooks.ts`, #7141) and `service-storage`
+ * (`attachment-access-hooks.ts`, #7145) — each forwarding a caller envelope to a
+ * gate or a read that asks about a parent/target object rather than about the
+ * object the middleware resolved for. They and a third consumer (the saved-report
+ * service, #7204 — retired whole in #20102) each grew a byte-equivalent copy of
+ * this file by hand before #7284 gave the rule a home; `operation-private-
* keys.pin.test.ts` is what now catches a fourth.
*/
diff --git a/packages/drivers/driver-memory/src/memory-driver-query-narrowing.test.ts b/packages/drivers/driver-memory/src/memory-driver-query-narrowing.test.ts
index d5a2dc9329e..fce0eff3bbb 100644
--- a/packages/drivers/driver-memory/src/memory-driver-query-narrowing.test.ts
+++ b/packages/drivers/driver-memory/src/memory-driver-query-narrowing.test.ts
@@ -146,7 +146,7 @@ describe('InMemoryDriver — driver-owned query methods take DriverQuery (#6212
it('restores the `orderBy` check a blanket cast switched off (#4674)', async () => {
// `orderBy` is `SortNode[]` (`{ field, order }`), closed since #4721. The
- // `direction` spelling is `IReportService`'s vocabulary and sorted the
+ // `direction` spelling is another contract's vocabulary and sorted the
// wrong way in silence. Before this batch the only way to hand `aggregate`
// a bare AST was `as any`, which switched this check off with it.
await driver.aggregate(tbl, {
diff --git a/packages/lint/src/validate-sortable-fields.ts b/packages/lint/src/validate-sortable-fields.ts
index 2c899d1e7b8..47b255663c3 100644
--- a/packages/lint/src/validate-sortable-fields.ts
+++ b/packages/lint/src/validate-sortable-fields.ts
@@ -101,18 +101,15 @@
*
* NOT walked, each verified against the schema rather than assumed:
*
- * - **A saved report's `query.orderBy`.** Verified: it is not an authoring
- * surface at all. `sys_saved_report` is a platform OBJECT and the envelope
- * lives in its `query_json` COLUMN (`packages/platform-objects/src/audit/
- * sys-saved-report.object.ts`, `contracts/report-service.ts`) — a runtime
- * record written through the reports API, never a key in stack metadata.
- * The stack's own `reports[]` is `ReportSchema`, whose ADR-0021 single-form
- * cutover REMOVED the inline query; what it declares instead is
- * `order[].by`, naming a dataset dimension or measure, and `checkReportOrder`
- * already refines that against what the report selects. So there is no
- * authored `query.orderBy` for a stack rule to reach; the engine door
- * (#7095) is the only door that surface has, which is precisely why #7095
- * added it.
+ * - **A report's `query.orderBy`.** Verified: no such authoring surface
+ * exists. The one place a raw report query ever lived was a runtime record
+ * (the saved-report stack's `query_json` column), never a key in stack
+ * metadata — and that stack was retired whole in #20102. The stack's own
+ * `reports[]` is `ReportSchema`, whose ADR-0021 single-form cutover REMOVED
+ * the inline query; what it declares instead is `order[].by`, naming a
+ * dataset dimension or measure, and `checkReportOrder` already refines that
+ * against what the report selects. So there is no authored `query.orderBy`
+ * for a stack rule to reach.
* - **Flow node sort config.** Verified: none exists.
* `automation/builtin-node-config.zod.ts`'s record-reading node declares
* `limit` and no ordering key at all, and no schema under
diff --git a/packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts b/packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts
index dd442d5899c..316318bcb8f 100644
--- a/packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts
+++ b/packages/metadata-protocol/src/protocol.orderby-vocabulary.test.ts
@@ -7,9 +7,9 @@
// (`packages/spec/src/data/query.zod.ts`), and both real drivers normalize off
// `.order` with no fallback — `sql-driver` maps `item.order === 'desc'`,
// `mongodb-driver` the same. With `order` absent, `undefined === 'desc'` is
-// false and both land on ASCENDING. `direction` is `IReportService`'s
-// vocabulary; it is a genuinely different contract, which is how the wrong
-// spelling looked plausible.
+// false and both land on ASCENDING. `direction` is another contract's
+// vocabulary (better-auth's adapter `sortBy`); it is a genuinely different
+// contract, which is how the wrong spelling looked plausible.
//
// Because both queries carry a `limit`, the wrong direction did not merely
// reorder a page — it changed WHICH ROWS CAME BACK. So these tests assert on
diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts
index 1df09c2eb43..ba5fbccde60 100644
--- a/packages/metadata-protocol/src/protocol.ts
+++ b/packages/metadata-protocol/src/protocol.ts
@@ -3432,8 +3432,8 @@ function invalidSortError(
* This is the one unknown key on this axis that has a KNOWN right answer, so it
* gets a rejection that carries the translation rather than a generic refusal.
* `direction` is not a typo — it is the live vocabulary of a neighbouring
- * contract (`IReportService.orderBy`, `spec/src/contracts/report-service.ts`),
- * which `plugin-auth/objectql-adapter.ts` already translates to `order` by hand.
+ * contract (better-auth's adapter `sortBy`), which
+ * `plugin-auth/objectql-adapter.ts` already translates to `order` by hand.
* A necessary translation nothing enforced is exactly ADR-0049's shape.
*
* Measured on `main` before this rejection existed, on the schema side of the
@@ -3460,7 +3460,7 @@ function invalidSortDirectionKeyError(param: string, field: string): Error {
{
hint:
` Write \`{ field: '${field}', order: 'desc' }\`. \`direction\` is`
- + " `IReportService.orderBy`'s vocabulary, a genuinely different contract; on this"
+ + " the better-auth adapter's `sortBy` vocabulary, a genuinely different contract; on this"
+ ' axis it was silently dropped and `order` fell back to `asc`, so a descending'
+ ' request came back ascending — and with `limit`, a different set of rows.',
extra: { field, key: 'direction' },
@@ -9049,8 +9049,8 @@ export class ObjectStackProtocolImplementation implements
};
// `order`, NOT `direction`: the QueryAST sort shape is
// `SortNodeSchema` = `{ field, order }`, and both drivers normalize
- // off `.order` with no fallback. `direction` is `IReportService`'s
- // vocabulary and is silently DROPPED here (the schema is not
+ // off `.order` with no fallback. `direction` is another contract's
+ // vocabulary (better-auth's adapter `sortBy`) and is silently DROPPED here (the schema is not
// `.strict()`), which left this query running ascending — the
// OLDEST `limit` audit events, i.e. the beginning of an object's
// life and never its recent changes (#4674). The `as any` is gone
@@ -9513,8 +9513,8 @@ export class ObjectStackProtocolImplementation implements
* SCOPE: this is an INGRESS gate, so it covers what reaches {@link
* findData}. The half it cannot reach — a caller handing a `where` straight
* to `engine.find` / `findOne` / `count` / `aggregate` / `update` /
- * `delete`, which is how a saved report's `query.filter` travels
- * (`plugin-reports` forwards it verbatim) — is closed at the engine's own
+ * `delete`, which is how a flow node's `config.filter` travels — is closed
+ * at the engine's own
* filter seam by `assertFilterIsMaterializable` (`@objectstack/objectql`,
* `filter-comparand-shape.ts`), with the same `400 INVALID_FIELD` and the
* same remedy sentence. Same two-door shape, and same reason, as the sort
@@ -9790,9 +9790,9 @@ export class ObjectStackProtocolImplementation implements
* sentence this gate emits, ruled on #7095 (an ORDER BY the engine cannot
* apply is a refusal with guidance prose, never a silent drop). What made
* leaving it at ingress untenable is that the direct path is AUTHOR-
- * reachable, not merely internal: a saved report's `query.orderBy` is
- * forwarded verbatim into `engine.find` (`plugin-reports`), and it never
- * passes through here.
+ * reachable, not merely internal: a saved report's `query.orderBy` was
+ * forwarded verbatim into `engine.find` (by the saved-report stack, since
+ * retired in #20102), and it never passed through here.
*
* ONE EDGE, measured and deliberately left: a nested `expand` sort is also
* forwarded into the expansion sub-read (`expandRelatedRecords`), and the
diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts
index d1ed9ee6feb..54ddcbc1f0d 100644
--- a/packages/objectql/src/engine.ts
+++ b/packages/objectql/src/engine.ts
@@ -1273,9 +1273,9 @@ function assertOrderByIsMaterializable(
* SQL renders `"account"."name"` against a table that was never joined, the DB
* answers `no such column`, and the #3821 recovery ladder retries `select('*')`.
* The caller asked to narrow and silently received EVERY field, byte-identical
- * to no projection at all. A saved report's `query.fields` reaches this the
- * same way (`plugin-reports` forwards it verbatim), as does every hook and
- * internal caller.
+ * to no projection at all. Every hook and internal caller reaches this the
+ * same way (a saved report's `query.fields` did too, until the saved-report
+ * stack was retired in #20102).
*
* WHY A REFUSAL: ruled 2026-08-12 on #7589 (adopting the drivers seat's
* Option B) — a dotted entry the engine cannot resolve is refused loudly at
diff --git a/packages/objectql/src/filter-comparand-shape.ts b/packages/objectql/src/filter-comparand-shape.ts
index 3b09a0ee563..0eab708c29c 100644
--- a/packages/objectql/src/filter-comparand-shape.ts
+++ b/packages/objectql/src/filter-comparand-shape.ts
@@ -147,10 +147,11 @@ export function assertListComparandShapes(
*
* WHY AT THIS SEAM AND NOT ONLY AT INGRESS: #7095 had to add
* `assertOrderByIsMaterializable` inside this package because a saved report's
- * `query.orderBy` is forwarded verbatim into `engine.find` and never passes the
- * REST door. Filters travel the SAME path — `plugin-reports`' `executeReport`
- * calls `this.engine.find(report.object_name, { where: q.filter, … })` — so an
- * ingress-only fix would have left the author-reachable half open. This gate
+ * `query.orderBy` was forwarded verbatim into `engine.find` and never passed the
+ * REST door. Filters traveled the SAME path — the saved-report executor (the
+ * stack was retired in #20102) handed `q.filter` to the engine's `find` as its
+ * `where`, and a flow node's `config.filter` still does — so an ingress-only fix
+ * would have left the author-reachable half open. This gate
* runs inside `lowerWhereFilterArray`, the one seam EVERY caller-supplied
* `where` passes through (`find` / `findOne` / `count` / `aggregate` / `update`
* / `delete`), which is what makes a new verb unable to miss it by omission.
diff --git a/packages/objectql/src/query-expression-conformance.test.ts b/packages/objectql/src/query-expression-conformance.test.ts
index a45c67d9823..e4a997b4c93 100644
--- a/packages/objectql/src/query-expression-conformance.test.ts
+++ b/packages/objectql/src/query-expression-conformance.test.ts
@@ -411,7 +411,7 @@ describe('#4226 — sort / select / expand on the list path (real ObjectQL engin
* but a different set of rows — the "latest N" footgun below, reached
* through a spelling rather than a typo.
*
- * `direction` is `IReportService.orderBy`'s live vocabulary, which
+ * `direction` is the better-auth adapter's `sortBy` vocabulary, which
* `plugin-auth/objectql-adapter.ts` already translates by hand; the schema
* half of this door (`SortNodeSchema`'s `aliases: { direction: 'order' }`)
* landed in the same change.
@@ -992,11 +992,11 @@ describe('#4226 — sort / select / expand on the list path (real ObjectQL engin
['delete', (e: ObjectQL) => e.delete('showcase_task', { where: { is_open: true }, multi: true })],
])('`engine.%s` REFUSES it too — the door the REST ingress cannot reach', async (_verb, call) => {
// The half an ingress-only fix leaves open, and it is AUTHOR-reachable
- // rather than merely internal: `plugin-reports`' `executeReport`
- // forwards a saved report's `query.filter` verbatim into
- // `engine.find(object, { where: q.filter, … })`, exactly as #7095
- // measured for `query.orderBy`. Flows and dashboards travel the same
- // path. Every verb that accepts a caller `where` passes through the one
+ // rather than merely internal: a flow node's `config.filter` reaches the
+ // engine's `find` verbatim as its `where` (so did a saved report's
+ // `query.filter`, until the saved-report stack was retired in #20102),
+ // exactly as #7095 measured for `query.orderBy`. Dashboards travel the
+ // same path. Every verb that accepts a caller `where` passes through the one
// lowering seam this gate lives in, which is why all six answer alike.
await expect(call(engine)).rejects.toMatchObject({
status: 400,
@@ -1342,9 +1342,9 @@ describe('#4226 — sort / select / expand on the list path (real ObjectQL engin
['update', (e: ObjectQL) => e.update('showcase_task', { status: 'done' }, { where: { 'project_id.name': 'Apollo' }, multi: true })],
['delete', (e: ObjectQL) => e.delete('showcase_task', { where: { 'project_id.name': 'Apollo' }, multi: true })],
])('`engine.%s` refuses the dotted spelling too — the door a saved report reaches', async (_verb, call) => {
- // Same author-reachable surfaces as the #8296 verdict one block up:
- // `plugin-reports` forwards a saved report's `query.filter` verbatim
- // into `engine.find`, never passing the ingress.
+ // Same author-reachable surfaces as the #8296 verdict one block up: a
+ // flow node's `config.filter` reaches `engine.find` verbatim, never
+ // passing the ingress.
await expect(call(engine)).rejects.toMatchObject({
status: 400,
code: 'INVALID_FIELD',
diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts
index 138c1401d93..b1613326be8 100644
--- a/packages/platform-objects/scripts/i18n-extract.config.ts
+++ b/packages/platform-objects/scripts/i18n-extract.config.ts
@@ -167,8 +167,6 @@ import {
SysAttachment,
SysEmail,
SysEmailTemplate,
- SysSavedReport,
- SysReportSchedule,
// sys_approval_* moved to @objectstack/plugin-approvals (ADR-0029 K2.b / D8).
SysJob,
SysJobRun,
@@ -302,8 +300,6 @@ const config: ObjectStackDefinition = defineStack({
SysAttachment,
SysEmail,
SysEmailTemplate,
- SysSavedReport,
- SysReportSchedule,
// sys_approval_* moved to @objectstack/plugin-approvals (ADR-0029 K2.b / D8).
SysJob,
SysJobRun,
diff --git a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts
index 12de4a2861a..f26bb215155 100644
--- a/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts
+++ b/packages/platform-objects/src/apps/translations/bundle-ownership.test.ts
@@ -33,8 +33,8 @@ const OWNED_OBJECTS = new Set([
'sys_scim_subject', 'sys_scim_user',
// audit / messaging-adjacent (still owned here)
'sys_notification', 'sys_attachment', 'sys_email', 'sys_email_template',
- 'sys_saved_report', 'sys_report_schedule', 'sys_job', 'sys_job_run', 'sys_job_queue',
- 'sys_import_job',
+ // (sys_saved_report / sys_report_schedule retired with the saved-report stack, #20102)
+ 'sys_job', 'sys_job_run', 'sys_job_queue', 'sys_import_job',
// metadata
'sys_metadata', 'sys_metadata_history', 'sys_view_definition', 'sys_metadata_audit',
// system
diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts
index 3ecc03566b9..af486d23de0 100644
--- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts
+++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts
@@ -2646,130 +2646,6 @@ export const enObjects: NonNullable = {
}
}
},
- sys_saved_report: {
- label: "Saved Report",
- pluralLabel: "Saved Reports",
- description: "Persisted ObjectQL report definition — re-runnable and schedulable",
- fields: {
- id: {
- label: "Report ID"
- },
- name: {
- label: "Name"
- },
- description: {
- label: "Description"
- },
- object_name: {
- label: "Object",
- help: "Short object name the report queries"
- },
- query_json: {
- label: "Query",
- help: "ObjectQL query envelope — { filter, fields, orderBy, limit, groupBy }"
- },
- format: {
- label: "Format",
- help: "Rendering used by IReportService.run() and email digests",
- options: {
- csv: "csv",
- json: "json",
- html_table: "html_table"
- }
- },
- owner_id: {
- label: "Owner",
- help: "User that owns the report definition (drives sharing)"
- },
- last_run_at: {
- label: "Last Run",
- help: "Stamped by IReportService.run() on successful execution"
- },
- last_row_count: {
- label: "Last Row Count"
- },
- created_at: {
- label: "Created At"
- },
- updated_at: {
- label: "Updated At"
- }
- }
- },
- sys_report_schedule: {
- label: "Report Schedule",
- pluralLabel: "Report Schedules",
- description: "Recurring delivery of a sys_saved_report via email",
- fields: {
- id: {
- label: "Schedule ID"
- },
- report_id: {
- label: "Report"
- },
- name: {
- label: "Name",
- help: "Optional label for the digest — used in the email subject"
- },
- interval_minutes: {
- label: "Interval (minutes)",
- help: "How often to send (1440 = daily, 10080 = weekly)"
- },
- cron_expression: {
- label: "Cron Expression",
- help: "Optional 5/6-field cron — overrides interval_minutes when present"
- },
- timezone: {
- label: "Timezone"
- },
- active: {
- label: "Active"
- },
- recipients: {
- label: "Recipients",
- help: "Comma-separated email addresses"
- },
- format: {
- label: "Format",
- help: "Render format — csv is attached, html_table is inlined",
- options: {
- csv: "csv",
- html_table: "html_table"
- }
- },
- subject_template: {
- label: "Subject Template",
- help: "Email subject; {{name}} / {{date}} / {{rows}} are substituted"
- },
- owner_id: {
- label: "Owner"
- },
- next_run_at: {
- label: "Next Run",
- help: "Dispatcher loads schedules where next_run_at <= now"
- },
- last_sent_at: {
- label: "Last Sent"
- },
- last_status: {
- label: "Last Status",
- options: {
- ok: "ok",
- failed: "failed",
- skipped: "skipped"
- }
- },
- last_error: {
- label: "Last Error"
- },
- created_at: {
- label: "Created At"
- },
- updated_at: {
- label: "Updated At"
- }
- }
- },
sys_job: {
label: "Background Job",
pluralLabel: "Background Jobs",
diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts
index 23b9117eb8b..20ef8ed1746 100644
--- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts
+++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts
@@ -2646,130 +2646,6 @@ export const esESObjects: NonNullable = {
}
}
},
- sys_saved_report: {
- label: "Informe guardado",
- pluralLabel: "Informes guardados",
- description: "Definición de informe ObjectQL persistida; se puede volver a ejecutar y programar",
- fields: {
- id: {
- label: "ID de informe"
- },
- name: {
- label: "Nombre"
- },
- description: {
- label: "Descripción"
- },
- object_name: {
- label: "Objeto",
- help: "Nombre corto del objeto que consulta el informe."
- },
- query_json: {
- label: "Consulta",
- help: "Envoltorio de consulta ObjectQL: { filter, fields, orderBy, limit, groupBy }."
- },
- format: {
- label: "Formato",
- help: "Renderizado utilizado por IReportService.run() y los resúmenes por correo.",
- options: {
- csv: "CSV",
- json: "JSON",
- html_table: "Tabla HTML"
- }
- },
- owner_id: {
- label: "Propietario",
- help: "Usuario propietario de la definición del informe (controla la compartición)."
- },
- last_run_at: {
- label: "Última ejecución",
- help: "Marcado por IReportService.run() tras una ejecución correcta."
- },
- last_row_count: {
- label: "Último recuento de filas"
- },
- created_at: {
- label: "Creado el"
- },
- updated_at: {
- label: "Actualizado el"
- }
- }
- },
- sys_report_schedule: {
- label: "Programación de informe",
- pluralLabel: "Programaciones de informes",
- description: "Entrega recurrente de un sys_saved_report por correo electrónico",
- fields: {
- id: {
- label: "ID de programación"
- },
- report_id: {
- label: "Informe"
- },
- name: {
- label: "Nombre",
- help: "Nombre visible opcional para el resumen; se usa en el asunto del correo."
- },
- interval_minutes: {
- label: "Intervalo (minutos)",
- help: "Frecuencia de envío (1440 = diario, 10080 = semanal)."
- },
- cron_expression: {
- label: "Expresión Cron",
- help: "Cron opcional de 5/6 campos; sustituye a interval_minutes cuando está presente."
- },
- timezone: {
- label: "Zona horaria"
- },
- active: {
- label: "Activo"
- },
- recipients: {
- label: "Destinatarios",
- help: "Direcciones de correo electrónico separadas por comas."
- },
- format: {
- label: "Formato",
- help: "Formato de renderizado; csv se adjunta y html_table se inserta en línea.",
- options: {
- csv: "CSV",
- html_table: "Tabla HTML"
- }
- },
- subject_template: {
- label: "Plantilla de asunto",
- help: "Asunto del correo; se sustituyen {{name}} / {{date}} / {{rows}}."
- },
- owner_id: {
- label: "Propietario"
- },
- next_run_at: {
- label: "Próxima ejecución",
- help: "El despachador carga las programaciones cuyo next_run_at <= now."
- },
- last_sent_at: {
- label: "Último envío"
- },
- last_status: {
- label: "Último estado",
- options: {
- ok: "Correcto",
- failed: "Fallido",
- skipped: "Omitido"
- }
- },
- last_error: {
- label: "Último error"
- },
- created_at: {
- label: "Creado el"
- },
- updated_at: {
- label: "Actualizado el"
- }
- }
- },
sys_job: {
label: "Tarea en segundo plano",
pluralLabel: "Tareas en segundo plano",
diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts
index 375e8e0ec97..12c0136f779 100644
--- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts
+++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts
@@ -2646,130 +2646,6 @@ export const jaJPObjects: NonNullable = {
}
}
},
- sys_saved_report: {
- label: "保存済みレポート",
- pluralLabel: "保存済みレポート",
- description: "永続化された ObjectQL レポート定義 — 再実行およびスケジュール可能",
- fields: {
- id: {
- label: "レポート ID"
- },
- name: {
- label: "名前"
- },
- description: {
- label: "説明"
- },
- object_name: {
- label: "オブジェクト",
- help: "レポートがクエリするオブジェクトの短い名前"
- },
- query_json: {
- label: "クエリ",
- help: "ObjectQL クエリエンベロープ — { filter, fields, orderBy, limit, groupBy }"
- },
- format: {
- label: "フォーマット",
- help: "IReportService.run() とメールダイジェストで使用するレンダリング",
- options: {
- csv: "CSV",
- json: "JSON",
- html_table: "HTML テーブル"
- }
- },
- owner_id: {
- label: "所有者",
- help: "レポート定義を所有するユーザー(共有を促進)"
- },
- last_run_at: {
- label: "最終実行",
- help: "正常実行時に IReportService.run() がスタンプします"
- },
- last_row_count: {
- label: "最終行数"
- },
- created_at: {
- label: "作成日時"
- },
- updated_at: {
- label: "更新日時"
- }
- }
- },
- sys_report_schedule: {
- label: "レポートスケジュール",
- pluralLabel: "レポートスケジュール",
- description: "sys_saved_report のメールによる定期配信",
- fields: {
- id: {
- label: "スケジュール ID"
- },
- report_id: {
- label: "レポート"
- },
- name: {
- label: "名前",
- help: "ダイジェストの任意の表示名 — メール件名で使用"
- },
- interval_minutes: {
- label: "間隔(分)",
- help: "送信頻度(1440 = 毎日、10080 = 毎週)"
- },
- cron_expression: {
- label: "Cron 式",
- help: "オプションの 5/6 フィールド Cron — 存在する場合 interval_minutes を上書き"
- },
- timezone: {
- label: "タイムゾーン"
- },
- active: {
- label: "有効"
- },
- recipients: {
- label: "受信者",
- help: "カンマ区切りのメールアドレス"
- },
- format: {
- label: "フォーマット",
- help: "レンダリング形式 — CSV は添付、HTML テーブルはインライン",
- options: {
- csv: "CSV",
- html_table: "HTML テーブル"
- }
- },
- subject_template: {
- label: "件名テンプレート",
- help: "メール件名。{{name}} / {{date}} / {{rows}} が置換されます"
- },
- owner_id: {
- label: "所有者"
- },
- next_run_at: {
- label: "次回実行",
- help: "next_run_at <= 現在時刻のスケジュールをディスパッチャーが読み込みます"
- },
- last_sent_at: {
- label: "最終送信日時"
- },
- last_status: {
- label: "最終ステータス",
- options: {
- ok: "正常",
- failed: "失敗",
- skipped: "スキップ"
- }
- },
- last_error: {
- label: "最終エラー"
- },
- created_at: {
- label: "作成日時"
- },
- updated_at: {
- label: "更新日時"
- }
- }
- },
sys_job: {
label: "ジョブ",
pluralLabel: "ジョブ",
diff --git a/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts b/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts
index 610d1fabd30..7003fe8f49f 100644
--- a/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts
+++ b/packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts
@@ -301,7 +301,7 @@ const DECISIONS: readonly Decision[] = [
en: 'Autonumber Format',
verdict: ALL_TRANSLATE,
reason:
- 'A compound of two ordinary words: Format is authored as 格式 / フォーマット / Formato at three other paths (dataset.fields.measures.format, sys_saved_report.format, sys_report_schedule.format), and this panel authors its other format-shaped label, maskingRule, rather than keeping it English.',
+ 'A compound of two ordinary words: Format is authored as 格式 / フォーマット / Formato at dataset.fields.measures.format (sys_saved_report.format and sys_report_schedule.format carried it too until the saved-report stack was retired), and this panel authors its other format-shaped label, maskingRule, rather than keeping it English.',
},
{
type: 'object',
diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts
index b43770758d8..805d2d11df2 100644
--- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts
+++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts
@@ -2646,130 +2646,6 @@ export const zhCNObjects: NonNullable = {
}
}
},
- sys_saved_report: {
- label: "保存的报表",
- pluralLabel: "保存的报表",
- description: "持久化的 ObjectQL 报表定义——可重复运行并可调度",
- fields: {
- id: {
- label: "报表 ID"
- },
- name: {
- label: "名称"
- },
- description: {
- label: "描述"
- },
- object_name: {
- label: "对象",
- help: "该报表查询的短对象名"
- },
- query_json: {
- label: "查询",
- help: "ObjectQL 查询封装——{ filter, fields, orderBy, limit, groupBy }"
- },
- format: {
- label: "格式",
- help: "IReportService.run() 和邮件摘要使用的渲染格式",
- options: {
- csv: "CSV",
- json: "JSON",
- html_table: "HTML 表格"
- }
- },
- owner_id: {
- label: "所有者",
- help: "拥有该报表定义的用户(决定共享)"
- },
- last_run_at: {
- label: "上次运行时间",
- help: "由 IReportService.run() 在成功执行时写入"
- },
- last_row_count: {
- label: "上次行数"
- },
- created_at: {
- label: "创建时间"
- },
- updated_at: {
- label: "更新时间"
- }
- }
- },
- sys_report_schedule: {
- label: "报表计划",
- pluralLabel: "报表计划",
- description: "通过邮件周期性投递 sys_saved_report",
- fields: {
- id: {
- label: "计划 ID"
- },
- report_id: {
- label: "报表"
- },
- name: {
- label: "名称",
- help: "摘要的可选标签——用于邮件主题"
- },
- interval_minutes: {
- label: "间隔(分钟)",
- help: "发送频率(1440 = 每日,10080 = 每周)"
- },
- cron_expression: {
- label: "Cron 表达式",
- help: "可选 5/6 段 Cron——存在时覆盖 interval_minutes"
- },
- timezone: {
- label: "时区"
- },
- active: {
- label: "启用"
- },
- recipients: {
- label: "收件人",
- help: "以逗号分隔的邮箱地址"
- },
- format: {
- label: "格式",
- help: "渲染格式——csv 作为附件,html_table 内嵌在正文中",
- options: {
- csv: "CSV",
- html_table: "HTML 表格"
- }
- },
- subject_template: {
- label: "主题模板",
- help: "邮件主题;会替换 {{name}} / {{date}} / {{rows}}"
- },
- owner_id: {
- label: "所有者"
- },
- next_run_at: {
- label: "下次运行时间",
- help: "调度器会加载 next_run_at <= now 的计划"
- },
- last_sent_at: {
- label: "上次发送时间"
- },
- last_status: {
- label: "最近状态",
- options: {
- ok: "成功",
- failed: "失败",
- skipped: "已跳过"
- }
- },
- last_error: {
- label: "最近错误"
- },
- created_at: {
- label: "创建时间"
- },
- updated_at: {
- label: "更新时间"
- }
- }
- },
sys_job: {
label: "后台任务",
pluralLabel: "后台任务",
diff --git a/packages/platform-objects/src/audit/index.ts b/packages/platform-objects/src/audit/index.ts
index c474dad81d2..937efe826b4 100644
--- a/packages/platform-objects/src/audit/index.ts
+++ b/packages/platform-objects/src/audit/index.ts
@@ -13,8 +13,7 @@ export { SysNotification } from './sys-notification.object.js';
export { SysAttachment } from './sys-attachment.object.js';
export { SysEmail } from './sys-email.object.js';
export { SysEmailTemplate } from './sys-email-template.object.js';
-export { SysSavedReport } from './sys-saved-report.object.js';
-export { SysReportSchedule } from './sys-report-schedule.object.js';
+// sys_saved_report / sys_report_schedule retired with the saved-report stack (#20102).
// sys_approval_request / sys_approval_action moved to @objectstack/plugin-approvals (ADR-0029 K2.b).
export { SysJob } from './sys-job.object.js';
export { SysJobRun } from './sys-job-run.object.js';
diff --git a/packages/platform-objects/src/audit/platform-iana-timezone-columns.test.ts b/packages/platform-objects/src/audit/platform-iana-timezone-columns.test.ts
index c96f9948d28..9c30e7a3e57 100644
--- a/packages/platform-objects/src/audit/platform-iana-timezone-columns.test.ts
+++ b/packages/platform-objects/src/audit/platform-iana-timezone-columns.test.ts
@@ -1,44 +1,34 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
/**
- * #15872 — the platform's two OLDER IANA columns, `sys_job.timezone` and
- * `sys_report_schedule.timezone`, predate `valueDomain` and disagreed with each
- * other in three dimensions at once (length 100 vs 64, default none vs `'UTC'`,
- * validation none vs none). This file pins what that card actually changed and,
- * just as deliberately, what it did NOT.
+ * #15872 — `sys_job.timezone`, the platform's OLDER IANA column, predates
+ * `valueDomain`. It had a sibling, `sys_report_schedule.timezone`, and the two
+ * disagreed in three dimensions at once (length 100 vs 64, default none vs
+ * `'UTC'`, validation none vs none); that sibling was retired with the
+ * saved-report stack (#20102), so what this file pins is the surviving column —
+ * what #15872 changed on it and, just as deliberately, what it did NOT.
*
- * CLOSED here — validation. Both columns now declare
- * `valueDomain: 'iana_time_zone'`, the same declaration the ruled pair
- * `sys_business_unit.timezone` / `sys_organization.timezone` carries (#14238,
- * pinned in `identity/org-hierarchy-timezone.test.ts`). Four columns, one
- * membership predicate.
+ * CLOSED here — validation. The column declares `valueDomain: 'iana_time_zone'`,
+ * the same declaration the ruled pair `sys_business_unit.timezone` /
+ * `sys_organization.timezone` carries (#14238, pinned in
+ * `identity/org-hierarchy-timezone.test.ts`). Three columns, one membership
+ * predicate.
*
* LEFT ALONE, and pinned so that staying alone is a decision rather than a
* drift someone repairs by reflex:
*
- * - the DEFAULTS still differ, because a default here is a CONSUMER semantic,
- * not a shape question. `sys_report_schedule` documents "default UTC" and its
- * reader falls back to `'UTC'`; `sys_job` says nothing, and giving it one
- * would change what an unset row means. The ruled pair, for its own reasons,
- * has none on either column — so "all four agree" is NOT the invariant, and
- * a test asserting it would be asserting a bug.
- * - the BOUNDS still differ (100 vs 64). `maxLength` is not only a write bound:
- * it reaches DDL, and narrowing a physical `varchar(100)` is `driver-sql`'s
+ * - NO DEFAULT. A default here is a CONSUMER semantic, not a shape question:
+ * `sys_job.timezone` is written and never read, and giving it a default
+ * would change what an unset row means.
+ * - the BOUND stays 100. `maxLength` is not only a write bound: it reaches
+ * DDL, and narrowing a physical `varchar(100)` is `driver-sql`'s
* `narrow_varchar` op at severity `error`, category destructive. What the
- * column physically holds in a deployment is not readable from the repo, so
- * the convergence is a separate decision and #15872 stays open on it.
- *
- * The reader measurement that decided the card's severity is recorded beside
- * each declaration, not here: the `sys_job` column is written and never read,
- * while the `sys_report_schedule` column is read back into croner by
- * `ReportService.nextRunAt`, whose catch turned a non-member zone into a silent
- * fall back to `interval_minutes` — the wrong instant, forever.
+ * column physically holds in a deployment is not readable from the repo.
*/
import { describe, it, expect } from 'vitest';
import { isValueDomainMember } from '@objectstack/spec/shared';
import { SysJob } from './sys-job.object';
-import { SysReportSchedule } from './sys-report-schedule.object';
type ColumnShape = {
type?: unknown;
@@ -49,23 +39,17 @@ type ColumnShape = {
};
const jobColumn = () => (SysJob.fields as Record).timezone;
-const scheduleColumn = () => (SysReportSchedule.fields as Record).timezone;
-describe('#15872 — the platform\'s two older IANA time-zone columns', () => {
- it('reads the real declarations, not an empty probe', () => {
+describe('#15872 — the platform\'s older IANA time-zone column', () => {
+ it('reads the real declaration, not an empty probe', () => {
// Vacuity control: a renamed column or a changed export would otherwise let
// every assertion below pass over `undefined`.
expect(SysJob.name).toBe('sys_job');
- expect(SysReportSchedule.name).toBe('sys_report_schedule');
expect(jobColumn()).toBeTypeOf('object');
- expect(scheduleColumn()).toBeTypeOf('object');
});
- it.each([
- ['sys_job', jobColumn],
- ['sys_report_schedule', scheduleColumn],
- ])('%s.timezone is an optional text column validated against the IANA domain', (_object, column) => {
- const c = column();
+ it('sys_job.timezone is an optional text column validated against the IANA domain', () => {
+ const c = jobColumn();
// `VALUE_DOMAIN_FIELD_TYPES` is `{text}`, so the declaration below is also
// the reason the type must stay `text`.
expect(c.type).toBe('text');
@@ -73,49 +57,42 @@ describe('#15872 — the platform\'s two older IANA time-zone columns', () => {
expect(c.valueDomain).toBe('iana_time_zone');
});
- it('the DEFAULTS deliberately still differ — a default here is a consumer semantic', () => {
- // ⛔ Not a tidy-up target. `sys_report_schedule`'s reader documents and
- // implements a UTC default; `sys_job` has no reader at all, and minting one
- // would give "unset" a new meaning on rows that predate it.
- expect(scheduleColumn().defaultValue).toBe('UTC');
+ it('declares NO default — a default here is a consumer semantic', () => {
+ // ⛔ Not a tidy-up target. `sys_job` has no reader at all, and minting a
+ // default would give "unset" a new meaning on rows that predate it.
expect('defaultValue' in jobColumn()).toBe(false);
});
- it('the BOUNDS deliberately still differ — converging them is a DDL question, not a shape one', () => {
- // If someone converges these, they owe the reading #15872 could not take:
+ it('keeps its 100-character bound — narrowing it is a DDL question, not a shape one', () => {
+ // If someone narrows this, they owe the reading #15872 could not take:
// what the physical column holds. Red here is the prompt to go and take it.
expect(jobColumn().maxLength).toBe(100);
- expect(scheduleColumn().maxLength).toBe(64);
});
it('the declared domain refuses every non-member this card was filed over', () => {
// Asked of the predicate the write path calls (`isValueDomainMember`) under
- // the domain the columns actually declare — never a re-implementation.
+ // the domain the column actually declares — never a re-implementation.
const domain = jobColumn().valueDomain as 'iana_time_zone';
- expect(domain).toBe(scheduleColumn().valueDomain);
// The card's own three examples. `Mars/Olympus` is shape-valid and
// nonexistent, `UTC+8` and `China Standard Time` are the two spellings a
// human reaches for that the tzdb does not carry.
expect(isValueDomainMember(domain, 'Mars/Olympus')).toBe(false);
expect(isValueDomainMember(domain, 'UTC+8')).toBe(false);
expect(isValueDomainMember(domain, 'China Standard Time')).toBe(false);
- // …and still admits what both columns must keep taking, `UTC` included —
+ // …and still admits what the column must keep taking, `UTC` included —
// which `Intl.supportedValuesOf('timeZone')` omits, so a column judged
- // against the enumeration would refuse `sys_report_schedule`'s own default.
+ // against the enumeration would refuse the platform's own default zone.
expect(isValueDomainMember(domain, 'UTC')).toBe(true);
expect(isValueDomainMember(domain, 'Asia/Shanghai')).toBe(true);
- expect(isValueDomainMember(domain, scheduleColumn().defaultValue as string)).toBe(true);
});
- it('both bounds admit every zone the runtime enumerates, so neither refuses a legal value', () => {
- // The smaller bound is the one that could bite; assert against both so a
- // future ICU that enumerates a longer name reds here rather than silently
+ it('the bound admits every zone the runtime enumerates, so it refuses no legal value', () => {
+ // A future ICU that enumerates a longer name reds here rather than silently
// refusing a legal zone at the write seam.
// `Intl.supportedValuesOf` is ES2022; the package's `lib` predates it, so
// the call is typed here rather than the whole program's lib widened.
const intl = Intl as unknown as { supportedValuesOf(key: 'timeZone'): string[] };
const longest = Math.max(...intl.supportedValuesOf('timeZone').map((z) => z.length));
- expect(longest).toBeLessThanOrEqual(scheduleColumn().maxLength as number);
expect(longest).toBeLessThanOrEqual(jobColumn().maxLength as number);
});
});
diff --git a/packages/platform-objects/src/audit/sys-job.object.ts b/packages/platform-objects/src/audit/sys-job.object.ts
index f2db10eac5f..a7dcf605f9b 100644
--- a/packages/platform-objects/src/audit/sys-job.object.ts
+++ b/packages/platform-objects/src/audit/sys-job.object.ts
@@ -72,7 +72,8 @@ export const SysJob = ObjectSchema.create({
// added, because it decides what the declaration is worth: NOTHING does.
// `DbJobAdapter` writes it (`upsertJobRow`, `schedule.timezone ?? null`) and
// its three `sys_job` read sites take `id` / `run_count` / `failure_count`
- // only — the tree's one `row.timezone` read belongs to `sys_report_schedule`.
+ // only — the tree's one other `row.timezone` read belonged to
+ // `sys_report_schedule`, retired with the saved-report stack (#20102).
// The value the scheduler actually honours travels in memory
// (`toBoundaryJobSchedule` -> `CronJobAdapter.schedule` -> croner), and
// `DbJobAdapter.schedule` awaits that call BEFORE `upsertJobRow`, so a
@@ -83,8 +84,8 @@ export const SysJob = ObjectSchema.create({
// The door this declaration actually closes is the OTHER one: a direct write
// to the object (Studio, REST, a script), which had no validation whatever.
//
- // ⚠️ `maxLength` deliberately still says 100 while `sys_report_schedule`
- // says 64. Converging it is the card's third dimension and is NOT landed
+ // ⚠️ `maxLength` deliberately still says 100 where `sys_report_schedule`
+ // (retired since, #20102) said 64. Converging it is the card's third dimension and is NOT landed
// here: `maxLength` is not only a write bound, it reaches DDL — narrowing a
// physical `varchar(100)` produces `driver-sql`'s `narrow_varchar` op at
// severity `error`, category destructive ("narrowing may truncate",
diff --git a/packages/platform-objects/src/audit/sys-report-schedule.object.ts b/packages/platform-objects/src/audit/sys-report-schedule.object.ts
deleted file mode 100644
index cb7114fa843..00000000000
--- a/packages/platform-objects/src/audit/sys-report-schedule.object.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-import { ObjectSchema, Field } from '@objectstack/spec/data';
-
-/**
- * sys_report_schedule — Recurring Report Delivery
- *
- * Joins a `sys_saved_report` to an interval and a recipient list so
- * the reports plugin can deliver "daily pipeline digest" / "weekly
- * lead summary" without a separate workflow.
- *
- * Scheduling: `interval_minutes` (1440 = daily, 10080 = weekly) or a
- * `cron_expression`. When a `cron_expression` is set it wins over
- * `interval_minutes` and is evaluated in `timezone` (default UTC) via
- * croner — so "every weekday 09:00 local" is expressible. `next_run_at`
- * is computed from whichever applies.
- *
- * Delivery: when the master dispatch job ticks (every minute by
- * default), every schedule with `next_run_at <= now` is loaded,
- * its report is executed, the result is rendered into the report's
- * `format`, and the rendered body is emailed to each address in
- * `recipients`. `next_run_at` is then advanced by `interval_minutes`.
- *
- * Conventions:
- * - `recipients` is a comma-separated list of RFC-5322 addresses to
- * keep the schema driver-agnostic. The reports plugin splits and
- * trims before handing the list to IEmailService.
- * - `active=false` disables the schedule without losing its history.
- *
- * @namespace sys
- */
-export const SysReportSchedule = ObjectSchema.create({
- name: 'sys_report_schedule',
- label: 'Report Schedule',
- pluralLabel: 'Report Schedules',
- icon: 'clock',
- isSystem: true,
- managedBy: 'platform',
- description: 'Recurring delivery of a sys_saved_report via email',
- titleFormat: '{report_id} → {recipients}',
- highlightFields: ['report_id', 'recipients', 'interval_minutes', 'active', 'next_run_at'],
-
- fields: {
- id: Field.text({
- label: 'Schedule ID',
- required: true,
- readonly: true,
- group: 'System',
- }),
-
- report_id: Field.lookup('sys_saved_report', {
- label: 'Report',
- required: true,
- group: 'Schedule',
- }),
-
- name: Field.text({
- label: 'Name',
- required: false,
- maxLength: 200,
- description: 'Optional label for the digest — used in the email subject',
- group: 'Schedule',
- }),
-
- interval_minutes: Field.number({
- label: 'Interval (minutes)',
- required: false,
- defaultValue: 1440,
- description: 'How often to send (1440 = daily, 10080 = weekly)',
- group: 'Schedule',
- }),
-
- cron_expression: Field.text({
- label: 'Cron Expression',
- required: false,
- maxLength: 100,
- description: 'Optional 5/6-field cron — overrides interval_minutes when present',
- group: 'Schedule',
- }),
-
- // [#15872] Validated on write by `valueDomain: 'iana_time_zone'` — the same
- // declaration `sys_business_unit.timezone` / `sys_organization.timezone`
- // carry (#14238), and the same shared `Intl.DateTimeFormat` probe, never the
- // `Intl.supportedValuesOf('timeZone')` enumeration (which omits `UTC`, this
- // column's own default). Written values only (the `min`/`max`/`maxLength`
- // transition-gate class), so a stored non-member survives and no migration
- // is owed.
- //
- // WHY THIS COLUMN IS THE SHARP ONE, measured on #15872: unlike
- // `sys_job.timezone`, this value IS read back and handed to a scheduler.
- // `ReportService.rowFromSchedule` lifts it off the row and `nextRunAt` calls
- // `new Cron(cron, { timezone }).nextRun(from)`. croner (10.0.1) does not
- // reject a non-member zone when it is constructed WITHOUT a callback — it
- // throws from `nextRun()` — and `nextRunAt` CATCHES that throw and falls
- // back to `from + interval_minutes`. So before this line, a typo'd zone on a
- // cron schedule silently discarded the cron: an admin's "every weekday 09:00
- // Asia/Shanghai" became "every 1440 minutes, forever", logged only as
- // `invalid cron ''` — a warning that names the wrong input, since the
- // expression was fine. Neither a throw nor a fall back to UTC: the wrong
- // instant, permanently, which is the outcome this card was told to escalate
- // on. `scheduleReport`'s eager create-time guard did not catch it either;
- // it constructed a callback-less `Cron` and so was blind to exactly this
- // half of its own input. Refusing the write is what closes it HERE.
- //
- // [#16291] The reader's two halves are closed separately, and this line does
- // not stand in for either: `scheduleReport` now consults
- // `isValueDomainMember('iana_time_zone', …)` itself — this declaration's own
- // predicate, so neither door can accept what the other refuses — and the
- // sweep quarantines a row that was STORED before this line existed (it does
- // not run it and does not advance `next_run_at`, and says so in
- // `last_status` / `last_error`) rather than re-deriving a cadence from
- // `interval_minutes` that nobody asked for.
- //
- // `maxLength: 64` and `defaultValue: 'UTC'` are BOTH unchanged. The bound is
- // already the value #14238 justified (twice the domain's real ceiling: the
- // enumeration's longest name is 30 characters on this Node baseline, the
- // longest tzdb link 32). The default is a consumer semantic — this reader
- // documents "default UTC" and falls back to `'UTC'` in four places — and is
- // deliberately NOT converged with `sys_job`, which has none.
- timezone: Field.text({
- label: 'Timezone',
- required: false,
- maxLength: 64,
- defaultValue: 'UTC',
- valueDomain: 'iana_time_zone',
- group: 'Schedule',
- }),
-
- active: Field.boolean({
- label: 'Active',
- required: true,
- defaultValue: true,
- group: 'Schedule',
- }),
-
- recipients: Field.text({
- label: 'Recipients',
- required: true,
- maxLength: 4000,
- description: 'Comma-separated email addresses',
- group: 'Delivery',
- }),
-
- format: Field.select(
- ['csv', 'html_table'],
- {
- label: 'Format',
- required: false,
- defaultValue: 'html_table',
- description: 'Render format — csv is attached, html_table is inlined',
- group: 'Delivery',
- },
- ),
-
- subject_template: Field.text({
- label: 'Subject Template',
- required: false,
- maxLength: 200,
- description: 'Email subject; {{name}} / {{date}} / {{rows}} are substituted',
- group: 'Delivery',
- }),
-
- owner_id: Field.lookup('sys_user', {
- label: 'Owner',
- required: false,
- group: 'Provenance',
- }),
-
- next_run_at: Field.datetime({
- label: 'Next Run',
- required: false,
- description: 'Dispatcher loads schedules where next_run_at <= now',
- group: 'State',
- }),
-
- last_sent_at: Field.datetime({
- label: 'Last Sent',
- required: false,
- group: 'State',
- }),
-
- last_status: Field.select(
- ['ok', 'failed', 'skipped'],
- {
- label: 'Last Status',
- required: false,
- group: 'State',
- },
- ),
-
- last_error: Field.textarea({
- label: 'Last Error',
- required: false,
- group: 'State',
- }),
-
- created_at: Field.datetime({
- label: 'Created At',
- required: true,
- defaultValue: 'NOW()',
- readonly: true,
- group: 'System',
- }),
-
- updated_at: Field.datetime({
- label: 'Updated At',
- required: false,
- group: 'System',
- }),
- },
-
- indexes: [
- // Hot path for the dispatch loop.
- { fields: ['active', 'next_run_at'] },
- { fields: ['report_id'] },
- { fields: ['owner_id'] },
- ],
-});
diff --git a/packages/platform-objects/src/audit/sys-saved-report.object.ts b/packages/platform-objects/src/audit/sys-saved-report.object.ts
deleted file mode 100644
index 3962207ddf2..00000000000
--- a/packages/platform-objects/src/audit/sys-saved-report.object.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-import { ObjectSchema, Field } from '@objectstack/spec/data';
-
-/**
- * sys_saved_report — Reusable Report Definition
- *
- * A persisted query against a single object that can be re-executed
- * on demand or on a schedule. Acts as the bridge between ad-hoc
- * filtering in the UI and the structured report library administrators
- * curate.
- *
- * The query envelope (`query_json`) is the same shape ObjectQL accepts
- * — `{ filter, fields, orderBy, limit, groupBy }` — so a report can be
- * round-tripped between the list view and the report definition
- * without re-translation.
- *
- * Conventions:
- * - `object_name` is the short object the report queries.
- * - `format` controls how `IReportService.run()` renders the rows
- * (`csv` for downloads, `html_table` for email digests, `json`
- * for raw API consumption).
- *
- * @namespace sys
- */
-export const SysSavedReport = ObjectSchema.create({
- name: 'sys_saved_report',
- label: 'Saved Report',
- pluralLabel: 'Saved Reports',
- icon: 'bar-chart',
- isSystem: true,
- managedBy: 'platform',
- description: 'Persisted ObjectQL report definition — re-runnable and schedulable',
- displayNameField: 'name',
- nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
- titleFormat: '{name}',
- highlightFields: ['name', 'object_name', 'format', 'owner_id', 'updated_at'],
-
- fields: {
- id: Field.text({
- label: 'Report ID',
- required: true,
- readonly: true,
- group: 'System',
- }),
-
- name: Field.text({
- label: 'Name',
- required: true,
- maxLength: 200,
- searchable: true,
- group: 'Definition',
- }),
-
- description: Field.textarea({
- label: 'Description',
- required: false,
- group: 'Definition',
- }),
-
- object_name: Field.text({
- label: 'Object',
- required: true,
- maxLength: 100,
- description: 'Short object name the report queries',
- group: 'Definition',
- }),
-
- query_json: Field.textarea({
- label: 'Query',
- required: true,
- description: 'ObjectQL query envelope — { filter, fields, orderBy, limit, groupBy }',
- group: 'Definition',
- }),
-
- format: Field.select(
- ['csv', 'json', 'html_table'],
- {
- label: 'Format',
- required: true,
- defaultValue: 'csv',
- description: 'Rendering used by IReportService.run() and email digests',
- group: 'Definition',
- },
- ),
-
- owner_id: Field.lookup('sys_user', {
- label: 'Owner',
- required: false,
- description: 'User that owns the report definition (drives sharing)',
- group: 'Provenance',
- }),
-
- last_run_at: Field.datetime({
- label: 'Last Run',
- required: false,
- description: 'Stamped by IReportService.run() on successful execution',
- group: 'State',
- }),
-
- last_row_count: Field.number({
- label: 'Last Row Count',
- required: false,
- group: 'State',
- }),
-
- created_at: Field.datetime({
- label: 'Created At',
- required: true,
- defaultValue: 'NOW()',
- readonly: true,
- group: 'System',
- }),
-
- updated_at: Field.datetime({
- label: 'Updated At',
- required: false,
- group: 'System',
- }),
- },
-
- indexes: [
- { fields: ['object_name'] },
- { fields: ['owner_id'] },
- { fields: ['name'] },
- ],
-});
diff --git a/packages/platform-objects/src/identity/sys-business-unit.object.ts b/packages/platform-objects/src/identity/sys-business-unit.object.ts
index 19d606670fb..82b3ff9066e 100644
--- a/packages/platform-objects/src/identity/sys-business-unit.object.ts
+++ b/packages/platform-objects/src/identity/sys-business-unit.object.ts
@@ -162,9 +162,9 @@ export const SysBusinessUnit = ObjectSchema.create({
// the ruling's own precondition, 「rather than shipping an unvalidated text
// column」): membership is the shared `Intl.DateTimeFormat` probe, never
// the `Intl.supportedValuesOf('timeZone')` enumeration, which omits `UTC`
- // — the very fallback this contract names. `maxLength: 64` follows
- // `sys_report_schedule.timezone`, the platform's other IANA column that
- // pairs a bound with the `UTC` default; the enumeration's longest name on
+ // — the very fallback this contract names. `maxLength: 64` followed
+ // `sys_report_schedule.timezone` (retired since, #20102), then the
+ // platform's other IANA column pairing a bound with the `UTC` default; the enumeration's longest name on
// the repo's Node baseline is 30 characters and the tzdb caps each path
// component at 14, so 64 is twice the domain's real ceiling and the smaller
// of the two precedents (`sys_job.timezone` still says 100 — #15872 gave
diff --git a/packages/plugins/plugin-reports/CHANGELOG.md b/packages/plugins/plugin-reports/CHANGELOG.md
deleted file mode 100644
index ba4c503ddc5..00000000000
--- a/packages/plugins/plugin-reports/CHANGELOG.md
+++ /dev/null
@@ -1,4368 +0,0 @@
-# @objectstack/plugin-reports
-
-## 17.4.0
-
-### Minor Changes
-
-- afa3a26: fix(plugin-reports)!: a non-member schedule `timezone` no longer discards the cron expression, and a schedule already holding one stops instead of firing on a cadence nobody asked for (#16291)
-
- **BREAKING** for a deployment that already stores a report schedule with a cron expression and a `timezone` that is not an IANA member. Such a schedule is delivering today, on the wrong cadence; after this change it does not deliver at all until a human corrects the zone. It ships as `minor` under the lockstep launch-window convention (`scripts/check-changeset-no-major.mjs` refuses `major`); the version number is not the signal here, this entry is.
-
-
-
- ## What an upgrading operator has to do, and how to find out
-
- If `sys_report_schedule` holds a row whose `timezone` is not a real IANA zone **and** whose `cron_expression` is set, the sweep now marks it `last_status: 'failed'` with a `last_error` naming the zone, and stops running it. Correct the `timezone` on that row; the schedule resumes on the next sweep with no re-enable and no second action, because `active` and the past `next_run_at` are deliberately left alone.
-
- Only rows written **before** `valueDomain: 'iana_time_zone'` landed on that column can be in this state, and the set cannot grow: measured on a real kernel with a real SQLite driver, `insert` into `sys_report_schedule` with `timezone: 'Mars/Olympus'` is already refused today — `VALIDATION_FAILED · Timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "Mars/Olympus")`. A set that cannot grow is still not an empty one, which is why this carries a banner rather than a shrug.
-
- ## The defect
-
- croner (10.0.1) answers a non-member zone in three different ways, and only the middle one was ever reached here: `new Cron(expr, { timezone })` **without a callback** validates the expression and lets any zone through, `nextRun()` on that instance then throws a `CronDate` conversion `TypeError`, and the callback form throws at construction. `scheduleReport`'s eager guard used the callback-less form, so the timezone half of its own input passed straight under a guard whose stated purpose was "a clear error at schedule time instead of a schedule that silently falls back to interval on sweep" — and `nextRunAt` caught that deferred throw and returned `from + interval_minutes`. A schedule authored as "every weekday 09:00 Asia/Shanghai" became "every 1440 minutes, forever", re-derived on every sweep, logged only as a complaint about a cron expression that was perfectly good.
-
- ## What changed
-
- - **The create-time guard now asks the right question.** `scheduleReport` consults `isValueDomainMember('iana_time_zone', …)` from `@objectstack/spec/shared` — the same predicate `sys_report_schedule.timezone`'s `valueDomain` declaration enforces on write — and refuses a non-member with `VALIDATION_FAILED: invalid timezone '': not a member of the 'iana_time_zone' value domain`. One answer at both doors, so this one cannot accept what the storage door refuses; it says so earlier and names the input that is actually wrong. It applies whether or not a `cron_expression` is set, because the storage gate does too. **This is not what makes the change breaking:** the storage door already refuses the same value today, so no reachable accept set narrows — what moves is which door answers and how clearly.
- - **The row now stores the string the scheduler evaluates.** An empty `timezone` was stored verbatim while every `new Cron` call site read it as `UTC`; it is normalised to `UTC` on the way in.
- - **A schedule already holding an unusable zone is stopped, not rescheduled.** It is not run and its `next_run_at` is not advanced; `last_status` / `last_error` carry the reason. Repairing the value automatically was rejected: the intended zone is not recoverable from a typo, and rewriting it to `UTC` would deliver at yet another set of wrong instants while the row looked healthy. Interval-only schedules are untouched — interval arithmetic never consults the zone, so a legacy bad value there still delivers on the cadence its author asked for.
- - **Both fall-back warnings name both inputs.** The "no next occurrence" and the former "invalid cron" lines each mentioned only the expression, so either of them on a timezone fault sent an investigator to audit the half that was fine. They now carry the expression *and* the zone, and the second no longer asserts the expression is the broken one.
-
-### Patch Changes
-
-- be92d46: These fourteen packages now declare `repository.directory`, so their npm pages carry a working "source" deep link to their own directory in the monorepo.
-
- npm renders that field by concatenating it onto `repository.url`. None of these fourteen manifests carried a `repository` block at all, so every one of their npm pages offered no route from the package back to its code — not a broken link, no link. That is what this publishes: the block those pages read, naming each package's own directory.
-
- Nothing else about these packages changes. No export, no runtime behaviour, no dependency and no file in the tarball other than the manifest's own `repository` key. The version bump exists because the fix is only real once it is published: the field lives in the manifest npm serves, so a corrected manifest sitting in the repository leaves the package page exactly as wrong as it was.
-
- The rule behind it is now mechanical rather than remembered — `check:manifest-repository-directory` makes a publishable (non-private) workspace manifest declare the field naming its own directory, so a package added or moved after this cannot quietly go back to having no source link.
-- Updated dependencies [fe0d9a4]
-- Updated dependencies [ecd2158]
-- Updated dependencies [f2b5e46]
-- Updated dependencies [2ed6be6]
-- Updated dependencies [ed7243d]
-- Updated dependencies [6ba0db4]
-- Updated dependencies [625b0c3]
-- Updated dependencies [233222e]
-- Updated dependencies [07f40e5]
-- Updated dependencies [ceb4877]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [90e7e6d]
-- Updated dependencies [2bdabe6]
-- Updated dependencies [ca326b5]
-- Updated dependencies [8f404a5]
-- Updated dependencies [159dbad]
-- Updated dependencies [68437d4]
-- Updated dependencies [abb140c]
-- Updated dependencies [8333a6c]
-- Updated dependencies [3e3ecb0]
-- Updated dependencies [3030369]
-- Updated dependencies [d5d8d50]
-- Updated dependencies [e08892d]
-- Updated dependencies [ae05f2e]
-- Updated dependencies [b548e43]
-- Updated dependencies [c463d03]
-- Updated dependencies [64bd6a3]
-- Updated dependencies [13c48c2]
-- Updated dependencies [b0529e1]
-- Updated dependencies [66dc6ab]
-- Updated dependencies [6f94458]
-- Updated dependencies [6e67b86]
-- Updated dependencies [132742f]
-- Updated dependencies [85a2459]
-- Updated dependencies [50dc214]
-- Updated dependencies [e89fa92]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [8976ea1]
-- Updated dependencies [56fe8c2]
-- Updated dependencies [acabd24]
-- Updated dependencies [ab50c8f]
-- Updated dependencies [6491463]
-- Updated dependencies [89cf4d6]
-- Updated dependencies [21c5dcb]
-- Updated dependencies [6d4d5d3]
-- Updated dependencies [ed5d557]
-- Updated dependencies [bca21f7]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [2025b1f]
-- Updated dependencies [1a7a7c9]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [cbca47d]
-- Updated dependencies [acf4d38]
-- Updated dependencies [ef3a138]
-- Updated dependencies [68d5dfd]
-- Updated dependencies [3e21cf0]
-- Updated dependencies [4cfc93b]
-- Updated dependencies [efd6b43]
-- Updated dependencies [859ded3]
-- Updated dependencies [fa125f3]
-- Updated dependencies [74628d9]
-- Updated dependencies [a646120]
-- Updated dependencies [6f1ce7d]
-- Updated dependencies [7778115]
-- Updated dependencies [2c753fe]
-- Updated dependencies [52804cd]
-- Updated dependencies [3f89967]
-- Updated dependencies [53cf263]
-- Updated dependencies [21aabbc]
-- Updated dependencies [9c270bb]
-- Updated dependencies [76c8c5a]
-- Updated dependencies [a84e1ce]
-- Updated dependencies [bf1054a]
-- Updated dependencies [d8d2776]
-- Updated dependencies [222dc0f]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [32c917d]
-- Updated dependencies [f9a3c32]
-- Updated dependencies [f502898]
-- Updated dependencies [51ae731]
-- Updated dependencies [af7edfe]
-- Updated dependencies [b60f48b]
-- Updated dependencies [c78c918]
-- Updated dependencies [4ca358d]
-- Updated dependencies [cf9bda4]
-- Updated dependencies [784cb92]
-- Updated dependencies [7629f4d]
-- Updated dependencies [51df9fd]
-- Updated dependencies [a7da4de]
-- Updated dependencies [de0bcdd]
-- Updated dependencies [70f7d6d]
-- Updated dependencies [c677cda]
-- Updated dependencies [6acb37e]
-- Updated dependencies [7797102]
-- Updated dependencies [554a160]
-- Updated dependencies [f7da71e]
-- Updated dependencies [7f745c3]
-- Updated dependencies [0a038cc]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [97adce2]
-- Updated dependencies [a83482c]
-- Updated dependencies [5eb24f8]
-- Updated dependencies [2a3decc]
-- Updated dependencies [cc00df2]
-- Updated dependencies [cc00df2]
-- Updated dependencies [f4e6adf]
-- Updated dependencies [ee4a59b]
-- Updated dependencies [4db3c61]
-- Updated dependencies [5ca314a]
-- Updated dependencies [e0af1a8]
-- Updated dependencies [4771bd9]
-- Updated dependencies [414c1fc]
-- Updated dependencies [22c0279]
-- Updated dependencies [c930f85]
-- Updated dependencies [0db2947]
-- Updated dependencies [92b5d7f]
-- Updated dependencies [613bfbd]
-- Updated dependencies [abae16a]
-- Updated dependencies [094b8fd]
-- Updated dependencies [c7aca0d]
-- Updated dependencies [c1d8f98]
-- Updated dependencies [8e0b297]
-- Updated dependencies [d4f9b2a]
-- Updated dependencies [5f7fa1d]
-- Updated dependencies [87f0ccc]
-- Updated dependencies [aedbaef]
-- Updated dependencies [a727043]
-- Updated dependencies [c5d6803]
-- Updated dependencies [10d05bb]
-- Updated dependencies [69602e5]
-- Updated dependencies [c3ce76c]
-- Updated dependencies [7936b29]
-- Updated dependencies [46803fa]
-- Updated dependencies [c2a336c]
-- Updated dependencies [9f890d3]
-- Updated dependencies [0bb2318]
-- Updated dependencies [f7db8f4]
-- Updated dependencies [1ecee3e]
-- Updated dependencies [9408b7f]
-- Updated dependencies [2bb0614]
-- Updated dependencies [b3820c3]
-- Updated dependencies [e9fcd6b]
-- Updated dependencies [9bcd9be]
-- Updated dependencies [b398ad2]
-- Updated dependencies [99261a7]
-- Updated dependencies [81b426f]
-- Updated dependencies [001af1c]
-- Updated dependencies [fb77aa5]
-- Updated dependencies [581d8f8]
-- Updated dependencies [f81afe3]
-- Updated dependencies [40a44b9]
-- Updated dependencies [f89812e]
-- Updated dependencies [7a7fb03]
-- Updated dependencies [8fd246d]
-- Updated dependencies [021a735]
-- Updated dependencies [7bdb163]
- - @objectstack/spec@17.4.0
- - @objectstack/core@17.4.0
- - @objectstack/platform-objects@17.4.0
-
-## 17.3.0
-
-### Patch Changes
-
-- ad54eb3: feat(tooling): onboard all 14 `packages/plugins/**` packages into `check:test-typecheck` (#14062)
-
- Every plugin package now has a `tsconfig.test.json` compiled by the shared
- `check:test-typecheck` gate, and its `typecheck` script names it. Before this,
- the shrink-only `test-typecheck-debt.json` ratchet said **nothing** about a
- third of the repo's runtime surface: 14 packages, 1 `tsconfig.test.json`
- (`plugin-security`, wired directly to `tsc` rather than to the instrument), and
- 0 `check:test-typecheck` scripts.
-
- Onboarded as a family by the director ruling of 2026-09-01 on #14062
- (maintainer verbatim: 「同意」), which also carries the #5286 maintainer
- authority the starting ledgers need. The smaller branch triage recommended —
- declare the instrument's scope and re-site the two compile-time pins — was
- recorded as considered and not taken: an instrument silent over a third of the
- runtime surface is a hole readers generalise across, and that costs more than
- fourteen tsconfigs.
-
- **Measured, not assumed** (at `e80889095`, workspace closure built first). Four
- packages carry residue and therefore a starting ledger — plugin-approvals 324
- over 8 files, plugin-auth 94 over 10, plugin-sharing 3 over 2,
- knowledge-ragflow 3 over 1. The other ten measure **zero** and deliberately get
- no ledger file at all: the gate reads a missing ledger as `{ entries: {} }`, so
- any error there is red immediately with no entry to be added to — strictly
- stronger than a ledger holding nothing, and the call `plugin-security` had
- already recorded for itself.
-
- ⛔ **This does not repair 345 type errors.** Per ruling item 3 it makes the
- ratchet able to *see* them; paydown follows the ratchet's own shrink-only
- discipline on its own cards. No test file is edited here.
-
- Two corrections to the finding's own prose, both measured: the exclusion is
- narrower than "no plugin package compiles its tests" — 9 of the 14 already
- compiled their tests inside the `typecheck`-invoked build config, at zero
- errors — and `exec-context-annotation.pin.ts` is a `.pin.ts`, which
- `**/*.test.ts` never excluded, so its directives were already live. The pin
- this change genuinely makes real is
- `plugin-approvals/src/manager-org-screen-parity.contract.test.ts`, which no tsc
- program had ever read.
-- a1c804b: Report CSV and HTML exports now infer their columns from **every** row of the result set, not from the first 50. Column inference sampled `rows.slice(0, 50)` while the projection it produced was applied to all rows, so when a report declared no explicit `query.fields`, any key whose first occurrence fell at row 51 or later was absent from the header *and* dropped from every row that carried it. The export gave no signal: the CSV was well-formed, every row had the same arity, and nothing marked a column as inferred rather than declared, so a recipient of a scheduled report attachment could not tell. Sparse columns are the normal shape of report output — an optional field, a formula only some records satisfy, a lookup that resolves for a subset — and the sampled prefix is the query's first page in its own `orderBy`, so for a report sorted by status or created date the sample correlated with exactly the column it dropped. Both affected renderers are fixed (`csv`, which is also the `default:` format branch, and `html_table`); `json` was never affected. Already-inferred columns keep their position and late-appearing ones are appended, so an export that was correct before is byte-identical now.
-- Updated dependencies [809d417]
-- Updated dependencies [387e231]
-- Updated dependencies [f794e4e]
-- Updated dependencies [cae2169]
-- Updated dependencies [b812a54]
-- Updated dependencies [2d4fa75]
-- Updated dependencies [0e4e51b]
-- Updated dependencies [e84bbf6]
-- Updated dependencies [effae80]
-- Updated dependencies [efb3513]
-- Updated dependencies [d62f990]
-- Updated dependencies [c45d8e6]
-- Updated dependencies [2e3e8c7]
-- Updated dependencies [e621291]
-- Updated dependencies [655b106]
-- Updated dependencies [40a93b5]
-- Updated dependencies [d5b330d]
-- Updated dependencies [dda969c]
-- Updated dependencies [1f45690]
-- Updated dependencies [277948f]
-- Updated dependencies [8bdd955]
-- Updated dependencies [f3bbbef]
-- Updated dependencies [4f24e9d]
-- Updated dependencies [e27583e]
-- Updated dependencies [4bd6faa]
-- Updated dependencies [86cbe37]
-- Updated dependencies [6a180e4]
-- Updated dependencies [474242f]
-- Updated dependencies [63cd487]
-- Updated dependencies [bd4aa4e]
-- Updated dependencies [803eaab]
-- Updated dependencies [f8e8f03]
-- Updated dependencies [983edf1]
-- Updated dependencies [eae824e]
-- Updated dependencies [f6fa22c]
-- Updated dependencies [8a483b3]
-- Updated dependencies [3bc2e38]
-- Updated dependencies [97bcd99]
-- Updated dependencies [df59de0]
-- Updated dependencies [96e25a8]
-- Updated dependencies [f75a38a]
-- Updated dependencies [7a25e7d]
-- Updated dependencies [1fa05a6]
-- Updated dependencies [c85a265]
-- Updated dependencies [dcb10a5]
-- Updated dependencies [773a999]
-- Updated dependencies [35dffea]
-- Updated dependencies [d8024f0]
-- Updated dependencies [8120808]
-- Updated dependencies [776a098]
-- Updated dependencies [5060877]
-- Updated dependencies [4f6325d]
-- Updated dependencies [52954c0]
-- Updated dependencies [2aa8456]
-- Updated dependencies [93809a3]
-- Updated dependencies [7c0d0c3]
-- Updated dependencies [daae7aa]
-- Updated dependencies [8dc22d6]
-- Updated dependencies [a392dbf]
-- Updated dependencies [279431e]
-- Updated dependencies [948dd6b]
-- Updated dependencies [3b4c56c]
-- Updated dependencies [ae8edd2]
-- Updated dependencies [e25403c]
-- Updated dependencies [64baa68]
-- Updated dependencies [9fa70d7]
-- Updated dependencies [09db64a]
-- Updated dependencies [92916e7]
-- Updated dependencies [a84f3ea]
-- Updated dependencies [f2eaae8]
-- Updated dependencies [c09451b]
-- Updated dependencies [ba64877]
-- Updated dependencies [7345308]
-- Updated dependencies [79b6a22]
-- Updated dependencies [30d96ab]
-- Updated dependencies [f658793]
-- Updated dependencies [c95ad19]
-- Updated dependencies [e58ea8b]
-- Updated dependencies [4a17645]
-- Updated dependencies [3795c5f]
-- Updated dependencies [8ab926b]
-- Updated dependencies [7317cf2]
-- Updated dependencies [e25e839]
-- Updated dependencies [5997207]
-- Updated dependencies [8b13cc8]
-- Updated dependencies [4a4a35d]
-- Updated dependencies [86e765a]
-- Updated dependencies [1d7e76a]
-- Updated dependencies [53dc739]
-- Updated dependencies [fd289be]
-- Updated dependencies [03bf7b1]
-- Updated dependencies [f90e820]
-- Updated dependencies [18d816a]
-- Updated dependencies [e8bd715]
-- Updated dependencies [b91c351]
-- Updated dependencies [a28a3c0]
-- Updated dependencies [daeaaf9]
-- Updated dependencies [c459da6]
-- Updated dependencies [e914733]
-- Updated dependencies [f887e52]
-- Updated dependencies [881f8d8]
-- Updated dependencies [3bfa1e6]
-- Updated dependencies [0a8ebf3]
-- Updated dependencies [901355c]
-- Updated dependencies [34ce8e7]
-- Updated dependencies [33681ea]
-- Updated dependencies [4635f3e]
-- Updated dependencies [fd289be]
-- Updated dependencies [ee3595c]
-- Updated dependencies [09b4f4e]
-- Updated dependencies [b2eab95]
-- Updated dependencies [93940d4]
-- Updated dependencies [3a04b01]
-- Updated dependencies [45b9051]
-- Updated dependencies [3954fb7]
-- Updated dependencies [4805b56]
-- Updated dependencies [b9e9227]
-- Updated dependencies [d395692]
-- Updated dependencies [5894d30]
-- Updated dependencies [a3765f6]
-- Updated dependencies [2d5cee3]
-- Updated dependencies [e22158f]
-- Updated dependencies [7404925]
-- Updated dependencies [0c2334f]
-- Updated dependencies [778c59f]
-- Updated dependencies [d2619fd]
-- Updated dependencies [af56546]
-- Updated dependencies [6acb11a]
-- Updated dependencies [33c5fd3]
-- Updated dependencies [20b0fdb]
-- Updated dependencies [905019b]
-- Updated dependencies [a286411]
-- Updated dependencies [98c0d33]
-- Updated dependencies [368a82e]
-- Updated dependencies [a3d5724]
-- Updated dependencies [93ea19b]
-- Updated dependencies [9ee2dcf]
-- Updated dependencies [8cb96ec]
-- Updated dependencies [8f10a79]
-- Updated dependencies [6269a55]
-- Updated dependencies [a17da05]
-- Updated dependencies [a8c00e2]
-- Updated dependencies [0fb8760]
-- Updated dependencies [e5ce2ed]
-- Updated dependencies [be21955]
-- Updated dependencies [bc56e18]
-- Updated dependencies [be21955]
-- Updated dependencies [a9ee989]
-- Updated dependencies [4d0d944]
-- Updated dependencies [15d58db]
-- Updated dependencies [d63b014]
-- Updated dependencies [9abe4e4]
-- Updated dependencies [2cc7122]
-- Updated dependencies [50d6c92]
-- Updated dependencies [9e0ba21]
-- Updated dependencies [311433f]
-- Updated dependencies [3e5ad08]
-- Updated dependencies [9abe4e4]
-- Updated dependencies [b7131f3]
-- Updated dependencies [e5812fa]
-- Updated dependencies [7085f90]
-- Updated dependencies [dee4dd4]
-- Updated dependencies [ce7e497]
-- Updated dependencies [51ecb2f]
-- Updated dependencies [9086761]
-- Updated dependencies [f6344e7]
-- Updated dependencies [42a117b]
-- Updated dependencies [1401ae7]
-- Updated dependencies [4297fe7]
-- Updated dependencies [e398863]
-- Updated dependencies [d16df74]
-- Updated dependencies [d79c602]
-- Updated dependencies [f11fc61]
-- Updated dependencies [e808890]
-- Updated dependencies [8f79379]
-- Updated dependencies [e6ca40e]
-- Updated dependencies [0c77ea4]
-- Updated dependencies [52954c0]
-- Updated dependencies [89eb997]
-- Updated dependencies [7131f12]
-- Updated dependencies [aa5994e]
-- Updated dependencies [be93457]
-- Updated dependencies [a65db76]
-- Updated dependencies [15eb2c9]
-- Updated dependencies [5691b07]
-- Updated dependencies [2a6122b]
-- Updated dependencies [225e769]
-- Updated dependencies [8af88dd]
-- Updated dependencies [fb5fbb8]
-- Updated dependencies [d7b3963]
-- Updated dependencies [33184fd]
-- Updated dependencies [7c41693]
-- Updated dependencies [b72db01]
-- Updated dependencies [dce5cd4]
-- Updated dependencies [9688f58]
-- Updated dependencies [556ebc1]
-- Updated dependencies [177ebdc]
-- Updated dependencies [8d237b4]
-- Updated dependencies [2d2e6f0]
-- Updated dependencies [2d8dd8d]
-- Updated dependencies [22d573e]
-- Updated dependencies [b5a2398]
-- Updated dependencies [348860c]
-- Updated dependencies [5383fa6]
-- Updated dependencies [5b3ff63]
-- Updated dependencies [1a6a19c]
-- Updated dependencies [064d484]
-- Updated dependencies [527e050]
-- Updated dependencies [dd33bf9]
-- Updated dependencies [4cb2a90]
-- Updated dependencies [74a7804]
-- Updated dependencies [53d3689]
-- Updated dependencies [b3a63d3]
-- Updated dependencies [49f0dcf]
-- Updated dependencies [033a34c]
-- Updated dependencies [4d25d22]
-- Updated dependencies [1ffee51]
-- Updated dependencies [5ae4303]
-- Updated dependencies [ece4dad]
-- Updated dependencies [e9b377e]
-- Updated dependencies [146f448]
-- Updated dependencies [735f5c7]
-- Updated dependencies [a7e18de]
-- Updated dependencies [366f895]
-- Updated dependencies [dc75ba8]
-- Updated dependencies [cce0aa9]
-- Updated dependencies [e764507]
-- Updated dependencies [cff17af]
-- Updated dependencies [39404f3]
-- Updated dependencies [ca1965f]
-- Updated dependencies [8619f95]
-- Updated dependencies [b706af9]
-- Updated dependencies [add4360]
-- Updated dependencies [e0abc38]
-- Updated dependencies [fc9ba76]
-- Updated dependencies [0f94cc7]
-- Updated dependencies [a11c1a5]
-- Updated dependencies [71f9cd1]
-- Updated dependencies [ee17d86]
-- Updated dependencies [cdbd920]
-- Updated dependencies [18c432e]
-- Updated dependencies [3c418c4]
-- Updated dependencies [fa8715a]
-- Updated dependencies [a933ed7]
-- Updated dependencies [b3ca463]
-- Updated dependencies [a933ed7]
-- Updated dependencies [0d4a6a8]
-- Updated dependencies [518d5e5]
-- Updated dependencies [6643ba1]
-- Updated dependencies [eeba2ef]
-- Updated dependencies [ec4c4d2]
-- Updated dependencies [424f73c]
-- Updated dependencies [cccbe51]
-- Updated dependencies [a8d6b1d]
-- Updated dependencies [e4a7695]
-- Updated dependencies [87075b1]
-- Updated dependencies [fc58a99]
-- Updated dependencies [14cfc00]
-- Updated dependencies [1c6f7b4]
-- Updated dependencies [e854a53]
-- Updated dependencies [dfebfc8]
-- Updated dependencies [598b7ec]
-- Updated dependencies [d028b37]
-- Updated dependencies [f7b25c5]
-- Updated dependencies [122ef38]
-- Updated dependencies [4a37870]
-- Updated dependencies [428f9b2]
-- Updated dependencies [aa7ff56]
-- Updated dependencies [811a3c2]
-- Updated dependencies [1401ae7]
-- Updated dependencies [2fd3f1c]
-- Updated dependencies [c41b42e]
-- Updated dependencies [c4db311]
-- Updated dependencies [750fff5]
-- Updated dependencies [c19035e]
-- Updated dependencies [ececf7a]
-- Updated dependencies [d173125]
-- Updated dependencies [8eeca27]
-- Updated dependencies [8425c17]
-- Updated dependencies [a5ef1d8]
-- Updated dependencies [772d5de]
-- Updated dependencies [ce80ec2]
-- Updated dependencies [b372318]
-- Updated dependencies [97a2263]
-- Updated dependencies [29d0676]
-- Updated dependencies [0169d49]
-- Updated dependencies [6bd3231]
-- Updated dependencies [d2b5ba8]
-- Updated dependencies [b799ac5]
-- Updated dependencies [8f74307]
-- Updated dependencies [d23dc08]
-- Updated dependencies [644ad50]
-- Updated dependencies [0da7cd2]
-- Updated dependencies [28a5c3e]
-- Updated dependencies [4bc18e5]
-- Updated dependencies [9f57f1e]
- - @objectstack/spec@17.3.0
- - @objectstack/platform-objects@17.3.0
- - @objectstack/core@17.3.0
-
-## 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 [3b2af5e]
-- Updated dependencies [7d2d112]
-- Updated dependencies [5fa0d72]
-- Updated dependencies [02b3b07]
-- Updated dependencies [914c413]
-- Updated dependencies [55809a0]
-- Updated dependencies [ee2ff45]
-- Updated dependencies [47cd3ec]
-- Updated dependencies [52db1d1]
-- Updated dependencies [5649efb]
-- Updated dependencies [9d7d2de]
-- Updated dependencies [c815c50]
-- Updated dependencies [795ea05]
-- Updated dependencies [2306a76]
-- Updated dependencies [e5ea701]
-- Updated dependencies [a40dcc1]
-- Updated dependencies [def0d3e]
-- Updated dependencies [8d0bb79]
-- Updated dependencies [5acb58d]
-- Updated dependencies [2e3cf95]
-- Updated dependencies [4c93387]
-- Updated dependencies [504c8d5]
-- Updated dependencies [a037f7c]
-- Updated dependencies [3ee8ddf]
-- Updated dependencies [16cef97]
-- Updated dependencies [a79bd35]
-- Updated dependencies [6ceaa4b]
-- Updated dependencies [15ea214]
-- Updated dependencies [de19489]
-- Updated dependencies [c684d00]
-- Updated dependencies [923c424]
-- Updated dependencies [0ab81d1]
-- Updated dependencies [1ec36b7]
-- Updated dependencies [5f2e54c]
-- Updated dependencies [189373b]
-- Updated dependencies [35ad101]
-- Updated dependencies [ceb33a9]
-- Updated dependencies [dccbcec]
-- Updated dependencies [73d9795]
-- Updated dependencies [8012960]
-- Updated dependencies [266654d]
-- Updated dependencies [f34f56b]
-- Updated dependencies [f399618]
-- Updated dependencies [75e9301]
-- Updated dependencies [2810695]
- - @objectstack/platform-objects@17.2.0
- - @objectstack/spec@17.2.0
- - @objectstack/core@17.2.0
-
-## 17.1.0
-
-### Patch Changes
-
-- Updated dependencies [56656aa]
-- Updated dependencies [c9f5950]
-- Updated dependencies [d6e80b2]
-- Updated dependencies [07e630e]
-- Updated dependencies [66beee0]
-- Updated dependencies [2f65b1b]
-- Updated dependencies [720ee95]
-- Updated dependencies [f287435]
-- Updated dependencies [2782805]
-- Updated dependencies [e43d63a]
-- Updated dependencies [9aa8890]
-- Updated dependencies [7c9c1dd]
-- Updated dependencies [03520eb]
-- Updated dependencies [75b7c24]
-- Updated dependencies [d5552ca]
-- Updated dependencies [d9813a9]
-- Updated dependencies [8640fb2]
-- Updated dependencies [2420641]
-- Updated dependencies [2ad91c3]
-- Updated dependencies [f57fb38]
-- Updated dependencies [00777a0]
-- Updated dependencies [d491625]
-- Updated dependencies [420804d]
-- Updated dependencies [716ac9b]
-- Updated dependencies [a38408a]
-- Updated dependencies [62b1427]
-- Updated dependencies [7ea1372]
-- Updated dependencies [23abe27]
-- Updated dependencies [985a9cd]
-- Updated dependencies [5f5e234]
-- Updated dependencies [a8189ae]
-- Updated dependencies [26e70fb]
-- Updated dependencies [42b05af]
-- Updated dependencies [2b292ce]
-- Updated dependencies [abcf853]
-- Updated dependencies [8b9eba5]
-- Updated dependencies [d575779]
-- Updated dependencies [94f7ef8]
-- Updated dependencies [c5ac5e4]
-- Updated dependencies [a777944]
-- Updated dependencies [dd88e1c]
-- Updated dependencies [856527c]
-- Updated dependencies [870f710]
-- Updated dependencies [79c46da]
-- Updated dependencies [7ff3975]
-- Updated dependencies [29d055b]
-- Updated dependencies [65589d6]
-- Updated dependencies [2c86fe3]
-- Updated dependencies [e196c6a]
-- Updated dependencies [24173e9]
-- Updated dependencies [4ab7523]
-- Updated dependencies [19539b4]
-- Updated dependencies [f8eb736]
-- Updated dependencies [11b779e]
-- Updated dependencies [739fe5b]
-- Updated dependencies [4bfe1a5]
-- Updated dependencies [2065e31]
-- Updated dependencies [b69d0f5]
-- Updated dependencies [4d47afe]
-- Updated dependencies [e4e5c6e]
-- Updated dependencies [9a56784]
-- Updated dependencies [d00d2f6]
-- Updated dependencies [df0c12d]
-- Updated dependencies [d31785f]
-- Updated dependencies [c308a4f]
-- Updated dependencies [e2899f6]
-- Updated dependencies [3851f87]
-- Updated dependencies [2a29caa]
-- Updated dependencies [09a6eee]
-- Updated dependencies [1a7f907]
-- Updated dependencies [cd455c8]
-- Updated dependencies [e1bb0ca]
-- Updated dependencies [30d3752]
-- Updated dependencies [c80e7ae]
-- Updated dependencies [09a9a8a]
-- Updated dependencies [07026cf]
-- Updated dependencies [5d4f3d5]
-- Updated dependencies [4d80e8b]
-- Updated dependencies [30b1c63]
-- Updated dependencies [079b457]
-- Updated dependencies [e43b211]
-- Updated dependencies [890b38f]
-- Updated dependencies [8bee54b]
-- Updated dependencies [04f8fdb]
-- Updated dependencies [7a537ce]
-- Updated dependencies [593c4bf]
-- Updated dependencies [6158146]
-- Updated dependencies [84cb121]
-- Updated dependencies [ca19ee8]
-- Updated dependencies [a675b4d]
-- Updated dependencies [b887013]
-- Updated dependencies [ff08691]
-- Updated dependencies [60e0f90]
-- Updated dependencies [90c5285]
-- Updated dependencies [402c125]
-- Updated dependencies [7901b2d]
-- Updated dependencies [56bca91]
-- Updated dependencies [b3f9831]
-- Updated dependencies [79394d7]
-- Updated dependencies [730fd9a]
-- Updated dependencies [44bc51d]
-- Updated dependencies [73cfddf]
-- Updated dependencies [d634e66]
- - @objectstack/spec@17.1.0
- - @objectstack/platform-objects@17.1.0
- - @objectstack/core@17.1.0
-
-## 17.0.0
-
-### Major Changes
-
-- 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710)
-
- **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a
- record and taking a bulk machine-readable copy of the whole table are different
- privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite
- "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so.
-
- ### Migration — FROM → TO
-
- | | before | after |
- | -------------------- | ----------------------------------- | -------------------------- |
- | `allowExport` unset | export **allowed** (inherited read) | export **denied** |
- | `allowExport: false` | export denied | export denied (unchanged) |
- | `allowExport: true` | export allowed | export allowed (unchanged) |
-
- **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'`
- wildcard) of every permission set whose holders should keep exporting.
-
- ```ts
- objects: {
- deal: { allowRead: true, allowExport: true }, // ← add the grant
- }
- ```
-
- Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set
- that never exported is unaffected.
-
- **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the
- built-ins are handled for you — `admin_full_access` and `organization_admin` now
- carry `allowExport: true` explicitly. **Environment-authored sets are not**: any
- custom set whose users export must be edited. `member_default` deliberately does
- NOT carry the grant, so ordinary authenticated users lose export until an admin
- grants it — that is the point of the flip, not an oversight.
-
- **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set
- granting `true` grants export. `false` and unset are the same outcome; `false`
- is authoring intent, not a veto, because permission sets are additive capability
- containers (ADR-0090).
-
- **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no
- longer confer export. Separating "may see all data" from "may take a bulk copy"
- is the segregation-of-duties case the axis exists for.
-
- ### Also in this change
-
- - **spec** — a set carrying `allowExport` is now **high-privilege**
- (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` /
- `guest` audience anchors. Without this the opt-in was defeatable by binding an
- export-granting set to `everyone`. One predicate, so the runtime anchor gate,
- the `@objectstack/lint` security-posture rule and the install-time suggestion
- surface all pick it up together.
- - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so
- `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It
- explains as `read ∧ the export grant`: `object_crud` reports the conjunction
- and attributes the granting set, while every data-shaped layer
- (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed
- as the `find` the export actually performs — asking the RLS compiler about an
- `export` operation would match no policy and wrongly report "no RLS applies".
- `readFilter` is surfaced for `export` as it is for `read`.
- - **plugin-reports** — closes the reports side door (#3710). A report rendered
- as `csv`/`json` is the same bulk copy of the same object, so it is gated by
- the same `ISecurityService.canExport`. Enforced in `executeReport`, which the
- interactive run, the ad-hoc run and the scheduled dispatch all funnel through;
- `scheduleReport` additionally refuses at create time so an author is not told
- at 3am. A schedule created while granted stops delivering once the grant is
- revoked. `html_table` stays a read — it is a rendered view, not a bulk copy.
- Deployments without `plugin-security` are unaffected (no permission sets
- exist, so the axis does not apply).
-
-
-
-### Patch Changes
-
-- f40c5b4: refactor(plugin-approvals,plugin-reports): enforcement implementations annotate the full `ExecutionContext` (#7135)
-
- The services half of #7070, mirroring what PR #7140 did for
- `plugin-sharing` / `plugin-audit`. #6523 converged 36 contract signatures onto
- the complete `resolveAuthzContext` envelope, applying the #6206 ruling —
- enforcement adjudicates on the whole envelope, never a per-site subset. The
- implementations behind those contracts still annotated their own parameters
- with the six-field shape the contracts used to name, so nothing they could
- _read_ had widened.
-
- `ApprovalService`, the approval flow-node provider and `ReportService` now
- declare `ExecutionContext` on all 43 of those positions, and the casts the
- narrow annotation forced are gone:
-
- - `isOverrideActor()` read the derived `posture` (ADR-0095) through an
- unchecked `(context as any)`. That gate decides whether a platform or tenant
- admin may release a STUCK approval — one routed to an unstaffed position, the
- only in-product recovery from a permanently locked record — so an erasure sat
- directly on an enforcement input: a mistyped rung would have compiled and
- silently denied every override. It is a declared read now.
- - Both services' `SYSTEM_CTX` is typed as the envelope and passed as itself,
- retiring the `SYSTEM_CTX as unknown as …` double casts at the three sites
- that hand it to a contract method.
- - The `(context as any).userId` / `.tenantId` reads in `ApprovalService` now
- read declared fields.
- - `OwnerContextResolver` returns the envelope, which is what a scheduled report
- actually resolves for its owner (#2849 / #2980).
-
- **No runtime behaviour changes.** The values were always complete — this
- family's damage was type-side — so every gate answers exactly what it answered
- before. Method parameters only WIDEN what they accept, so no caller is
- affected, and no public export changes shape.
-
- Casts deliberately kept, and now documented where they sit: `organizationId`
- is not a field of the envelope at all — that spelling has its own history
- (#5858 / `check:org-identifier`) and was held out of this change by #7070. In
- `approval-node.ts` the single remaining assertion exists only because the
- literal names that key; it was reduced from `as unknown as …` to a single
- `as ExecutionContext`, which still requires the literal to be comparable to
- the envelope.
-
- Because a re-narrowed annotation would compile, ship and pass every test in
- these packages, the convergence is pinned by a new compile-time module per
- package, `exec-context-annotation.pin.ts`: it hands each parameter a fresh
- literal naming envelope-only fields (`posture`, `accessible_org_ids`,
- `org_user_ids`), which TypeScript's excess-property check rejects the moment a
- parameter narrows back, plus negative cases so a parameter erased to `any`
- cannot pass either.
-
- The exported `SharingExecutionContext` type itself is NOT removed here: it is
- defined in `packages/spec`, which is single-owner, so its retirement is a
- separate follow-up.
-
-- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
-
- The AGENTS.md post-task checklist requires breaking changesets to carry their
- FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
- inside the npm package and is what an upgrading agent greps after the tombstone
- error." That delivery path was severed for 68 of the 69 publishable packages:
- npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
- older npm versions — not `CHANGELOG.md`, and the canonical
- `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
- 10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
- 70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
- explicitly.
-
- The tombstone-error scenario is precisely the one where the repo is out of
- reach — the upgrading agent has `node_modules` and nothing else — so the
- migration text has to ride in the tarball. Every publishable package now
- declares `CHANGELOG.md` in `files`, and the canonical whitelist is
- `["dist", "README.md", "CHANGELOG.md"]`.
-
- The other half is the gate: `check:published-files` gains a fifth invariant,
- COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
- always-required lint job, so the next package cannot silently sever the path
- again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
- into the canonical set.
-
- Consumer-visible change: one more file per install (the package's changelog,
- e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
-node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
- promised.
-
-- b5f9397: fix(sharing,runtime): a `sort` passed straight to the engine never ordered anything; migrate every in-repo engine call to canonical QueryAST keys (#4346)
-
- Two changes with different weights, from one sweep of every in-repo engine
- call site that still speaks a deprecated alias.
-
- **The bug — three dropped sorts.** #4346 made the engine fold `filter`→`where`
- and `top`→`limit` on all six methods. The other four pairs in
- `RPC_QUERY_ALIAS_SLOTS` (`select`, `sort`, `skip`, `populate`) are folded at
- the RPC/wire layer only — their values need shape lowering that belongs to
- those layers — and a **direct `engine.find()` never crosses that layer**. Three
- call sites passed `sort` there, so it rode onto the AST untouched, every
- driver's `Array.isArray(query.orderBy)` guard declined to emit an ORDER BY, and
- the query returned an ordinary-looking, arbitrarily-ordered result:
-
- | call site | asked for | actually got |
- | ----------------------------------- | ------------------------------------------------- | --------------------------- |
- | `share-link-routes.ts` | shared AI conversation messages, `created_at asc` | messages in arbitrary order |
- | `runtime/domains/share-links.ts` | same route, runtime-domain copy | same |
- | `share-link-service.ts` `listLinks` | the 200 most recent share links | an arbitrary 200 |
-
- All three combine the dropped sort with a `limit` — the "latest N" shape whose
- failure #4226 spelled out: an unapplied sort returns rows in arbitrary order,
- which `limit` then slices into an arbitrary page. #4226 fixed that in the wire
- normalizer; these calls sit one layer below it. `listLinks` had no test at all,
- which is why it went unnoticed. Now pinned — on the option bag the engine
- receives, not on row order, because the failure is that the key never becomes
- `orderBy` and a fake engine honouring either spelling would pass either way.
-
- **The cleanup — 27 no-op renames.** Every remaining in-repo engine call passing
- `filter` now passes `where` (approvals 5, auth 2, reports 6, sharing 11,
- webhooks 2, plus the one `filters` in a spec doc example). These are strict
- no-ops since #4346 folds the alias — the point is that the framework stops
- depending on a spelling it asks users to migrate off, which is a prerequisite
- for ever retiring the aliases. Service-level `filter` PARAMETERS (each
- service's own public API, e.g. `listRequests(filter)`) are deliberately
- untouched — those are not engine option bags.
-
- Two of the renamed calls were live victims of the #4346 bug rather than
- cosmetic: `auth-manager`'s `stampIdentitySource` read the table's first row via
- `findOne({filter})` and counted the whole table via `count({filter})`, so a
- federated sign-in never stamped `source: 'idp_provisioned'`. #4346 already
- corrected the behaviour; this makes the call say what it means.
-
-- d0d5205: refactor(core,plugin-audit,service-storage,plugin-reports): give the `__` operation-private-key convention a single owner (#7284)
-
- `withoutOperationPrivateKeys` — the rule that a consumer forwarding a caller's
- execution envelope to a question about a DIFFERENT object must first drop the
- `__`-prefixed keys plugin-security stamped for the operation in flight — had been
- hand-copied into three packages: `plugin-audit`'s comment access hooks (#7141),
- `service-storage`'s attachment access hooks (#7145) and `plugin-reports`' report
- service (#7204). Each carried its own `OPERATION_PRIVATE_KEY_PREFIX` and its own
- doc block, and the prose had already diverged while the code still agreed — the
- shape that makes a later divergence in behaviour hard to notice.
-
- The helper now lives once, in `@objectstack/core`
- (`security/operation-private-keys.ts`), exported from the package root. Core is
- the only candidate all three consumers already depend on: `plugin-security` is
- the producer of the convention and the most honest owner, but none of the three
- depends on it and a string-prefix filter does not justify three new dependency
- edges onto a plugin; `@objectstack/spec` is fenced off by Prime Directive #2. The
- new home sits beside `assemble-execution-context.ts`, which owns the other end of
- the same lifecycle — that file is where an `ExecutionContext` is built at a
- transport entry point, this one is where it is stripped back down before being
- forwarded.
-
- The full reasoning moved with the code rather than being thinned: which keys the
- middleware stamps and why each is a widening input, why they are dropped by
- PREFIX and never by a name list, and why the fresh copy is load-bearing in both
- directions. Each consumer keeps only its own local half — which object _its_
- gates actually ask about — and points at the shared home.
-
- No behaviour change: the three copies were byte-equivalent, and all three
- packages' suites pass unchanged. Two new pins at the home cover it — the rule's
- own behaviour, which no package-level test had ever asserted directly, and a
- repository-shape pin that turns red if a fourth file declares its own copy.
-
-- cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
-
- These 20 packages declared no `files` field, so npm fell back to packing the
- whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
- listed **21 files** — 15 under `src/`, three of them unit tests
- (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
- build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
- publish time rather than instead of it, so consumers were installing the
- TypeScript sources and the test suite alongside the artifact they asked for.
-
- Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
- that already did. Nothing a consumer imports moves: every `main` / `types` /
- `exports` target in all 20 already resolved inside `dist/`, which the new
- `check:published-files` guard verifies rather than assumes. The visible change
- is a smaller install and a smaller dependency-scanning surface — `npm pack` on
- `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
-
- The other half of the fix is the gate. Half the packages declaring `files` and
- half not was the #3786 shape — a hand-copied convention with nothing enforcing
- it, where whoever forgets the line gets no signal at all. `check:published-files`
- (new, wired into the always-required `lint` job) holds every non-private
- workspace package to four invariants: `files` is **declared**; it is
- **sufficient** (covers every entry point, so tightening a whitelist cannot ship
- a package that fails to resolve); it is **minimal** (admits no test, test-harness
- config or build script); and anything beyond `dist` + `README.md` is
- **registered** with a reason, reconciled in both directions so a stale exemption
- is an error rather than dead text. `@objectstack/spec` is the one package with
- registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
- `CHANGELOG.md` are product, not build input.
-
- This also closes an assumption #4206 was resting on. Excluding `/scripts/**`
- from the docs-drift implementation test is sound only while no package publishes
- `scripts/` as runtime code; that held, but it held because someone read all three
- offenders by hand. It is now checked on every PR.
-
-- cb466aa: Reports read with the caller's whole execution envelope, so a `group`-posture report no longer under-reports
-
- `executeReport` rebuilt a five-field projection of the caller's `ExecutionContext`
- (`userId` / `tenantId` / `positions` / `permissions` / `isSystem`) before handing it to
- the engine read that produces the report — while the method's own comment promised
- "reports execute with the caller's identity".
-
- **Before.** `accessible_org_ids` was not in that projection, and the engine reads it by
- name (`buildDriverOptions`, ADR-0105 D2 / #3623) to widen the driver's native tenant
- scope to the caller's whole membership set under the `group` tenancy posture. Absent, the
- drivers fall back to active-org equality — "fail toward isolation". So the identical query
- returned the membership union in an interactive list view and collapsed to the active org
- inside a **saved or scheduled** report: silently short rows, no error, nothing in the
- output saying so. Measured end-to-end on a real kernel + SQL driver: three rows across two
- member orgs came back as three interactively and two in the report, and a scheduled CSV
- digest emailed the owner the same two. `timezone` went the same way, so a read-time
- formula field resolved its calendar day in UTC instead of the caller's business timezone;
- `posture`, `org_user_ids`, `systemPermissions` and `onBehalfOf` were dropped too.
-
- **After.** The read receives the caller's envelope whole (the #6206 ruling — enforcement
- adjudicates on the whole `resolveAuthzContext` envelope, never a per-site subset), minus
- the `__`-prefixed keys plugin-security stamps for the operation in flight, and as a fresh
- object so a callee's stamp cannot write back into the caller's request context. The same
- shape `plugin-audit` (#7141) and `service-storage` (#7145) landed. Direction is unchanged
- outside `group`: the `isolated` posture, a deployment with no posture provider, and a
- `group` caller with an empty accessible set all still read at active-org equality.
-
-- 2c2a212: fix(reports): owner-gate the saved-report schedule routes (#2980)
-
- The report read/run/delete routes are owner-isolated (a caller may only touch a
- report they own, denied as `REPORT_NOT_FOUND` to avoid leaking that the id
- exists), but the two schedule routes bypassed that gate: `unscheduleReport` and
- `listSchedules` took the caller `context` as `_context` and never consulted it,
- querying under the system context (RLS-bypassing). Any authenticated caller
- could therefore delete another owner's report schedule — a cross-owner
- destructive write — or list another owner's schedules (leaking recipient
- addresses and cron), by supplying an id.
-
- Both now resolve the schedule's parent report and require the caller to own it,
- mirroring the sibling routes:
-
- - **`unscheduleReport`** loads the schedule, then its report, and deletes only
- when `canAccessReport` holds; a cross-owner attempt throws `REPORT_NOT_FOUND`
- (mapped to `404` by the REST layer, deny-as-404 anti-enumeration), while a
- genuinely-absent schedule stays idempotent. `scheduleReport` (create) was
- already gated via `getReport`, so only the delete/list doors were open.
- - **`listSchedules`** returns an empty list to any non-system caller who cannot
- access the report it is scoped to — the same non-leaking posture as
- `listReports`. The scheduler's system context still sees every schedule.
-
- No authoring-surface or metadata change; existing owner-path behavior is
- unchanged.
-
-- dadd1ad: refactor(spec,plugin-sharing): retire the exported `SharingExecutionContext` type (#7218)
-
-
-
- **BREAKING — public surface removal.** `SharingExecutionContext` is deleted from
- `@objectstack/spec` (`contracts/sharing-service`) and from
- `@objectstack/plugin-sharing`, which re-exported it. Both `api-surface/` and
- `export-origins/` snapshots are regenerated accordingly.
-
- This is the deferred deletion recorded when #7070 split the convergence in two.
- #6523 / PR #7068 converged 36 contract signatures onto the full
- `resolveAuthzContext` envelope (`ExecutionContext`), applying the #6206 ruling —
- enforcement adjudicates on the whole envelope, never a per-site subset. The
- consumer halves then re-annotated the implementations: PR #7140 (identity:
- `plugin-sharing`, `plugin-audit`) and PR #7206 (services: `plugin-approvals`,
- `plugin-reports`). Both landed with the type still exported, because it is
- DEFINED in `packages/spec` and that package's retirement is the spec seat's to
- make. Nothing declares it any more, so it goes.
-
- **Migration.** Anyone who imported `SharingExecutionContext` from either package
- should import `ExecutionContext` from `@objectstack/spec` instead — the type the
- contracts have declared since #7068. The old shape was six optional fields, all
- of which exist on the envelope with the same names and types, so a value that
- satisfied the retired type already satisfies `ExecutionContext`; only the
- spelling of the annotation changes.
-
- **No runtime behaviour changes.** The type was erased at compile time and no
- signature's accepted shape moved: the contracts already took the wide envelope.
-
- **What the retirement did NOT remove — the reason to read the pins.** Deleting
- the type does not make re-narrowing a compile error. Structural subtyping still
- accepts a six-field context where the envelope is expected, so the boundary is
- held by the declared parameter type plus the pins, exactly as before. The three
- `exec-context-annotation.pin.ts` files (`plugin-sharing`, `plugin-approvals`,
- `plugin-reports`) told their failure story as "the parameter narrows back to
- `SharingExecutionContext`", which a deletion would have quietly hollowed out.
- Each now keeps the retired six-field shape as a local, non-exported SPECIMEN
- type and refutes every enforcement parameter against it by type identity, so a
- re-narrowing under ANY name is red — alongside the fresh-literal
- excess-property checks they already carried. `sharing-service.test.ts` in
- `packages/spec` is re-anchored the same way, and its "twin unchanged in shape"
- case becomes a "twin stays retired" case. The narrative the retired type's doc
- block carried (the measured `(context as any).posture` specimen, and why tsc
- cannot police this) moves to the module doc of `contracts/sharing-service`,
- which the contracts and pins now point at.
-
-- ea936f3: fix(plugin-reports): `DELETE /api/v1/reports/schedules/:scheduleId` stops telling a caller whether a schedule id exists
-
- `DELETE /api/v1/reports/schedules/:scheduleId` answered differently depending on
- whether the target id **existed**, which let any authenticated caller enumerate
- other owners' report schedules by probing ids and reading the status code:
-
- | Target | Before | After |
- | ----------------------------------- | ---------------------- | ---------------------------------- |
- | Another owner's schedule id | `404 REPORT_NOT_FOUND` | `404 REPORT_NOT_FOUND` (unchanged) |
- | A schedule id that does not exist | `204 No Content` | `404 REPORT_NOT_FOUND` |
- | A schedule whose report row is gone | `404 REPORT_NOT_FOUND` | `404 REPORT_NOT_FOUND` (unchanged) |
- | Your own schedule | `204 No Content` | `204 No Content` (unchanged) |
-
- This is the same defect #7523 closed on the sibling `DELETE /reports/:id`, in the
- costume that card explicitly warned about: there the split was 500-vs-204 and
- loud, here it was 404-vs-204 and read as correct. The route was in fact cited by
- #7523's investigation as the example of the _right_ shape, because it does route
- its catch through `handleValidation` — which is why the cross-owner arm is a
- clean 404 rather than a 500. Only the cross-owner arm was ever probed (QA run
- #7515); the unknown-id arm was not, so the surviving half went unseen and
- `packages/rest/src/rest.test.ts` pinned its `204` green.
-
- `ReportService.unscheduleReport()` carried the intent — _"others get a not-found
- so the delete neither fires nor reveals the schedule's existence"_ — and a hole
- one line wide above it: `if (!schedule) return; // idempotent`. Idempotence is
- only harmless where every caller may see the row; with a cross-owner arm that
- throws, resolving quietly _is_ the tell.
-
- Both deny arms are now one decision, taken before the delete fires, by the
- predicate already blind to the difference between them: `canAccessReport` is
- false for a schedule that does not exist, for one whose report is gone, and for
- one owned by somebody else alike. A single throw site means a single message, so
- the route's single `handleValidation` call emits a single response — status and
- body cannot drift apart.
-
- Unlike `deleteReport`, this could not be pre-empted in the route. That one
- collapses its arms with `getReport()`, which is already blind to the same
- difference (#2980); the caller here presents a `scheduleId`, and `IReportService`
- exposes no by-id schedule read to be blind with (`listSchedules` is keyed by
- `reportId`). The blinding therefore lives in the service, and
- `IReportService.unscheduleReport` now states it as a contract obligation rather
- than leaving each implementation to rediscover it.
-
- Deleting a schedule you own still answers `204`. Deleting one you cannot see is
- now `404` instead of a silent `204` — the cost of closing the oracle, and in line
- with the cross-owner GET / run / upsert-overwrite / delete arms, which all
- already answer 404. A system/dispatcher context deleting an id with no row now
- gets `REPORT_NOT_FOUND` too, where it previously resolved; no caller in the repo
- relies on that (the route is the only production caller).
-
- Tests assert the two deny arms' responses are **EQUAL** rather than pinning each
- arm's status separately, so the plausible half-fix cannot pass through them — a
- mutation that answers both arms 404 with different bodies leaves every per-arm
- status assertion green and turns the equality assertions red.
-
-- 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
-
- Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
- roots. 35 lookup sites that had been erased to `any` now carry the slot's
- contract, so the compiler checks what each plugin actually calls on the service
- it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
-
- **Two real defects, both of the shape this sweep exists to find.** Approvals'
- actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
- read the HTTP server under `http-server` _only_ — the deprecated alias. The
- ledger records `http.server` as canonical and as the only name present on every
- provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
- that path both lookups threw, the surrounding `catch` swallowed it, and the
- routes silently never mounted — approval e-mail action links 404'd and the
- share-link surface was absent, with nothing in the log to say so. Both reads are
- now canonical-first with the alias as fallback, each name in its own `try`
- because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
- never reaches `b` — the same correction #4393 made in metadata and
- cloud-connection).
-
- Typing choices follow the batch method: pure data-plane consumers take the
- narrow contract (`IDataEngine` in reports), consumers that bind hook or
- middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
- sharing and pinyin-search), and slots with no contract get a **named** local
- surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
- surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
- `SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
- still makes the compiler name every call site; `any` says nothing.
-
- No behaviour change beyond the two alias reads. No contract changes.
-
-- Updated dependencies [50616d9]
-- Updated dependencies [430dcc2]
-- Updated dependencies [690ccf2]
-- Updated dependencies [6a67d7a]
-- Updated dependencies [098f4bb]
-- Updated dependencies [333a374]
-- Updated dependencies [9fe9c1d]
-- Updated dependencies [3d5c090]
-- Updated dependencies [e5bd768]
-- Updated dependencies [08b5a3d]
-- Updated dependencies [e027b3e]
-- Updated dependencies [e6ac4bd]
-- Updated dependencies [c2429b0]
-- Updated dependencies [445a0c2]
-- Updated dependencies [d99aeb3]
-- Updated dependencies [f6609e6]
-- Updated dependencies [4727eb8]
-- Updated dependencies [a70358a]
-- Updated dependencies [0ecc656]
-- Updated dependencies [06772eb]
-- Updated dependencies [d4e0809]
-- Updated dependencies [80334c7]
-- Updated dependencies [f63cd09]
-- Updated dependencies [97e7e3c]
-- Updated dependencies [ce5242c]
-- Updated dependencies [a7163ea]
-- Updated dependencies [e6e9379]
-- Updated dependencies [5823d59]
-- Updated dependencies [3140f9c]
-- Updated dependencies [9500ba4]
-- Updated dependencies [fa3d0cf]
-- Updated dependencies [af5a224]
-- Updated dependencies [71f76e1]
-- Updated dependencies [37b1346]
-- Updated dependencies [99736a0]
-- Updated dependencies [fe67e34]
-- Updated dependencies [fdb4f50]
-- Updated dependencies [270650f]
-- Updated dependencies [3aef718]
-- Updated dependencies [1bd5652]
-- Updated dependencies [14252d3]
-- Updated dependencies [7fb436c]
-- Updated dependencies [879ea13]
-- Updated dependencies [8828b9e]
-- Updated dependencies [1ea6bce]
-- Updated dependencies [c1dcacd]
-- Updated dependencies [ad303ed]
-- Updated dependencies [32ccb23]
-- Updated dependencies [f5a4ef0]
-- Updated dependencies [2d3e255]
-- Updated dependencies [a8940e4]
-- Updated dependencies [7d7521f]
-- Updated dependencies [5dc4d02]
-- Updated dependencies [f724f69]
-- Updated dependencies [98877c9]
-- Updated dependencies [98877c9]
-- Updated dependencies [53068c1]
-- Updated dependencies [ee58392]
-- Updated dependencies [f16e54e]
-- Updated dependencies [c44dd5e]
-- Updated dependencies [06be54e]
-- Updated dependencies [28ad90e]
-- Updated dependencies [76d74ec]
-- Updated dependencies [201b31f]
-- Updated dependencies [e6b1b69]
-- Updated dependencies [259459d]
-- Updated dependencies [3f7f14e]
-- Updated dependencies [e2616e0]
-- Updated dependencies [6fdc5c6]
-- Updated dependencies [8b9d71e]
-- Updated dependencies [05154a1]
-- Updated dependencies [33f5e23]
-- Updated dependencies [259af21]
-- Updated dependencies [f8644c7]
-- Updated dependencies [306ca50]
-- Updated dependencies [978fed2]
-- Updated dependencies [cfc293f]
-- Updated dependencies [587fc91]
-- Updated dependencies [de70b42]
-- Updated dependencies [9b6fe7c]
-- Updated dependencies [fb3d99b]
-- Updated dependencies [1986594]
-- Updated dependencies [6968885]
-- Updated dependencies [eaed61f]
-- Updated dependencies [52200b4]
-- Updated dependencies [cdfbee2]
-- Updated dependencies [ad4af62]
-- Updated dependencies [debe2f6]
-- Updated dependencies [d44dbfa]
-- Updated dependencies [29c6c9d]
-- Updated dependencies [d21c001]
-- Updated dependencies [ad047d2]
-- Updated dependencies [8c711fb]
-- Updated dependencies [f1cc3a3]
-- Updated dependencies [09e4547]
-- Updated dependencies [97b0798]
-- Updated dependencies [474fe39]
-- Updated dependencies [0bc685a]
-- Updated dependencies [b949059]
-- Updated dependencies [2826d1e]
-- Updated dependencies [be1c52c]
-- Updated dependencies [c5ff96d]
-- Updated dependencies [5a84d41]
-- Updated dependencies [84e7be9]
-- Updated dependencies [91f4c78]
-- Updated dependencies [ddc2527]
-- Updated dependencies [820eff9]
-- Updated dependencies [a6c3f38]
-- Updated dependencies [5fa04fb]
-- Updated dependencies [debc23a]
-- Updated dependencies [0f8ad09]
-- Updated dependencies [553a47f]
-- Updated dependencies [43a7a8d]
-- Updated dependencies [a98085f]
-- Updated dependencies [20b1a9e]
-- Updated dependencies [344a22a]
-- Updated dependencies [4827e91]
-- Updated dependencies [8d895ff]
-- Updated dependencies [86f7a20]
-- Updated dependencies [a3a884d]
-- Updated dependencies [cfed092]
-- Updated dependencies [203a449]
-- Updated dependencies [8f9689f]
-- Updated dependencies [73f69dc]
-- Updated dependencies [04c56aa]
-- Updated dependencies [f6472d7]
-- Updated dependencies [57a3bb3]
-- Updated dependencies [b3efeb7]
-- Updated dependencies [ddd075a]
-- Updated dependencies [88154be]
-- Updated dependencies [e8dc61e]
-- Updated dependencies [9c82146]
-- Updated dependencies [5f9a987]
-- Updated dependencies [744b8f5]
-- Updated dependencies [ac37fc6]
-- Updated dependencies [9f060e5]
-- Updated dependencies [bc17d39]
-- Updated dependencies [2f3e793]
-- Updated dependencies [4820f55]
-- Updated dependencies [462d9c4]
-- Updated dependencies [78caf51]
-- Updated dependencies [7d21581]
-- Updated dependencies [37785ed]
-- Updated dependencies [62a789b]
-- Updated dependencies [2e284b2]
-- Updated dependencies [d8e8d9c]
-- Updated dependencies [789ad63]
-- Updated dependencies [f2445c9]
-- Updated dependencies [94e749b]
-- Updated dependencies [ea1d916]
-- Updated dependencies [2af1988]
-- Updated dependencies [0af50a3]
-- Updated dependencies [1b49eaf]
-- Updated dependencies [ae31a19]
-- Updated dependencies [2e836de]
-- Updated dependencies [e0f300b]
-- Updated dependencies [0161c7f]
-- Updated dependencies [e900015]
-- Updated dependencies [db02d47]
-- Updated dependencies [b5bdf48]
-- Updated dependencies [23338c3]
-- Updated dependencies [12a19a8]
-- Updated dependencies [5b843fb]
-- Updated dependencies [62b6a2f]
-- Updated dependencies [7e5af5c]
-- Updated dependencies [5b4780b]
-- Updated dependencies [a933452]
-- Updated dependencies [9d1d9c7]
-- Updated dependencies [8140915]
-- Updated dependencies [a019e52]
-- Updated dependencies [e8f8f6c]
-- Updated dependencies [41dcda3]
-- Updated dependencies [7b48cf9]
-- Updated dependencies [b5404f4]
-- Updated dependencies [64fc6d5]
-- Updated dependencies [b746aa0]
-- Updated dependencies [b4487aa]
-- Updated dependencies [1007379]
-- Updated dependencies [65ca83a]
-- Updated dependencies [0bfdf46]
-- Updated dependencies [947d4f9]
-- Updated dependencies [f764691]
-- Updated dependencies [e120a5a]
-- Updated dependencies [e5bd2f6]
-- Updated dependencies [e650d67]
-- Updated dependencies [04476e7]
-- Updated dependencies [67bf2e2]
-- Updated dependencies [eaaf03c]
-- Updated dependencies [d17df80]
-- Updated dependencies [7d0e7b5]
-- Updated dependencies [c6d1cb4]
-- Updated dependencies [6513c17]
-- Updated dependencies [36030ff]
-- Updated dependencies [79228cd]
-- Updated dependencies [6117f7b]
-- Updated dependencies [e533b0b]
-- Updated dependencies [cdf4d9a]
-- Updated dependencies [aee1806]
-- Updated dependencies [c13350b]
-- Updated dependencies [c13350b]
-- Updated dependencies [2c1988c]
-- Updated dependencies [9ca2d85]
-- Updated dependencies [c13350b]
-- Updated dependencies [891d345]
-- Updated dependencies [c8124e5]
-- Updated dependencies [a52e2ef]
-- Updated dependencies [5293114]
-- Updated dependencies [376a061]
-- Updated dependencies [c142ced]
-- Updated dependencies [211abdb]
-- Updated dependencies [b3363e9]
-- Updated dependencies [eda599e]
-- Updated dependencies [a1a4140]
-- Updated dependencies [7c7e246]
-- Updated dependencies [2ef1807]
-- Updated dependencies [f35cdc5]
-- Updated dependencies [d03fe25]
-- Updated dependencies [217e2e6]
-- Updated dependencies [2672f85]
-- Updated dependencies [20bc357]
-- Updated dependencies [11066f6]
-- Updated dependencies [916af17]
-- Updated dependencies [84c86fb]
-- Updated dependencies [2a2a9fb]
-- Updated dependencies [86a71d1]
-- Updated dependencies [c001422]
-- Updated dependencies [77022a9]
-- Updated dependencies [d5c75e2]
-- Updated dependencies [03d26f7]
-- Updated dependencies [5966c2a]
-- Updated dependencies [2382580]
-- Updated dependencies [9ea2bc5]
-- Updated dependencies [a2e157c]
-- Updated dependencies [95c4227]
-- Updated dependencies [2a61116]
-- Updated dependencies [52760bf]
-- Updated dependencies [5543020]
-- Updated dependencies [880d343]
-- Updated dependencies [6e82972]
-- Updated dependencies [d4df105]
-- Updated dependencies [4615a18]
-- Updated dependencies [f505689]
-- Updated dependencies [d9fa683]
-- Updated dependencies [606d577]
-- Updated dependencies [4384921]
-- Updated dependencies [e2798fa]
-- Updated dependencies [3c628ce]
-- Updated dependencies [c2d9098]
-- Updated dependencies [0fd8556]
-- Updated dependencies [3c7bcc0]
-- Updated dependencies [4b6cac7]
-- Updated dependencies [7631964]
-- Updated dependencies [ac471a0]
-- Updated dependencies [60ae58e]
-- Updated dependencies [7f62706]
-- Updated dependencies [667fa44]
-- Updated dependencies [37e38d1]
-- Updated dependencies [e906126]
-- Updated dependencies [ce92674]
-- Updated dependencies [08363a0]
-- Updated dependencies [444de5b]
-- Updated dependencies [a227ed7]
-- Updated dependencies [7cb922e]
-- Updated dependencies [1d22114]
-- Updated dependencies [1eb13a0]
-- Updated dependencies [c52e608]
-- Updated dependencies [9613396]
-- Updated dependencies [3f7b4ff]
-- Updated dependencies [74155c7]
-- Updated dependencies [b5f9397]
-- Updated dependencies [ed77493]
-- Updated dependencies [6908830]
-- Updated dependencies [8b06bba]
-- Updated dependencies [58a03d2]
-- Updated dependencies [2bacd1a]
-- Updated dependencies [e47b342]
-- Updated dependencies [4c54037]
-- Updated dependencies [dc530b4]
-- Updated dependencies [9f601e8]
-- Updated dependencies [6a9dec6]
-- Updated dependencies [0f7157b]
-- Updated dependencies [4dc1c7d]
-- Updated dependencies [d9bef45]
-- Updated dependencies [4dfd002]
-- Updated dependencies [f549a0d]
-- Updated dependencies [51c5227]
-- Updated dependencies [82da264]
-- Updated dependencies [f586f1a]
-- Updated dependencies [77be690]
-- Updated dependencies [4ed7ed4]
-- Updated dependencies [9b9b70f]
-- Updated dependencies [f5a9bc2]
-- Updated dependencies [e59786e]
-- Updated dependencies [2fa4ca1]
-- Updated dependencies [bcf1112]
-- Updated dependencies [baeb4f0]
-- Updated dependencies [29488cc]
-- Updated dependencies [881a3cc]
-- Updated dependencies [f5a2320]
-- Updated dependencies [ad6317b]
-- Updated dependencies [811c30c]
-- Updated dependencies [a4a85c8]
-- Updated dependencies [859cb83]
-- Updated dependencies [07a4e26]
-- Updated dependencies [9774b78]
-- Updated dependencies [8a88885]
-- Updated dependencies [deb538f]
-- Updated dependencies [b49ccfd]
-- Updated dependencies [5b89711]
-- Updated dependencies [85d95e7]
-- Updated dependencies [08cd163]
-- Updated dependencies [0c8a22f]
-- Updated dependencies [5f7669e]
-- Updated dependencies [becbe53]
-- Updated dependencies [b127c8b]
-- Updated dependencies [763931e]
-- Updated dependencies [ec975f1]
-- Updated dependencies [168f60f]
-- Updated dependencies [b07d829]
-- Updated dependencies [de9af8a]
-- Updated dependencies [eb4204b]
-- Updated dependencies [a80302a]
-- Updated dependencies [a648e96]
-- Updated dependencies [a47ac06]
-- Updated dependencies [e4c61a7]
-- Updated dependencies [cc60165]
-- Updated dependencies [474f131]
-- Updated dependencies [081aa6f]
-- Updated dependencies [91f4c78]
-- Updated dependencies [050cd82]
-- Updated dependencies [4d552af]
-- Updated dependencies [44d677c]
-- Updated dependencies [c32944d]
-- Updated dependencies [1dd780f]
-- Updated dependencies [e8d0c21]
-- Updated dependencies [244ca86]
-- Updated dependencies [546ab3c]
-- Updated dependencies [c4df271]
-- Updated dependencies [c8d6f6e]
-- Updated dependencies [0b51bb6]
-- Updated dependencies [d9971d3]
-- Updated dependencies [7dc1067]
-- Updated dependencies [4f13be2]
-- Updated dependencies [a41ba5c]
-- Updated dependencies [189854c]
-- Updated dependencies [0e3a226]
-- Updated dependencies [92a67f2]
-- Updated dependencies [9136327]
-- Updated dependencies [bf0ae99]
-- Updated dependencies [eb3e650]
-- Updated dependencies [abeb375]
-- Updated dependencies [cb3b6cd]
-- Updated dependencies [73b7234]
-- Updated dependencies [d2b97c3]
-- Updated dependencies [61cc079]
-- Updated dependencies [45dc446]
-- Updated dependencies [0e96e46]
-- Updated dependencies [c1d44f7]
-- Updated dependencies [59b794f]
-- Updated dependencies [ef4efa8]
-- Updated dependencies [cbb6a5c]
-- Updated dependencies [fc3a36a]
-- Updated dependencies [ab9fb5c]
-- Updated dependencies [69787f0]
-- Updated dependencies [5d022a1]
-- Updated dependencies [042b9ee]
-- Updated dependencies [f985b3f]
-- Updated dependencies [795b6e1]
-- Updated dependencies [d52d4fe]
-- Updated dependencies [742cebb]
-- Updated dependencies [175d789]
-- Updated dependencies [f549a0d]
-- Updated dependencies [524151c]
-- Updated dependencies [427344c]
-- Updated dependencies [8af76ae]
-- Updated dependencies [1d4756e]
-- Updated dependencies [720c5ad]
-- Updated dependencies [a8d1e24]
-- Updated dependencies [b85cc54]
-- Updated dependencies [a36db28]
-- Updated dependencies [7a8476f]
-- Updated dependencies [518ca7a]
-- Updated dependencies [d1cabaa]
-- Updated dependencies [41642b0]
-- Updated dependencies [4cca74c]
-- Updated dependencies [88ef03e]
-- Updated dependencies [9a4932a]
-- Updated dependencies [3f8817a]
-- Updated dependencies [a2443e3]
-- Updated dependencies [e1554b1]
-- Updated dependencies [9e2caf3]
-- Updated dependencies [4856789]
-- Updated dependencies [81ce41a]
-- Updated dependencies [85e1e4e]
-- Updated dependencies [c3f4916]
-- Updated dependencies [55dbbba]
-- Updated dependencies [33e0385]
-- Updated dependencies [dac6a08]
-- Updated dependencies [72c3c86]
-- Updated dependencies [2d8dba3]
-- Updated dependencies [7f1a635]
-- Updated dependencies [2205363]
-- Updated dependencies [09fe58d]
-- Updated dependencies [f9fc874]
-- Updated dependencies [d62f8eb]
-- Updated dependencies [d0a5ceb]
-- Updated dependencies [a7586cd]
-- Updated dependencies [4c5e80e]
-- Updated dependencies [4b5702a]
-- Updated dependencies [011b386]
-- Updated dependencies [e18a162]
-- Updated dependencies [e98fb14]
-- Updated dependencies [394b7a1]
-- Updated dependencies [ce92674]
-- Updated dependencies [0f2fdcd]
-- Updated dependencies [d6d1a50]
-- Updated dependencies [cf2c9b7]
-- Updated dependencies [8ffa8b9]
-- Updated dependencies [d127ff0]
-- Updated dependencies [674ac99]
-- Updated dependencies [833b512]
-- Updated dependencies [1b9a53b]
-- Updated dependencies [36d90fc]
-- Updated dependencies [7777e8f]
-- Updated dependencies [9b86cf6]
-- Updated dependencies [d063a96]
-- Updated dependencies [8825a06]
-- Updated dependencies [5087ac6]
-- Updated dependencies [677b591]
-- Updated dependencies [cf7c694]
-- Updated dependencies [ddd0f06]
-- Updated dependencies [d77d1b7]
-- Updated dependencies [0f9faa2]
-- Updated dependencies [2d1ddf0]
-- Updated dependencies [354b00f]
-- Updated dependencies [3de535b]
-- Updated dependencies [fe2e15a]
-- Updated dependencies [5b79a34]
-- Updated dependencies [502564d]
-- Updated dependencies [603cab8]
-- Updated dependencies [c757854]
-- Updated dependencies [471839d]
-- Updated dependencies [507b92a]
-- Updated dependencies [46365ab]
-- Updated dependencies [b508244]
-- Updated dependencies [df95346]
-- Updated dependencies [3dede58]
-- Updated dependencies [c6b6bb4]
-- Updated dependencies [594508e]
-- Updated dependencies [7cf42fe]
-- Updated dependencies [5966c2a]
-- Updated dependencies [59c544d]
-- Updated dependencies [0045682]
-- Updated dependencies [7309c81]
-- Updated dependencies [2f59da0]
-- Updated dependencies [d56012f]
-- Updated dependencies [f78dd83]
-- Updated dependencies [a2cd18a]
-- Updated dependencies [9051802]
-- Updated dependencies [20bc1ec]
-- Updated dependencies [1c625ca]
-- Updated dependencies [2f8328c]
-- Updated dependencies [2a6c279]
-- Updated dependencies [9319586]
-- Updated dependencies [8c8f0df]
-- Updated dependencies [8ad609c]
-- Updated dependencies [bbee302]
-- Updated dependencies [90c2b15]
-- Updated dependencies [4638aaa]
-- Updated dependencies [0222d3c]
-- Updated dependencies [08863dd]
-- Updated dependencies [071d0dc]
-- Updated dependencies [f293d45]
-- Updated dependencies [56664f5]
-- Updated dependencies [71f205d]
-- Updated dependencies [f067930]
-- Updated dependencies [414395b]
-- Updated dependencies [42eeb7d]
-- Updated dependencies [31cbe90]
-- Updated dependencies [6b7129a]
-- Updated dependencies [c5adfe1]
-- Updated dependencies [97ace2a]
-- Updated dependencies [26e1029]
-- Updated dependencies [0a936ea]
-- Updated dependencies [90bbf25]
-- Updated dependencies [023c00b]
-- Updated dependencies [eb91eba]
-- Updated dependencies [42da73d]
-- Updated dependencies [01e124d]
-- Updated dependencies [ef7b5ef]
-- Updated dependencies [9514767]
-- Updated dependencies [8f20201]
-- Updated dependencies [155507e]
-- Updated dependencies [643b7c7]
-- Updated dependencies [7bba90b]
-- Updated dependencies [8813b90]
-- Updated dependencies [108ba8d]
-- Updated dependencies [2a5f04a]
-- Updated dependencies [4f740b0]
-- Updated dependencies [7ce02eb]
-- Updated dependencies [b4ad984]
-- Updated dependencies [e7a7506]
-- Updated dependencies [a9f32df]
-- Updated dependencies [aeb9b27]
-- Updated dependencies [7d27da0]
-- Updated dependencies [d0d5205]
-- Updated dependencies [1a15893]
-- Updated dependencies [b70e534]
-- Updated dependencies [7e05d8e]
-- Updated dependencies [8f1851e]
-- Updated dependencies [61ea810]
-- Updated dependencies [2233a85]
-- Updated dependencies [67452d1]
-- Updated dependencies [089767f]
-- Updated dependencies [a13827e]
-- Updated dependencies [66d99ec]
-- Updated dependencies [cb43296]
-- Updated dependencies [b61afc1]
-- Updated dependencies [79021fc]
-- Updated dependencies [7733604]
-- Updated dependencies [4921a95]
-- Updated dependencies [40e420f]
-- Updated dependencies [62dd69a]
-- Updated dependencies [d13004a]
-- Updated dependencies [be7360c]
-- Updated dependencies [e15e679]
-- Updated dependencies [2ab1257]
-- Updated dependencies [0fc6219]
-- Updated dependencies [061406d]
-- Updated dependencies [e4c8b6c]
-- Updated dependencies [acb10f6]
-- Updated dependencies [605e190]
-- Updated dependencies [c6c59f1]
-- Updated dependencies [b0e78a8]
-- Updated dependencies [f31cc8d]
-- Updated dependencies [f343dc4]
-- Updated dependencies [8269e32]
-- Updated dependencies [74f7339]
-- Updated dependencies [a6c35a2]
-- Updated dependencies [c2f1002]
-- Updated dependencies [4cc4fb7]
-- Updated dependencies [97b6658]
-- Updated dependencies [28d1eb7]
-- Updated dependencies [06770c0]
-- Updated dependencies [2c26040]
-- Updated dependencies [f758cec]
-- Updated dependencies [5b47ab5]
-- Updated dependencies [b09d8d9]
-- Updated dependencies [b09d8d9]
-- Updated dependencies [8675db6]
-- Updated dependencies [b09d8d9]
-- Updated dependencies [27358d5]
-- Updated dependencies [1c3da1f]
-- Updated dependencies [c1f344b]
-- Updated dependencies [3eb1b2b]
-- Updated dependencies [9c93465]
-- Updated dependencies [a34fd2e]
-- Updated dependencies [ebb209c]
-- Updated dependencies [76bcb83]
-- Updated dependencies [59b85c0]
-- Updated dependencies [889ae47]
-- Updated dependencies [4f4c3fb]
-- Updated dependencies [78f0be8]
-- Updated dependencies [6e357ed]
-- Updated dependencies [d6938bf]
-- Updated dependencies [35f7fb4]
-- Updated dependencies [0410522]
-- Updated dependencies [63b33e6]
-- Updated dependencies [f163028]
-- Updated dependencies [814db6d]
-- Updated dependencies [a5302c7]
-- Updated dependencies [31e0be9]
-- Updated dependencies [4bfd455]
-- Updated dependencies [ffd2ce2]
-- Updated dependencies [2a44c1d]
-- Updated dependencies [7084313]
-- Updated dependencies [f07808c]
-- Updated dependencies [7ffc3d3]
-- Updated dependencies [88346ba]
-- Updated dependencies [4631592]
-- Updated dependencies [62f8017]
-- Updated dependencies [32ff033]
-- Updated dependencies [a831df1]
-- Updated dependencies [f752ee3]
-- Updated dependencies [a1b61e0]
-- Updated dependencies [cd6b9f2]
-- Updated dependencies [2cb6d3c]
-- Updated dependencies [af2a095]
-- Updated dependencies [5ac93d4]
-- Updated dependencies [695cfbd]
-- Updated dependencies [0e043d8]
-- Updated dependencies [93f267f]
-- Updated dependencies [7445149]
-- Updated dependencies [ec796d5]
-- Updated dependencies [071d0dc]
-- Updated dependencies [0024abf]
-- Updated dependencies [8dd98bf]
-- Updated dependencies [e87fea1]
-- Updated dependencies [c65e529]
-- Updated dependencies [0848bea]
-- Updated dependencies [d51bed2]
-- Updated dependencies [dadd1ad]
-- Updated dependencies [acbf364]
-- Updated dependencies [3ca34c1]
-- Updated dependencies [7adc841]
-- Updated dependencies [239c3a3]
-- Updated dependencies [b8b3c64]
-- Updated dependencies [2f2e63c]
-- Updated dependencies [4845f85]
-- Updated dependencies [486d526]
-- Updated dependencies [94a0bbc]
-- Updated dependencies [d6bfb3d]
-- Updated dependencies [8a9c079]
-- Updated dependencies [7b005b4]
-- Updated dependencies [cc3555e]
-- Updated dependencies [a2266a6]
-- Updated dependencies [d25a0ec]
-- Updated dependencies [89d7b35]
-- Updated dependencies [94f7b6a]
-- Updated dependencies [5c94f83]
-- Updated dependencies [ea936f3]
-- Updated dependencies [0c0fbd9]
-- Updated dependencies [667b83e]
-- Updated dependencies [f3141d8]
-- Updated dependencies [5487c20]
-- Updated dependencies [aa8b847]
-- Updated dependencies [7687f7b]
-- Updated dependencies [5a84d41]
-- Updated dependencies [fd3013a]
-- Updated dependencies [85ec26d]
-- Updated dependencies [73e576f]
-- Updated dependencies [f6476fc]
-- Updated dependencies [69ac82c]
-- Updated dependencies [4ac12ef]
-- Updated dependencies [833ed84]
-- Updated dependencies [a18abf3]
-- Updated dependencies [c6a4eeb]
-- Updated dependencies [1659072]
-- Updated dependencies [f450ae7]
-- Updated dependencies [abceb0d]
-- Updated dependencies [627b188]
-- Updated dependencies [8d4eae7]
-- Updated dependencies [c5a5996]
-- Updated dependencies [0c302a7]
-- Updated dependencies [b88f5e8]
-- Updated dependencies [857a6cf]
-- Updated dependencies [65a3a84]
-- Updated dependencies [6633337]
-- Updated dependencies [21676eb]
-- Updated dependencies [3f296bf]
-- Updated dependencies [e474853]
-- Updated dependencies [e9cb9ab]
-- Updated dependencies [42cc219]
-- Updated dependencies [d42a92f]
-- Updated dependencies [569611f]
-- Updated dependencies [51d74ad]
-- Updated dependencies [d7e0b42]
-- Updated dependencies [3510e4a]
-- Updated dependencies [f00d8d4]
-- Updated dependencies [5326b36]
-- Updated dependencies [aa4b90d]
-- Updated dependencies [ccd9397]
-- Updated dependencies [503be86]
-- Updated dependencies [54299ca]
-- Updated dependencies [ae490ef]
-- Updated dependencies [e124711]
-- Updated dependencies [dc61def]
-- Updated dependencies [bca935b]
-- Updated dependencies [d92c72d]
-- Updated dependencies [c54c822]
-- Updated dependencies [8dcc0f5]
-- Updated dependencies [75b9e51]
-- Updated dependencies [f61c8cf]
-- Updated dependencies [e3ef52b]
-- Updated dependencies [0a2f233]
-- Updated dependencies [8621cdd]
-- Updated dependencies [251e888]
-- Updated dependencies [07f1822]
-- Updated dependencies [e336549]
-- Updated dependencies [3bb9340]
-- Updated dependencies [1e604c4]
-- Updated dependencies [04fab5e]
-- Updated dependencies [183b4c4]
-- Updated dependencies [7f713b6]
-- Updated dependencies [d40f43a]
-- Updated dependencies [2fdb36e]
-- Updated dependencies [e787608]
-- Updated dependencies [6f23667]
-- Updated dependencies [cde1975]
-- Updated dependencies [0bc685a]
-- Updated dependencies [20526f5]
-- Updated dependencies [efedd28]
-- Updated dependencies [5d21a48]
-- Updated dependencies [5278e11]
-- Updated dependencies [c5eef1d]
-- Updated dependencies [e5e7ee0]
-- Updated dependencies [23dba62]
-- Updated dependencies [e0f300b]
-- Updated dependencies [761a0ba]
-- Updated dependencies [c960170]
-- Updated dependencies [19365b7]
-- Updated dependencies [ba98e26]
-- Updated dependencies [b7ed26d]
-- Updated dependencies [a2ebea2]
-- Updated dependencies [800bdb0]
-- Updated dependencies [9d4dfc4]
-- Updated dependencies [1059965]
-- Updated dependencies [def5919]
-- Updated dependencies [ee264b2]
-- Updated dependencies [60b672e]
-- Updated dependencies [f104bab]
-- Updated dependencies [68dea0b]
-- Updated dependencies [6b441a8]
-- Updated dependencies [64f8cbe]
-- Updated dependencies [6cb81c7]
-- Updated dependencies [61282f9]
-- Updated dependencies [ce0cfe9]
-- Updated dependencies [04f1182]
-- Updated dependencies [3a2dde7]
-- Updated dependencies [8c20f75]
-- Updated dependencies [be87153]
-- Updated dependencies [dd0f681]
-- Updated dependencies [60f0dd8]
-- Updated dependencies [a87c5cd]
-- Updated dependencies [a47f338]
-- Updated dependencies [b3a3d83]
-- Updated dependencies [7a55913]
-- Updated dependencies [35accbf]
-- Updated dependencies [6038de7]
-- Updated dependencies [fc5f536]
-- Updated dependencies [5647006]
-- Updated dependencies [e654bfd]
-- Updated dependencies [01a7337]
-- Updated dependencies [b45c71e]
-- Updated dependencies [d71ff32]
-- Updated dependencies [f8cfbb4]
-- Updated dependencies [6e6c872]
-- Updated dependencies [2598216]
-- Updated dependencies [11949fc]
-- Updated dependencies [2c7e62d]
-- Updated dependencies [eb95d97]
-- Updated dependencies [b098b0e]
-- Updated dependencies [4d00b13]
-- Updated dependencies [1363084]
-- Updated dependencies [fa5758e]
-- Updated dependencies [38f7e4f]
-- Updated dependencies [eb7613c]
-- Updated dependencies [c57f3cf]
-- Updated dependencies [ecc9110]
-- Updated dependencies [9aa5510]
-- Updated dependencies [e4c2dc8]
-- Updated dependencies [97faca3]
-- Updated dependencies [57bab76]
-- Updated dependencies [c89d18c]
-- Updated dependencies [1bd2795]
-- Updated dependencies [f7bd4e2]
-- Updated dependencies [361bd5b]
-- Updated dependencies [aac90a5]
-- Updated dependencies [3da3da5]
-- Updated dependencies [1e6ab15]
-- Updated dependencies [b90086a]
-- Updated dependencies [8186a70]
-- Updated dependencies [a329cca]
-- Updated dependencies [c87ef70]
-- Updated dependencies [3cb0618]
-- Updated dependencies [32a0874]
-- Updated dependencies [6eec18c]
-- Updated dependencies [4d7bebf]
-- Updated dependencies [821ac7a]
-- Updated dependencies [8f81731]
-- Updated dependencies [7055c22]
-- Updated dependencies [785a748]
-- Updated dependencies [3af0354]
-- Updated dependencies [866ff16]
-- Updated dependencies [5a85e67]
-- Updated dependencies [8b50cb3]
-- Updated dependencies [a0fdc56]
-- Updated dependencies [b95577a]
-- Updated dependencies [0dcbc11]
-- Updated dependencies [d88f3e9]
-- Updated dependencies [ad5fe25]
-- Updated dependencies [c183a12]
-- Updated dependencies [83c161f]
-- Updated dependencies [d8c4957]
-- Updated dependencies [b9f930b]
-- Updated dependencies [f24cb83]
-- Updated dependencies [5dbbb92]
-- Updated dependencies [ea90179]
-- Updated dependencies [1818998]
-- Updated dependencies [ce92674]
-- Updated dependencies [5ef0b5b]
-- Updated dependencies [8c2db68]
-- Updated dependencies [22b5e54]
-- Updated dependencies [0166bd5]
-- Updated dependencies [8064b07]
-- Updated dependencies [09ee21c]
-- Updated dependencies [4a56dbd]
-- Updated dependencies [289d04a]
-- Updated dependencies [f549a0d]
-- Updated dependencies [48fbacb]
-- Updated dependencies [06df4fa]
-- Updated dependencies [3fc2e48]
-- Updated dependencies [c9b809f]
-- Updated dependencies [e8f435c]
-- Updated dependencies [32386f8]
-- Updated dependencies [9b702dc]
-- Updated dependencies [ab16331]
-- Updated dependencies [41610f6]
-- Updated dependencies [69f1dfd]
-- Updated dependencies [bbe05de]
-- Updated dependencies [355e951]
-- Updated dependencies [a1dd1e4]
-- Updated dependencies [dadb43f]
-- Updated dependencies [3556b67]
- - @objectstack/spec@17.0.0
- - @objectstack/core@17.0.0
- - @objectstack/platform-objects@17.0.0
-
-## 17.0.0-rc.6
-
-### Patch Changes
-
-- f40c5b4: refactor(plugin-approvals,plugin-reports): enforcement implementations annotate the full `ExecutionContext` (#7135)
-
- The services half of #7070, mirroring what PR #7140 did for
- `plugin-sharing` / `plugin-audit`. #6523 converged 36 contract signatures onto
- the complete `resolveAuthzContext` envelope, applying the #6206 ruling —
- enforcement adjudicates on the whole envelope, never a per-site subset. The
- implementations behind those contracts still annotated their own parameters
- with the six-field shape the contracts used to name, so nothing they could
- _read_ had widened.
-
- `ApprovalService`, the approval flow-node provider and `ReportService` now
- declare `ExecutionContext` on all 43 of those positions, and the casts the
- narrow annotation forced are gone:
-
- - `isOverrideActor()` read the derived `posture` (ADR-0095) through an
- unchecked `(context as any)`. That gate decides whether a platform or tenant
- admin may release a STUCK approval — one routed to an unstaffed position, the
- only in-product recovery from a permanently locked record — so an erasure sat
- directly on an enforcement input: a mistyped rung would have compiled and
- silently denied every override. It is a declared read now.
- - Both services' `SYSTEM_CTX` is typed as the envelope and passed as itself,
- retiring the `SYSTEM_CTX as unknown as …` double casts at the three sites
- that hand it to a contract method.
- - The `(context as any).userId` / `.tenantId` reads in `ApprovalService` now
- read declared fields.
- - `OwnerContextResolver` returns the envelope, which is what a scheduled report
- actually resolves for its owner (#2849 / #2980).
-
- **No runtime behaviour changes.** The values were always complete — this
- family's damage was type-side — so every gate answers exactly what it answered
- before. Method parameters only WIDEN what they accept, so no caller is
- affected, and no public export changes shape.
-
- Casts deliberately kept, and now documented where they sit: `organizationId`
- is not a field of the envelope at all — that spelling has its own history
- (#5858 / `check:org-identifier`) and was held out of this change by #7070. In
- `approval-node.ts` the single remaining assertion exists only because the
- literal names that key; it was reduced from `as unknown as …` to a single
- `as ExecutionContext`, which still requires the literal to be comparable to
- the envelope.
-
- Because a re-narrowed annotation would compile, ship and pass every test in
- these packages, the convergence is pinned by a new compile-time module per
- package, `exec-context-annotation.pin.ts`: it hands each parameter a fresh
- literal naming envelope-only fields (`posture`, `accessible_org_ids`,
- `org_user_ids`), which TypeScript's excess-property check rejects the moment a
- parameter narrows back, plus negative cases so a parameter erased to `any`
- cannot pass either.
-
- The exported `SharingExecutionContext` type itself is NOT removed here: it is
- defined in `packages/spec`, which is single-owner, so its retirement is a
- separate follow-up.
-
-- d0d5205: refactor(core,plugin-audit,service-storage,plugin-reports): give the `__` operation-private-key convention a single owner (#7284)
-
- `withoutOperationPrivateKeys` — the rule that a consumer forwarding a caller's
- execution envelope to a question about a DIFFERENT object must first drop the
- `__`-prefixed keys plugin-security stamped for the operation in flight — had been
- hand-copied into three packages: `plugin-audit`'s comment access hooks (#7141),
- `service-storage`'s attachment access hooks (#7145) and `plugin-reports`' report
- service (#7204). Each carried its own `OPERATION_PRIVATE_KEY_PREFIX` and its own
- doc block, and the prose had already diverged while the code still agreed — the
- shape that makes a later divergence in behaviour hard to notice.
-
- The helper now lives once, in `@objectstack/core`
- (`security/operation-private-keys.ts`), exported from the package root. Core is
- the only candidate all three consumers already depend on: `plugin-security` is
- the producer of the convention and the most honest owner, but none of the three
- depends on it and a string-prefix filter does not justify three new dependency
- edges onto a plugin; `@objectstack/spec` is fenced off by Prime Directive #2. The
- new home sits beside `assemble-execution-context.ts`, which owns the other end of
- the same lifecycle — that file is where an `ExecutionContext` is built at a
- transport entry point, this one is where it is stripped back down before being
- forwarded.
-
- The full reasoning moved with the code rather than being thinned: which keys the
- middleware stamps and why each is a widening input, why they are dropped by
- PREFIX and never by a name list, and why the fresh copy is load-bearing in both
- directions. Each consumer keeps only its own local half — which object _its_
- gates actually ask about — and points at the shared home.
-
- No behaviour change: the three copies were byte-equivalent, and all three
- packages' suites pass unchanged. Two new pins at the home cover it — the rule's
- own behaviour, which no package-level test had ever asserted directly, and a
- repository-shape pin that turns red if a fourth file declares its own copy.
-
-- cb466aa: Reports read with the caller's whole execution envelope, so a `group`-posture report no longer under-reports
-
- `executeReport` rebuilt a five-field projection of the caller's `ExecutionContext`
- (`userId` / `tenantId` / `positions` / `permissions` / `isSystem`) before handing it to
- the engine read that produces the report — while the method's own comment promised
- "reports execute with the caller's identity".
-
- **Before.** `accessible_org_ids` was not in that projection, and the engine reads it by
- name (`buildDriverOptions`, ADR-0105 D2 / #3623) to widen the driver's native tenant
- scope to the caller's whole membership set under the `group` tenancy posture. Absent, the
- drivers fall back to active-org equality — "fail toward isolation". So the identical query
- returned the membership union in an interactive list view and collapsed to the active org
- inside a **saved or scheduled** report: silently short rows, no error, nothing in the
- output saying so. Measured end-to-end on a real kernel + SQL driver: three rows across two
- member orgs came back as three interactively and two in the report, and a scheduled CSV
- digest emailed the owner the same two. `timezone` went the same way, so a read-time
- formula field resolved its calendar day in UTC instead of the caller's business timezone;
- `posture`, `org_user_ids`, `systemPermissions` and `onBehalfOf` were dropped too.
-
- **After.** The read receives the caller's envelope whole (the #6206 ruling — enforcement
- adjudicates on the whole `resolveAuthzContext` envelope, never a per-site subset), minus
- the `__`-prefixed keys plugin-security stamps for the operation in flight, and as a fresh
- object so a callee's stamp cannot write back into the caller's request context. The same
- shape `plugin-audit` (#7141) and `service-storage` (#7145) landed. Direction is unchanged
- outside `group`: the `isolated` posture, a deployment with no posture provider, and a
- `group` caller with an empty accessible set all still read at active-org equality.
-
-- 2c2a212: fix(reports): owner-gate the saved-report schedule routes (#2980)
-
- The report read/run/delete routes are owner-isolated (a caller may only touch a
- report they own, denied as `REPORT_NOT_FOUND` to avoid leaking that the id
- exists), but the two schedule routes bypassed that gate: `unscheduleReport` and
- `listSchedules` took the caller `context` as `_context` and never consulted it,
- querying under the system context (RLS-bypassing). Any authenticated caller
- could therefore delete another owner's report schedule — a cross-owner
- destructive write — or list another owner's schedules (leaking recipient
- addresses and cron), by supplying an id.
-
- Both now resolve the schedule's parent report and require the caller to own it,
- mirroring the sibling routes:
-
- - **`unscheduleReport`** loads the schedule, then its report, and deletes only
- when `canAccessReport` holds; a cross-owner attempt throws `REPORT_NOT_FOUND`
- (mapped to `404` by the REST layer, deny-as-404 anti-enumeration), while a
- genuinely-absent schedule stays idempotent. `scheduleReport` (create) was
- already gated via `getReport`, so only the delete/list doors were open.
- - **`listSchedules`** returns an empty list to any non-system caller who cannot
- access the report it is scoped to — the same non-leaking posture as
- `listReports`. The scheduler's system context still sees every schedule.
-
- No authoring-surface or metadata change; existing owner-path behavior is
- unchanged.
-
-- dadd1ad: refactor(spec,plugin-sharing): retire the exported `SharingExecutionContext` type (#7218)
-
-
-
- **BREAKING — public surface removal.** `SharingExecutionContext` is deleted from
- `@objectstack/spec` (`contracts/sharing-service`) and from
- `@objectstack/plugin-sharing`, which re-exported it. Both `api-surface/` and
- `export-origins/` snapshots are regenerated accordingly.
-
- This is the deferred deletion recorded when #7070 split the convergence in two.
- #6523 / PR #7068 converged 36 contract signatures onto the full
- `resolveAuthzContext` envelope (`ExecutionContext`), applying the #6206 ruling —
- enforcement adjudicates on the whole envelope, never a per-site subset. The
- consumer halves then re-annotated the implementations: PR #7140 (identity:
- `plugin-sharing`, `plugin-audit`) and PR #7206 (services: `plugin-approvals`,
- `plugin-reports`). Both landed with the type still exported, because it is
- DEFINED in `packages/spec` and that package's retirement is the spec seat's to
- make. Nothing declares it any more, so it goes.
-
- **Migration.** Anyone who imported `SharingExecutionContext` from either package
- should import `ExecutionContext` from `@objectstack/spec` instead — the type the
- contracts have declared since #7068. The old shape was six optional fields, all
- of which exist on the envelope with the same names and types, so a value that
- satisfied the retired type already satisfies `ExecutionContext`; only the
- spelling of the annotation changes.
-
- **No runtime behaviour changes.** The type was erased at compile time and no
- signature's accepted shape moved: the contracts already took the wide envelope.
-
- **What the retirement did NOT remove — the reason to read the pins.** Deleting
- the type does not make re-narrowing a compile error. Structural subtyping still
- accepts a six-field context where the envelope is expected, so the boundary is
- held by the declared parameter type plus the pins, exactly as before. The three
- `exec-context-annotation.pin.ts` files (`plugin-sharing`, `plugin-approvals`,
- `plugin-reports`) told their failure story as "the parameter narrows back to
- `SharingExecutionContext`", which a deletion would have quietly hollowed out.
- Each now keeps the retired six-field shape as a local, non-exported SPECIMEN
- type and refutes every enforcement parameter against it by type identity, so a
- re-narrowing under ANY name is red — alongside the fresh-literal
- excess-property checks they already carried. `sharing-service.test.ts` in
- `packages/spec` is re-anchored the same way, and its "twin unchanged in shape"
- case becomes a "twin stays retired" case. The narrative the retired type's doc
- block carried (the measured `(context as any).posture` specimen, and why tsc
- cannot police this) moves to the module doc of `contracts/sharing-service`,
- which the contracts and pins now point at.
-
-- 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
-
- Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
- roots. 35 lookup sites that had been erased to `any` now carry the slot's
- contract, so the compiler checks what each plugin actually calls on the service
- it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
-
- **Two real defects, both of the shape this sweep exists to find.** Approvals'
- actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
- read the HTTP server under `http-server` _only_ — the deprecated alias. The
- ledger records `http.server` as canonical and as the only name present on every
- provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
- that path both lookups threw, the surrounding `catch` swallowed it, and the
- routes silently never mounted — approval e-mail action links 404'd and the
- share-link surface was absent, with nothing in the log to say so. Both reads are
- now canonical-first with the alias as fallback, each name in its own `try`
- because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
- never reaches `b` — the same correction #4393 made in metadata and
- cloud-connection).
-
- Typing choices follow the batch method: pure data-plane consumers take the
- narrow contract (`IDataEngine` in reports), consumers that bind hook or
- middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
- sharing and pinyin-search), and slots with no contract get a **named** local
- surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
- surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
- `SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
- still makes the compiler name every call site; `any` says nothing.
-
- No behaviour change beyond the two alias reads. No contract changes.
-
-- Updated dependencies [3d5c090]
-- Updated dependencies [e5bd768]
-- Updated dependencies [e027b3e]
-- Updated dependencies [c2429b0]
-- Updated dependencies [445a0c2]
-- Updated dependencies [f6609e6]
-- Updated dependencies [a70358a]
-- Updated dependencies [97e7e3c]
-- Updated dependencies [8828b9e]
-- Updated dependencies [53068c1]
-- Updated dependencies [ee58392]
-- Updated dependencies [f16e54e]
-- Updated dependencies [06be54e]
-- Updated dependencies [259459d]
-- Updated dependencies [3f7f14e]
-- Updated dependencies [6968885]
-- Updated dependencies [eaed61f]
-- Updated dependencies [debe2f6]
-- Updated dependencies [97b0798]
-- Updated dependencies [5fa04fb]
-- Updated dependencies [43a7a8d]
-- Updated dependencies [73f69dc]
-- Updated dependencies [04c56aa]
-- Updated dependencies [b3efeb7]
-- Updated dependencies [ddd075a]
-- Updated dependencies [88154be]
-- Updated dependencies [e8dc61e]
-- Updated dependencies [2f3e793]
-- Updated dependencies [d8e8d9c]
-- Updated dependencies [94e749b]
-- Updated dependencies [ea1d916]
-- Updated dependencies [ae31a19]
-- Updated dependencies [e0f300b]
-- Updated dependencies [62b6a2f]
-- Updated dependencies [5b4780b]
-- Updated dependencies [a933452]
-- Updated dependencies [8140915]
-- Updated dependencies [7b48cf9]
-- Updated dependencies [b5404f4]
-- Updated dependencies [f764691]
-- Updated dependencies [e120a5a]
-- Updated dependencies [e650d67]
-- Updated dependencies [04476e7]
-- Updated dependencies [79228cd]
-- Updated dependencies [b3363e9]
-- Updated dependencies [2ef1807]
-- Updated dependencies [d03fe25]
-- Updated dependencies [2672f85]
-- Updated dependencies [11066f6]
-- Updated dependencies [916af17]
-- Updated dependencies [84c86fb]
-- Updated dependencies [2a2a9fb]
-- Updated dependencies [a2e157c]
-- Updated dependencies [95c4227]
-- Updated dependencies [2a61116]
-- Updated dependencies [d4df105]
-- Updated dependencies [e2798fa]
-- Updated dependencies [0fd8556]
-- Updated dependencies [74155c7]
-- Updated dependencies [6908830]
-- Updated dependencies [8b06bba]
-- Updated dependencies [4c54037]
-- Updated dependencies [0f7157b]
-- Updated dependencies [d9bef45]
-- Updated dependencies [f549a0d]
-- Updated dependencies [82da264]
-- Updated dependencies [f586f1a]
-- Updated dependencies [9b9b70f]
-- Updated dependencies [f5a9bc2]
-- Updated dependencies [881a3cc]
-- Updated dependencies [ad6317b]
-- Updated dependencies [8a88885]
-- Updated dependencies [5f7669e]
-- Updated dependencies [becbe53]
-- Updated dependencies [b127c8b]
-- Updated dependencies [a80302a]
-- Updated dependencies [474f131]
-- Updated dependencies [050cd82]
-- Updated dependencies [4d552af]
-- Updated dependencies [44d677c]
-- Updated dependencies [c32944d]
-- Updated dependencies [1dd780f]
-- Updated dependencies [c8d6f6e]
-- Updated dependencies [92a67f2]
-- Updated dependencies [9136327]
-- Updated dependencies [bf0ae99]
-- Updated dependencies [cb3b6cd]
-- Updated dependencies [73b7234]
-- Updated dependencies [d2b97c3]
-- Updated dependencies [59b794f]
-- Updated dependencies [fc3a36a]
-- Updated dependencies [69787f0]
-- Updated dependencies [5d022a1]
-- Updated dependencies [042b9ee]
-- Updated dependencies [f549a0d]
-- Updated dependencies [a36db28]
-- Updated dependencies [3f8817a]
-- Updated dependencies [a2443e3]
-- Updated dependencies [e1554b1]
-- Updated dependencies [4856789]
-- Updated dependencies [c3f4916]
-- Updated dependencies [33e0385]
-- Updated dependencies [2205363]
-- Updated dependencies [09fe58d]
-- Updated dependencies [d0a5ceb]
-- Updated dependencies [e18a162]
-- Updated dependencies [d6d1a50]
-- Updated dependencies [d127ff0]
-- Updated dependencies [9b86cf6]
-- Updated dependencies [8825a06]
-- Updated dependencies [5087ac6]
-- Updated dependencies [2d1ddf0]
-- Updated dependencies [354b00f]
-- Updated dependencies [3de535b]
-- Updated dependencies [fe2e15a]
-- Updated dependencies [c6b6bb4]
-- Updated dependencies [59c544d]
-- Updated dependencies [2f59da0]
-- Updated dependencies [8ad609c]
-- Updated dependencies [bbee302]
-- Updated dependencies [08863dd]
-- Updated dependencies [56664f5]
-- Updated dependencies [31cbe90]
-- Updated dependencies [90bbf25]
-- Updated dependencies [eb91eba]
-- Updated dependencies [42da73d]
-- Updated dependencies [643b7c7]
-- Updated dependencies [d0d5205]
-- Updated dependencies [1a15893]
-- Updated dependencies [b70e534]
-- Updated dependencies [2233a85]
-- Updated dependencies [62dd69a]
-- Updated dependencies [e15e679]
-- Updated dependencies [2ab1257]
-- Updated dependencies [4cc4fb7]
-- Updated dependencies [28d1eb7]
-- Updated dependencies [2c26040]
-- Updated dependencies [f758cec]
-- Updated dependencies [78f0be8]
-- Updated dependencies [35f7fb4]
-- Updated dependencies [a5302c7]
-- Updated dependencies [7084313]
-- Updated dependencies [0e043d8]
-- Updated dependencies [dadd1ad]
-- Updated dependencies [2f2e63c]
-- Updated dependencies [486d526]
-- Updated dependencies [89d7b35]
-- Updated dependencies [85ec26d]
-- Updated dependencies [f6476fc]
-- Updated dependencies [4ac12ef]
-- Updated dependencies [b88f5e8]
-- Updated dependencies [42cc219]
-- Updated dependencies [d42a92f]
-- Updated dependencies [51d74ad]
-- Updated dependencies [d7e0b42]
-- Updated dependencies [3510e4a]
-- Updated dependencies [aa4b90d]
-- Updated dependencies [54299ca]
-- Updated dependencies [dc61def]
-- Updated dependencies [251e888]
-- Updated dependencies [183b4c4]
-- Updated dependencies [2fdb36e]
-- Updated dependencies [e787608]
-- Updated dependencies [20526f5]
-- Updated dependencies [c5eef1d]
-- Updated dependencies [e0f300b]
-- Updated dependencies [761a0ba]
-- Updated dependencies [61282f9]
-- Updated dependencies [be87153]
-- Updated dependencies [60f0dd8]
-- Updated dependencies [a87c5cd]
-- Updated dependencies [a47f338]
-- Updated dependencies [2598216]
-- Updated dependencies [2c7e62d]
-- Updated dependencies [eb7613c]
-- Updated dependencies [ecc9110]
-- Updated dependencies [f7bd4e2]
-- Updated dependencies [361bd5b]
-- Updated dependencies [1818998]
-- Updated dependencies [09ee21c]
-- Updated dependencies [f549a0d]
-- Updated dependencies [3fc2e48]
-- Updated dependencies [e8f435c]
-- Updated dependencies [41610f6]
- - @objectstack/spec@17.0.0-rc.6
- - @objectstack/platform-objects@17.0.0-rc.6
- - @objectstack/core@17.0.0-rc.6
-
-## 17.0.0-rc.5
-
-### Patch Changes
-
-- Updated dependencies [e8f8f6c]
-- Updated dependencies [7f713b6]
-- Updated dependencies [c960170]
-- Updated dependencies [def5919]
-- Updated dependencies [ce0cfe9]
-- Updated dependencies [1363084]
- - @objectstack/spec@17.0.0-rc.5
- - @objectstack/core@17.0.0-rc.5
- - @objectstack/platform-objects@17.0.0-rc.5
-
-## 17.0.0-rc.4
-
-### Patch Changes
-
-- Updated dependencies [9fe9c1d]
-- Updated dependencies [d4e0809]
-- Updated dependencies [f724f69]
-- Updated dependencies [28ad90e]
-- Updated dependencies [f8644c7]
-- Updated dependencies [306ca50]
-- Updated dependencies [978fed2]
-- Updated dependencies [cfc293f]
-- Updated dependencies [de70b42]
-- Updated dependencies [fb3d99b]
-- Updated dependencies [cdfbee2]
-- Updated dependencies [29c6c9d]
-- Updated dependencies [d21c001]
-- Updated dependencies [f1cc3a3]
-- Updated dependencies [ddc2527]
-- Updated dependencies [553a47f]
-- Updated dependencies [a3a884d]
-- Updated dependencies [cfed092]
-- Updated dependencies [2e284b2]
-- Updated dependencies [1b49eaf]
-- Updated dependencies [0161c7f]
-- Updated dependencies [e900015]
-- Updated dependencies [b5bdf48]
-- Updated dependencies [a019e52]
-- Updated dependencies [64fc6d5]
-- Updated dependencies [b746aa0]
-- Updated dependencies [947d4f9]
-- Updated dependencies [eaaf03c]
-- Updated dependencies [d17df80]
-- Updated dependencies [7d0e7b5]
-- Updated dependencies [6513c17]
-- Updated dependencies [c142ced]
-- Updated dependencies [eda599e]
-- Updated dependencies [c001422]
-- Updated dependencies [77022a9]
-- Updated dependencies [52760bf]
-- Updated dependencies [5543020]
-- Updated dependencies [880d343]
-- Updated dependencies [6e82972]
-- Updated dependencies [4615a18]
-- Updated dependencies [7f62706]
-- Updated dependencies [667fa44]
-- Updated dependencies [37e38d1]
-- Updated dependencies [1eb13a0]
-- Updated dependencies [c52e608]
-- Updated dependencies [4dfd002]
-- Updated dependencies [77be690]
-- Updated dependencies [811c30c]
-- Updated dependencies [b49ccfd]
-- Updated dependencies [85d95e7]
-- Updated dependencies [168f60f]
-- Updated dependencies [244ca86]
-- Updated dependencies [546ab3c]
-- Updated dependencies [0b51bb6]
-- Updated dependencies [d9971d3]
-- Updated dependencies [eb3e650]
-- Updated dependencies [abeb375]
-- Updated dependencies [ef4efa8]
-- Updated dependencies [cbb6a5c]
-- Updated dependencies [795b6e1]
-- Updated dependencies [175d789]
-- Updated dependencies [55dbbba]
-- Updated dependencies [72c3c86]
-- Updated dependencies [7f1a635]
-- Updated dependencies [e98fb14]
-- Updated dependencies [0f2fdcd]
-- Updated dependencies [8ffa8b9]
-- Updated dependencies [674ac99]
-- Updated dependencies [1b9a53b]
-- Updated dependencies [502564d]
-- Updated dependencies [471839d]
-- Updated dependencies [46365ab]
-- Updated dependencies [b508244]
-- Updated dependencies [594508e]
-- Updated dependencies [1c625ca]
-- Updated dependencies [71f205d]
-- Updated dependencies [414395b]
-- Updated dependencies [c5adfe1]
-- Updated dependencies [26e1029]
-- Updated dependencies [108ba8d]
-- Updated dependencies [b4ad984]
-- Updated dependencies [a9f32df]
-- Updated dependencies [aeb9b27]
-- Updated dependencies [7d27da0]
-- Updated dependencies [089767f]
-- Updated dependencies [e4c8b6c]
-- Updated dependencies [acb10f6]
-- Updated dependencies [1c3da1f]
-- Updated dependencies [a34fd2e]
-- Updated dependencies [889ae47]
-- Updated dependencies [4f4c3fb]
-- Updated dependencies [7adc841]
-- Updated dependencies [4845f85]
-- Updated dependencies [7b005b4]
-- Updated dependencies [94f7b6a]
-- Updated dependencies [5c94f83]
-- Updated dependencies [73e576f]
-- Updated dependencies [c5a5996]
-- Updated dependencies [ae490ef]
-- Updated dependencies [f61c8cf]
-- Updated dependencies [e3ef52b]
-- Updated dependencies [07f1822]
-- Updated dependencies [04fab5e]
-- Updated dependencies [efedd28]
-- Updated dependencies [5278e11]
-- Updated dependencies [23dba62]
-- Updated dependencies [ba98e26]
-- Updated dependencies [f104bab]
-- Updated dependencies [fc5f536]
-- Updated dependencies [f8cfbb4]
-- Updated dependencies [c89d18c]
-- Updated dependencies [aac90a5]
-- Updated dependencies [1e6ab15]
-- Updated dependencies [c87ef70]
-- Updated dependencies [3cb0618]
-- Updated dependencies [32a0874]
-- Updated dependencies [7055c22]
-- Updated dependencies [785a748]
-- Updated dependencies [3af0354]
-- Updated dependencies [866ff16]
-- Updated dependencies [5a85e67]
-- Updated dependencies [c183a12]
-- Updated dependencies [8064b07]
-- Updated dependencies [4a56dbd]
-- Updated dependencies [06df4fa]
- - @objectstack/spec@17.0.0-rc.4
- - @objectstack/core@17.0.0-rc.4
- - @objectstack/platform-objects@17.0.0-rc.4
-
-## 17.0.0-rc.2
-
-### Patch Changes
-
-- Updated dependencies [430dcc2]
-- Updated dependencies [e6ac4bd]
-- Updated dependencies [80334c7]
-- Updated dependencies [ce5242c]
-- Updated dependencies [a7163ea]
-- Updated dependencies [e6e9379]
-- Updated dependencies [98877c9]
-- Updated dependencies [98877c9]
-- Updated dependencies [c44dd5e]
-- Updated dependencies [e6b1b69]
-- Updated dependencies [ad047d2]
-- Updated dependencies [2826d1e]
-- Updated dependencies [5a84d41]
-- Updated dependencies [20b1a9e]
-- Updated dependencies [203a449]
-- Updated dependencies [ac37fc6]
-- Updated dependencies [4820f55]
-- Updated dependencies [462d9c4]
-- Updated dependencies [7d21581]
-- Updated dependencies [f2445c9]
-- Updated dependencies [23338c3]
-- Updated dependencies [5b843fb]
-- Updated dependencies [b4487aa]
-- Updated dependencies [65ca83a]
-- Updated dependencies [67bf2e2]
-- Updated dependencies [c6d1cb4]
-- Updated dependencies [36030ff]
-- Updated dependencies [6117f7b]
-- Updated dependencies [e533b0b]
-- Updated dependencies [cdf4d9a]
-- Updated dependencies [aee1806]
-- Updated dependencies [c13350b]
-- Updated dependencies [c13350b]
-- Updated dependencies [9ca2d85]
-- Updated dependencies [c13350b]
-- Updated dependencies [891d345]
-- Updated dependencies [a52e2ef]
-- Updated dependencies [5293114]
-- Updated dependencies [20bc357]
-- Updated dependencies [5966c2a]
-- Updated dependencies [2382580]
-- Updated dependencies [d9fa683]
-- Updated dependencies [3c7bcc0]
-- Updated dependencies [4b6cac7]
-- Updated dependencies [7631964]
-- Updated dependencies [ac471a0]
-- Updated dependencies [60ae58e]
-- Updated dependencies [ce92674]
-- Updated dependencies [9f601e8]
-- Updated dependencies [51c5227]
-- Updated dependencies [a4a85c8]
-- Updated dependencies [07a4e26]
-- Updated dependencies [ec975f1]
-- Updated dependencies [eb4204b]
-- Updated dependencies [4f13be2]
-- Updated dependencies [61cc079]
-- Updated dependencies [0e96e46]
-- Updated dependencies [d52d4fe]
-- Updated dependencies [742cebb]
-- Updated dependencies [ce92674]
-- Updated dependencies [cf2c9b7]
-- Updated dependencies [833b512]
-- Updated dependencies [0f9faa2]
-- Updated dependencies [7cf42fe]
-- Updated dependencies [5966c2a]
-- Updated dependencies [f78dd83]
-- Updated dependencies [a2cd18a]
-- Updated dependencies [4638aaa]
-- Updated dependencies [0222d3c]
-- Updated dependencies [071d0dc]
-- Updated dependencies [0a936ea]
-- Updated dependencies [023c00b]
-- Updated dependencies [155507e]
-- Updated dependencies [7bba90b]
-- Updated dependencies [7e05d8e]
-- Updated dependencies [061406d]
-- Updated dependencies [c1f344b]
-- Updated dependencies [9c93465]
-- Updated dependencies [ebb209c]
-- Updated dependencies [63b33e6]
-- Updated dependencies [2a44c1d]
-- Updated dependencies [695cfbd]
-- Updated dependencies [7445149]
-- Updated dependencies [071d0dc]
-- Updated dependencies [0848bea]
-- Updated dependencies [d51bed2]
-- Updated dependencies [b8b3c64]
-- Updated dependencies [0c0fbd9]
-- Updated dependencies [f3141d8]
-- Updated dependencies [5a84d41]
-- Updated dependencies [fd3013a]
-- Updated dependencies [21676eb]
-- Updated dependencies [e336549]
-- Updated dependencies [d40f43a]
-- Updated dependencies [e5e7ee0]
-- Updated dependencies [a2ebea2]
-- Updated dependencies [800bdb0]
-- Updated dependencies [04f1182]
-- Updated dependencies [5647006]
-- Updated dependencies [38f7e4f]
-- Updated dependencies [c57f3cf]
-- Updated dependencies [97faca3]
-- Updated dependencies [ad5fe25]
-- Updated dependencies [ea90179]
-- Updated dependencies [ce92674]
-- Updated dependencies [5ef0b5b]
-- Updated dependencies [48fbacb]
-- Updated dependencies [355e951]
-- Updated dependencies [dadb43f]
- - @objectstack/spec@17.0.0-rc.2
- - @objectstack/platform-objects@17.0.0-rc.2
- - @objectstack/core@17.0.0-rc.2
-
-## 17.0.0-rc.1
-
-### Patch Changes
-
-- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
-
- The AGENTS.md post-task checklist requires breaking changesets to carry their
- FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
- inside the npm package and is what an upgrading agent greps after the tombstone
- error." That delivery path was severed for 68 of the 69 publishable packages:
- npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
- older npm versions — not `CHANGELOG.md`, and the canonical
- `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
- 10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
- 70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
- explicitly.
-
- The tombstone-error scenario is precisely the one where the repo is out of
- reach — the upgrading agent has `node_modules` and nothing else — so the
- migration text has to ride in the tarball. Every publishable package now
- declares `CHANGELOG.md` in `files`, and the canonical whitelist is
- `["dist", "README.md", "CHANGELOG.md"]`.
-
- The other half is the gate: `check:published-files` gains a fifth invariant,
- COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
- always-required lint job, so the next package cannot silently sever the path
- again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
- into the canonical set.
-
- Consumer-visible change: one more file per install (the package's changelog,
- e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
-node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
- promised.
-
-- b5f9397: fix(sharing,runtime): a `sort` passed straight to the engine never ordered anything; migrate every in-repo engine call to canonical QueryAST keys (#4346)
-
- Two changes with different weights, from one sweep of every in-repo engine
- call site that still speaks a deprecated alias.
-
- **The bug — three dropped sorts.** #4346 made the engine fold `filter`→`where`
- and `top`→`limit` on all six methods. The other four pairs in
- `RPC_QUERY_ALIAS_SLOTS` (`select`, `sort`, `skip`, `populate`) are folded at
- the RPC/wire layer only — their values need shape lowering that belongs to
- those layers — and a **direct `engine.find()` never crosses that layer**. Three
- call sites passed `sort` there, so it rode onto the AST untouched, every
- driver's `Array.isArray(query.orderBy)` guard declined to emit an ORDER BY, and
- the query returned an ordinary-looking, arbitrarily-ordered result:
-
- | call site | asked for | actually got |
- | ----------------------------------- | ------------------------------------------------- | --------------------------- |
- | `share-link-routes.ts` | shared AI conversation messages, `created_at asc` | messages in arbitrary order |
- | `runtime/domains/share-links.ts` | same route, runtime-domain copy | same |
- | `share-link-service.ts` `listLinks` | the 200 most recent share links | an arbitrary 200 |
-
- All three combine the dropped sort with a `limit` — the "latest N" shape whose
- failure #4226 spelled out: an unapplied sort returns rows in arbitrary order,
- which `limit` then slices into an arbitrary page. #4226 fixed that in the wire
- normalizer; these calls sit one layer below it. `listLinks` had no test at all,
- which is why it went unnoticed. Now pinned — on the option bag the engine
- receives, not on row order, because the failure is that the key never becomes
- `orderBy` and a fake engine honouring either spelling would pass either way.
-
- **The cleanup — 27 no-op renames.** Every remaining in-repo engine call passing
- `filter` now passes `where` (approvals 5, auth 2, reports 6, sharing 11,
- webhooks 2, plus the one `filters` in a spec doc example). These are strict
- no-ops since #4346 folds the alias — the point is that the framework stops
- depending on a spelling it asks users to migrate off, which is a prerequisite
- for ever retiring the aliases. Service-level `filter` PARAMETERS (each
- service's own public API, e.g. `listRequests(filter)`) are deliberately
- untouched — those are not engine option bags.
-
- Two of the renamed calls were live victims of the #4346 bug rather than
- cosmetic: `auth-manager`'s `stampIdentitySource` read the table's first row via
- `findOne({filter})` and counted the whole table via `count({filter})`, so a
- federated sign-in never stamped `source: 'idp_provisioned'`. #4346 already
- corrected the behaviour; this makes the call say what it means.
-
-- cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
-
- These 20 packages declared no `files` field, so npm fell back to packing the
- whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
- listed **21 files** — 15 under `src/`, three of them unit tests
- (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
- build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
- publish time rather than instead of it, so consumers were installing the
- TypeScript sources and the test suite alongside the artifact they asked for.
-
- Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
- that already did. Nothing a consumer imports moves: every `main` / `types` /
- `exports` target in all 20 already resolved inside `dist/`, which the new
- `check:published-files` guard verifies rather than assumes. The visible change
- is a smaller install and a smaller dependency-scanning surface — `npm pack` on
- `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
-
- The other half of the fix is the gate. Half the packages declaring `files` and
- half not was the #3786 shape — a hand-copied convention with nothing enforcing
- it, where whoever forgets the line gets no signal at all. `check:published-files`
- (new, wired into the always-required `lint` job) holds every non-private
- workspace package to four invariants: `files` is **declared**; it is
- **sufficient** (covers every entry point, so tightening a whitelist cannot ship
- a package that fails to resolve); it is **minimal** (admits no test, test-harness
- config or build script); and anything beyond `dist` + `README.md` is
- **registered** with a reason, reconciled in both directions so a stale exemption
- is an error rather than dead text. `@objectstack/spec` is the one package with
- registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
- `CHANGELOG.md` are product, not build input.
-
- This also closes an assumption #4206 was resting on. Excluding `/scripts/**`
- from the docs-drift implementation test is sound only while no package publishes
- `scripts/` as runtime code; that held, but it held because someone read all three
- offenders by hand. It is now checked on every PR.
-
-- Updated dependencies [6a67d7a]
-- Updated dependencies [0ecc656]
-- Updated dependencies [06772eb]
-- Updated dependencies [270650f]
-- Updated dependencies [3aef718]
-- Updated dependencies [1ea6bce]
-- Updated dependencies [c1dcacd]
-- Updated dependencies [ad303ed]
-- Updated dependencies [32ccb23]
-- Updated dependencies [f5a4ef0]
-- Updated dependencies [2d3e255]
-- Updated dependencies [7d7521f]
-- Updated dependencies [5dc4d02]
-- Updated dependencies [05154a1]
-- Updated dependencies [9b6fe7c]
-- Updated dependencies [8c711fb]
-- Updated dependencies [09e4547]
-- Updated dependencies [91f4c78]
-- Updated dependencies [820eff9]
-- Updated dependencies [8d895ff]
-- Updated dependencies [f6472d7]
-- Updated dependencies [78caf51]
-- Updated dependencies [62a789b]
-- Updated dependencies [789ad63]
-- Updated dependencies [2af1988]
-- Updated dependencies [0af50a3]
-- Updated dependencies [2e836de]
-- Updated dependencies [12a19a8]
-- Updated dependencies [41dcda3]
-- Updated dependencies [c8124e5]
-- Updated dependencies [a1a4140]
-- Updated dependencies [217e2e6]
-- Updated dependencies [86a71d1]
-- Updated dependencies [d5c75e2]
-- Updated dependencies [03d26f7]
-- Updated dependencies [4384921]
-- Updated dependencies [3c628ce]
-- Updated dependencies [7cb922e]
-- Updated dependencies [1d22114]
-- Updated dependencies [b5f9397]
-- Updated dependencies [ed77493]
-- Updated dependencies [58a03d2]
-- Updated dependencies [dc530b4]
-- Updated dependencies [e59786e]
-- Updated dependencies [bcf1112]
-- Updated dependencies [9774b78]
-- Updated dependencies [b07d829]
-- Updated dependencies [a648e96]
-- Updated dependencies [a47ac06]
-- Updated dependencies [e4c61a7]
-- Updated dependencies [cc60165]
-- Updated dependencies [081aa6f]
-- Updated dependencies [91f4c78]
-- Updated dependencies [e8d0c21]
-- Updated dependencies [45dc446]
-- Updated dependencies [c1d44f7]
-- Updated dependencies [ab9fb5c]
-- Updated dependencies [f985b3f]
-- Updated dependencies [9a4932a]
-- Updated dependencies [f9fc874]
-- Updated dependencies [011b386]
-- Updated dependencies [7777e8f]
-- Updated dependencies [507b92a]
-- Updated dependencies [7309c81]
-- Updated dependencies [20bc1ec]
-- Updated dependencies [90c2b15]
-- Updated dependencies [42eeb7d]
-- Updated dependencies [01e124d]
-- Updated dependencies [7ce02eb]
-- Updated dependencies [a13827e]
-- Updated dependencies [7733604]
-- Updated dependencies [40e420f]
-- Updated dependencies [d13004a]
-- Updated dependencies [be7360c]
-- Updated dependencies [5b47ab5]
-- Updated dependencies [b09d8d9]
-- Updated dependencies [b09d8d9]
-- Updated dependencies [8675db6]
-- Updated dependencies [b09d8d9]
-- Updated dependencies [3eb1b2b]
-- Updated dependencies [59b85c0]
-- Updated dependencies [6e357ed]
-- Updated dependencies [d6938bf]
-- Updated dependencies [31e0be9]
-- Updated dependencies [4bfd455]
-- Updated dependencies [ffd2ce2]
-- Updated dependencies [62f8017]
-- Updated dependencies [a831df1]
-- Updated dependencies [f752ee3]
-- Updated dependencies [a1b61e0]
-- Updated dependencies [cd6b9f2]
-- Updated dependencies [2cb6d3c]
-- Updated dependencies [af2a095]
-- Updated dependencies [ec796d5]
-- Updated dependencies [e87fea1]
-- Updated dependencies [c65e529]
-- Updated dependencies [3ca34c1]
-- Updated dependencies [239c3a3]
-- Updated dependencies [94a0bbc]
-- Updated dependencies [d6bfb3d]
-- Updated dependencies [a2266a6]
-- Updated dependencies [d25a0ec]
-- Updated dependencies [667b83e]
-- Updated dependencies [627b188]
-- Updated dependencies [8d4eae7]
-- Updated dependencies [857a6cf]
-- Updated dependencies [65a3a84]
-- Updated dependencies [ccd9397]
-- Updated dependencies [bca935b]
-- Updated dependencies [d92c72d]
-- Updated dependencies [c54c822]
-- Updated dependencies [8dcc0f5]
-- Updated dependencies [75b9e51]
-- Updated dependencies [0a2f233]
-- Updated dependencies [8621cdd]
-- Updated dependencies [6f23667]
-- Updated dependencies [5d21a48]
-- Updated dependencies [19365b7]
-- Updated dependencies [b7ed26d]
-- Updated dependencies [68dea0b]
-- Updated dependencies [64f8cbe]
-- Updated dependencies [b3a3d83]
-- Updated dependencies [7a55913]
-- Updated dependencies [35accbf]
-- Updated dependencies [6038de7]
-- Updated dependencies [eb95d97]
-- Updated dependencies [e4c2dc8]
-- Updated dependencies [1bd2795]
-- Updated dependencies [8186a70]
-- Updated dependencies [a329cca]
-- Updated dependencies [6eec18c]
-- Updated dependencies [4d7bebf]
-- Updated dependencies [821ac7a]
-- Updated dependencies [8f81731]
-- Updated dependencies [8b50cb3]
-- Updated dependencies [8c2db68]
-- Updated dependencies [22b5e54]
-- Updated dependencies [0166bd5]
-- Updated dependencies [9b702dc]
-- Updated dependencies [ab16331]
- - @objectstack/spec@17.0.0-rc.1
- - @objectstack/platform-objects@17.0.0-rc.1
- - @objectstack/core@17.0.0-rc.1
-
-## 17.0.0-rc.0
-
-### Major Changes
-
-- 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710)
-
- **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a
- record and taking a bulk machine-readable copy of the whole table are different
- privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite
- "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so.
-
- ### Migration — FROM → TO
-
- | | before | after |
- | -------------------- | ----------------------------------- | -------------------------- |
- | `allowExport` unset | export **allowed** (inherited read) | export **denied** |
- | `allowExport: false` | export denied | export denied (unchanged) |
- | `allowExport: true` | export allowed | export allowed (unchanged) |
-
- **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'`
- wildcard) of every permission set whose holders should keep exporting.
-
- ```ts
- objects: {
- deal: { allowRead: true, allowExport: true }, // ← add the grant
- }
- ```
-
- Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set
- that never exported is unaffected.
-
- **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the
- built-ins are handled for you — `admin_full_access` and `organization_admin` now
- carry `allowExport: true` explicitly. **Environment-authored sets are not**: any
- custom set whose users export must be edited. `member_default` deliberately does
- NOT carry the grant, so ordinary authenticated users lose export until an admin
- grants it — that is the point of the flip, not an oversight.
-
- **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set
- granting `true` grants export. `false` and unset are the same outcome; `false`
- is authoring intent, not a veto, because permission sets are additive capability
- containers (ADR-0090).
-
- **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no
- longer confer export. Separating "may see all data" from "may take a bulk copy"
- is the segregation-of-duties case the axis exists for.
-
- ### Also in this change
-
- - **spec** — a set carrying `allowExport` is now **high-privilege**
- (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` /
- `guest` audience anchors. Without this the opt-in was defeatable by binding an
- export-granting set to `everyone`. One predicate, so the runtime anchor gate,
- the `@objectstack/lint` security-posture rule and the install-time suggestion
- surface all pick it up together.
- - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so
- `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It
- explains as `read ∧ the export grant`: `object_crud` reports the conjunction
- and attributes the granting set, while every data-shaped layer
- (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed
- as the `find` the export actually performs — asking the RLS compiler about an
- `export` operation would match no policy and wrongly report "no RLS applies".
- `readFilter` is surfaced for `export` as it is for `read`.
- - **plugin-reports** — closes the reports side door (#3710). A report rendered
- as `csv`/`json` is the same bulk copy of the same object, so it is gated by
- the same `ISecurityService.canExport`. Enforced in `executeReport`, which the
- interactive run, the ad-hoc run and the scheduled dispatch all funnel through;
- `scheduleReport` additionally refuses at create time so an author is not told
- at 3am. A schedule created while granted stops delivering once the grant is
- revoked. `html_table` stays a read — it is a rendered view, not a bulk copy.
- Deployments without `plugin-security` are unaffected (no permission sets
- exist, so the axis does not apply).
-
-### Patch Changes
-
-- Updated dependencies [50616d9]
-- Updated dependencies [08b5a3d]
-- Updated dependencies [d99aeb3]
-- Updated dependencies [4727eb8]
-- Updated dependencies [f63cd09]
-- Updated dependencies [fa3d0cf]
-- Updated dependencies [af5a224]
-- Updated dependencies [71f76e1]
-- Updated dependencies [37b1346]
-- Updated dependencies [99736a0]
-- Updated dependencies [fe67e34]
-- Updated dependencies [fdb4f50]
-- Updated dependencies [1bd5652]
-- Updated dependencies [14252d3]
-- Updated dependencies [7fb436c]
-- Updated dependencies [879ea13]
-- Updated dependencies [201b31f]
-- Updated dependencies [e2616e0]
-- Updated dependencies [6fdc5c6]
-- Updated dependencies [8b9d71e]
-- Updated dependencies [33f5e23]
-- Updated dependencies [259af21]
-- Updated dependencies [587fc91]
-- Updated dependencies [1986594]
-- Updated dependencies [ad4af62]
-- Updated dependencies [d44dbfa]
-- Updated dependencies [474fe39]
-- Updated dependencies [0bc685a]
-- Updated dependencies [b949059]
-- Updated dependencies [be1c52c]
-- Updated dependencies [c5ff96d]
-- Updated dependencies [84e7be9]
-- Updated dependencies [a6c3f38]
-- Updated dependencies [debc23a]
-- Updated dependencies [0f8ad09]
-- Updated dependencies [8f9689f]
-- Updated dependencies [57a3bb3]
-- Updated dependencies [5f9a987]
-- Updated dependencies [9f060e5]
-- Updated dependencies [bc17d39]
-- Updated dependencies [db02d47]
-- Updated dependencies [0bfdf46]
-- Updated dependencies [376a061]
-- Updated dependencies [7c7e246]
-- Updated dependencies [f35cdc5]
-- Updated dependencies [9ea2bc5]
-- Updated dependencies [c2d9098]
-- Updated dependencies [a227ed7]
-- Updated dependencies [9613396]
-- Updated dependencies [e47b342]
-- Updated dependencies [4ed7ed4]
-- Updated dependencies [2fa4ca1]
-- Updated dependencies [f5a2320]
-- Updated dependencies [deb538f]
-- Updated dependencies [5b89711]
-- Updated dependencies [0c8a22f]
-- Updated dependencies [763931e]
-- Updated dependencies [de9af8a]
-- Updated dependencies [c4df271]
-- Updated dependencies [a41ba5c]
-- Updated dependencies [189854c]
-- Updated dependencies [0e3a226]
-- Updated dependencies [524151c]
-- Updated dependencies [1d4756e]
-- Updated dependencies [720c5ad]
-- Updated dependencies [a8d1e24]
-- Updated dependencies [d1cabaa]
-- Updated dependencies [41642b0]
-- Updated dependencies [4cca74c]
-- Updated dependencies [88ef03e]
-- Updated dependencies [9e2caf3]
-- Updated dependencies [81ce41a]
-- Updated dependencies [85e1e4e]
-- Updated dependencies [dac6a08]
-- Updated dependencies [394b7a1]
-- Updated dependencies [677b591]
-- Updated dependencies [d77d1b7]
-- Updated dependencies [5b79a34]
-- Updated dependencies [c757854]
-- Updated dependencies [0045682]
-- Updated dependencies [2a5f04a]
-- Updated dependencies [4f740b0]
-- Updated dependencies [67452d1]
-- Updated dependencies [4921a95]
-- Updated dependencies [0fc6219]
-- Updated dependencies [605e190]
-- Updated dependencies [c6c59f1]
-- Updated dependencies [b0e78a8]
-- Updated dependencies [f31cc8d]
-- Updated dependencies [f343dc4]
-- Updated dependencies [8269e32]
-- Updated dependencies [74f7339]
-- Updated dependencies [a6c35a2]
-- Updated dependencies [c2f1002]
-- Updated dependencies [f163028]
-- Updated dependencies [f07808c]
-- Updated dependencies [7ffc3d3]
-- Updated dependencies [88346ba]
-- Updated dependencies [4631592]
-- Updated dependencies [32ff033]
-- Updated dependencies [5ac93d4]
-- Updated dependencies [93f267f]
-- Updated dependencies [0024abf]
-- Updated dependencies [acbf364]
-- Updated dependencies [5487c20]
-- Updated dependencies [aa8b847]
-- Updated dependencies [7687f7b]
-- Updated dependencies [1659072]
-- Updated dependencies [abceb0d]
-- Updated dependencies [0c302a7]
-- Updated dependencies [6633337]
-- Updated dependencies [f00d8d4]
-- Updated dependencies [503be86]
-- Updated dependencies [cde1975]
-- Updated dependencies [0bc685a]
-- Updated dependencies [11949fc]
-- Updated dependencies [b098b0e]
-- Updated dependencies [4d00b13]
-- Updated dependencies [9aa5510]
-- Updated dependencies [57bab76]
-- Updated dependencies [b90086a]
-- Updated dependencies [b95577a]
-- Updated dependencies [83c161f]
-- Updated dependencies [d8c4957]
-- Updated dependencies [f24cb83]
-- Updated dependencies [5dbbb92]
-- Updated dependencies [69f1dfd]
- - @objectstack/spec@17.0.0-rc.0
- - @objectstack/platform-objects@17.0.0-rc.0
- - @objectstack/core@17.0.0-rc.0
-
-## 16.1.0
-
-### Patch Changes
-
-- Updated dependencies [212b66a]
-- Updated dependencies [d10c4dc]
-- Updated dependencies [9e45b63]
-- Updated dependencies [b20201f]
- - @objectstack/platform-objects@16.1.0
- - @objectstack/spec@16.1.0
- - @objectstack/core@16.1.0
-
-## 16.0.0
-
-### Patch Changes
-
-- Updated dependencies [f972574]
-- Updated dependencies [6289ec3]
-- Updated dependencies [22013aa]
-- Updated dependencies [3ad3dd5]
-- Updated dependencies [8efa395]
-- Updated dependencies [3a18b60]
-- Updated dependencies [a8aa34c]
-- Updated dependencies [e057f42]
-- Updated dependencies [a3823b2]
-- Updated dependencies [bc65105]
-- Updated dependencies [43a3efb]
-- Updated dependencies [524696a]
-- Updated dependencies [bfa3c3f]
-- Updated dependencies [5e3301d]
-- Updated dependencies [dd9f223]
-- Updated dependencies [46e876c]
-- Updated dependencies [5f05de2]
-- Updated dependencies [021ba4c]
-- Updated dependencies [158aa14]
-- Updated dependencies [62a2117]
-- Updated dependencies [d2723e2]
-- Updated dependencies [fefcd54]
-- Updated dependencies [beaf2de]
-- Updated dependencies [369eb6e]
-- Updated dependencies [06ff734]
-- Updated dependencies [b659111]
-- Updated dependencies [5754a23]
-- Updated dependencies [6c270a6]
-- Updated dependencies [290e2f0]
-- Updated dependencies [668dd17]
-- Updated dependencies [8abf133]
-- Updated dependencies [e0859b1]
-- Updated dependencies [04ecd4e]
-- Updated dependencies [4d5a892]
-- Updated dependencies [16cebeb]
-- Updated dependencies [86d30af]
-- Updated dependencies [8923843]
-- Updated dependencies [a2795f6]
-- Updated dependencies [f16b492]
-- Updated dependencies [4b6fde8]
-- Updated dependencies [2018df9]
-- Updated dependencies [fc5a3a2]
-- Updated dependencies [8ff9210]
- - @objectstack/spec@16.0.0
- - @objectstack/platform-objects@16.0.0
- - @objectstack/core@16.0.0
-
-## 16.0.0-rc.1
-
-### Patch Changes
-
-- Updated dependencies [6289ec3]
-- Updated dependencies [8efa395]
-- Updated dependencies [bfa3c3f]
-- Updated dependencies [62a2117]
-- Updated dependencies [06ff734]
- - @objectstack/spec@16.0.0-rc.1
- - @objectstack/platform-objects@16.0.0-rc.1
- - @objectstack/core@16.0.0-rc.1
-
-## 16.0.0-rc.0
-
-### Patch Changes
-
-- Updated dependencies [f972574]
-- Updated dependencies [22013aa]
-- Updated dependencies [3ad3dd5]
-- Updated dependencies [3a18b60]
-- Updated dependencies [a8aa34c]
-- Updated dependencies [e057f42]
-- Updated dependencies [a3823b2]
-- Updated dependencies [bc65105]
-- Updated dependencies [43a3efb]
-- Updated dependencies [524696a]
-- Updated dependencies [5e3301d]
-- Updated dependencies [dd9f223]
-- Updated dependencies [46e876c]
-- Updated dependencies [5f05de2]
-- Updated dependencies [021ba4c]
-- Updated dependencies [158aa14]
-- Updated dependencies [d2723e2]
-- Updated dependencies [fefcd54]
-- Updated dependencies [beaf2de]
-- Updated dependencies [369eb6e]
-- Updated dependencies [b659111]
-- Updated dependencies [5754a23]
-- Updated dependencies [6c270a6]
-- Updated dependencies [290e2f0]
-- Updated dependencies [668dd17]
-- Updated dependencies [8abf133]
-- Updated dependencies [e0859b1]
-- Updated dependencies [04ecd4e]
-- Updated dependencies [4d5a892]
-- Updated dependencies [16cebeb]
-- Updated dependencies [86d30af]
-- Updated dependencies [8923843]
-- Updated dependencies [a2795f6]
-- Updated dependencies [f16b492]
-- Updated dependencies [4b6fde8]
-- Updated dependencies [2018df9]
-- Updated dependencies [fc5a3a2]
- - @objectstack/spec@16.0.0-rc.0
- - @objectstack/platform-objects@16.0.0-rc.0
- - @objectstack/core@16.0.0-rc.0
-
-## 15.1.1
-
-### Patch Changes
-
-- @objectstack/spec@15.1.1
-- @objectstack/core@15.1.1
-- @objectstack/platform-objects@15.1.1
-
-## 15.1.0
-
-### Patch Changes
-
-- f531a26: fix(security): close three execution-surface authz holes surfaced by the #2849 class sweep (#2980, #2981, #2982)
-
- Three independent, confirmed-exploitable defects where an execution surface
- ignored the caller's identity or fell open on a missing one. Each is fixed at
- its own enforcement point; none change behaviour for correctly-scoped callers.
-
- - **#2980 — reports IDOR + scheduled-report RLS bypass.** `ReportService`
- discarded the caller's context and read/wrote `sys_saved_report` with a system
- context, so any authenticated user could read, delete, or overwrite any saved
- report by id (cross-owner / cross-tenant), and `listReports` enumerated all
- owners. `getReport`/`deleteReport`/`saveReport`/`listReports` are now
- owner-scoped (system read of the protection-locked metadata object, but
- authorization enforced by owner match); create/overwrite can no longer spoof
- ownership. Scheduled dispatch no longer runs `isSystem` (which emailed the
- target object's entire table past the owner's RLS): it resolves the owner to a
- real RLS-bearing context via a new `resolveOwnerContext` seam and **fails
- closed** (skips + marks the schedule failed) when the owner can't be resolved,
- rather than running elevated. Wiring that resolver is the reports-surface
- consumer of ADR-0073's user-less identity resolution.
-
- - **#2981 — knowledge/RAG retrieval fall-open.** `applyPermissionFilter` returned
- every hit when the context was missing _or_ system. A missing identity is no
- longer treated as a grant: object-backed hits fail closed (dropped, keeping
- ACL-less file/http hits), and only an **explicit** system context passes
- through. Closes the agent path where an omitted `ToolExecutionContext.actor`
- yielded unfiltered semantic search over the whole corpus.
-
- - **#2982 — bulk-write OWD gap.** `update({multi:true})` / `deleteMany` had no
- single id to `canEdit`-gate, so owner scoping was skipped on private (and
- public_read) objects. A new `SharingService.buildWriteFilter` (the edit-set
- analogue of `buildReadFilter`) is AND-ed into the write AST for multi writes,
- constraining them to rows the caller may edit — including the on-behalf-of
- delegator intersection.
-
- Tracked as the motivating evidence of ADR-0096 (execution-surface identity
- admission); the mechanism that would prevent the class structurally is separate.
-
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [3fe9df1]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [4109153]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [627f225]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
-- Updated dependencies [f531a26]
- - @objectstack/spec@15.1.0
- - @objectstack/platform-objects@15.1.0
- - @objectstack/core@15.1.0
-
-## 15.0.0
-
-### Patch Changes
-
-- Updated dependencies [02a014b]
-- Updated dependencies [28b7c28]
-- Updated dependencies [13749ec]
-- Updated dependencies [e62c233]
-- Updated dependencies [ed61c9b]
-- Updated dependencies [31d04d4]
- - @objectstack/platform-objects@15.0.0
- - @objectstack/spec@15.0.0
- - @objectstack/core@15.0.0
-
-## 14.8.0
-
-### Patch Changes
-
-- 607aaf4: 导出文件名本地化 + 系统字段标签内置多语言回退。
-
- **`@objectstack/rest` — 导出下载文件名**:`GET /data/:object/export` 的 `Content-Disposition` 不再是裸的 `<对象名>.<扩展名>`,改为「对象显示名-时间戳」:ASCII 兜底用 API 名(`filename="contracts-20260714-153045.xlsx"`),本地化标签(如中文)按 RFC 5987/6266 编码进 `filename*=UTF-8''…`(浏览器直接下载得到 `合同-20260714-153045.xlsx`)。新增导出 `exportContentDisposition(objectName, label, ext, now?)`。
-
- **`@objectstack/spec` — 系统字段标签回退**:ObjectQL 注册表给每个对象注入的系统字段(`owner_id`/`created_at`/`created_by`/`updated_at`/`updated_by`)只带英文标签,自定义对象又没有对应的翻译条目,导致中文界面的列表表头、导出文件、导入模板里漏出 "Owner"/"Created At" 等英文。`translateObject` 现内置这五个字段的 en/zh-CN/ja-JP/es-ES 标签表(措辞与平台生成的翻译包一致),仅当字段仍是注入的英文默认值时套用——作者自定义的标签绝不覆盖;无翻译包时也生效(`translateObject` 不再因缺 bundle 而提前返回,REST 元数据翻译路径同步放宽,缓存 ETag 本就按 locale 分键,无缓存串味风险)。
-
- **`@objectstack/plugin-reports` — 附件文件名**:定时报表附件的文件名清洗从「非 ASCII 全部替换成 `_`」改为按 Unicode 字母/数字保留(`\p{L}\p{N}`),中文计划名不再变成一串下划线。
-
- **`@objectstack/rest` — 导入接受翻译后的选项标签(导出 ↔ 导入闭环)**:导出与导入模板写出的是*翻译后*的选项标签(如 `待规划`),但导入强制转换只认作者原始 schema 的标签/值,导致用户把自己刚导出的本地化文件原样导回时 select 字段全部报 `invalid_option`。`prepareImportRequest` 新增 `localizeSchema` 钩子(REST 导入路由传入 `translateMetaItem`),把当前 locale 的翻译标签合并进字段选项作为匹配同义词——作者标签与选项 code 照常匹配,非法值照常报错,翻译失败时降级为仅作者标签匹配。新增导出 `mergeLocalizedOptionSynonyms(metaMap, localizedMetaMap)`。
-
-- Updated dependencies [16b4bf6]
-- Updated dependencies [16b4bf6]
-- Updated dependencies [10e8983]
-- Updated dependencies [607aaf4]
-- Updated dependencies [bb71321]
- - @objectstack/spec@14.8.0
- - @objectstack/platform-objects@14.8.0
- - @objectstack/core@14.8.0
-
-## 14.7.0
-
-### Patch Changes
-
-- Updated dependencies [d6a72eb]
- - @objectstack/spec@14.7.0
- - @objectstack/core@14.7.0
- - @objectstack/platform-objects@14.7.0
-
-## 14.6.0
-
-### Patch Changes
-
-- Updated dependencies [609cb13]
-- Updated dependencies [ce6d151]
- - @objectstack/spec@14.6.0
- - @objectstack/platform-objects@14.6.0
- - @objectstack/core@14.6.0
-
-## 14.5.0
-
-### Patch Changes
-
-- Updated dependencies [526805e]
-- Updated dependencies [d79ca07]
-- Updated dependencies [33ebd34]
-- Updated dependencies [c044f08]
-- Updated dependencies [01274eb]
-- Updated dependencies [8f23746]
-- Updated dependencies [b97af7e]
-- Updated dependencies [6da03ee]
- - @objectstack/spec@14.5.0
- - @objectstack/platform-objects@14.5.0
- - @objectstack/core@14.5.0
-
-## 14.4.0
-
-### Patch Changes
-
-- Updated dependencies [7953832]
-- Updated dependencies [82e745e]
-- Updated dependencies [f3035bd]
-- Updated dependencies [82c0d94]
-- Updated dependencies [7449476]
- - @objectstack/spec@14.4.0
- - @objectstack/platform-objects@14.4.0
- - @objectstack/core@14.4.0
-
-## 14.3.0
-
-### Patch Changes
-
-- Updated dependencies [2a71f48]
-- Updated dependencies [02f6af4]
-- Updated dependencies [c1064f1]
- - @objectstack/platform-objects@14.3.0
- - @objectstack/spec@14.3.0
- - @objectstack/core@14.3.0
-
-## 14.2.0
-
-### Patch Changes
-
-- Updated dependencies [ac8f029]
-- Updated dependencies [4ab9958]
- - @objectstack/spec@14.2.0
- - @objectstack/platform-objects@14.2.0
- - @objectstack/core@14.2.0
-
-## 14.1.0
-
-### Patch Changes
-
-- Updated dependencies [5a8465f]
-- Updated dependencies [7f8620b]
-- Updated dependencies [82ba3a6]
- - @objectstack/spec@14.1.0
- - @objectstack/core@14.1.0
- - @objectstack/platform-objects@14.1.0
-
-## 14.0.0
-
-### Patch Changes
-
-- Updated dependencies [0a8e685]
-- Updated dependencies [afa8115]
-- Updated dependencies [80f12ca]
-- Updated dependencies [332b711]
-- Updated dependencies [e2fa074]
-- Updated dependencies [23c8668]
-- Updated dependencies [29f017d]
-- Updated dependencies [216fa9a]
-- Updated dependencies [6c22b12]
-- Updated dependencies [d0531c4]
-- Updated dependencies [cff5aac]
- - @objectstack/spec@14.0.0
- - @objectstack/platform-objects@14.0.0
- - @objectstack/core@14.0.0
-
-## 13.0.0
-
-### Patch Changes
-
-- Updated dependencies [6d83431]
-- Updated dependencies [01917c2]
-- Updated dependencies [b271691]
-- Updated dependencies [a5a1e41]
-- Updated dependencies [466adf6]
-- Updated dependencies [5be00c3]
-- Updated dependencies [466adf6]
-- Updated dependencies [2bee609]
-- Updated dependencies [9fa84f9]
-- Updated dependencies [fc7e7f7]
- - @objectstack/spec@13.0.0
- - @objectstack/core@13.0.0
- - @objectstack/platform-objects@13.0.0
-
-## 12.6.0
-
-### Patch Changes
-
-- Updated dependencies [6cebf22]
-- Updated dependencies [21420d9]
- - @objectstack/spec@12.6.0
- - @objectstack/core@12.6.0
- - @objectstack/platform-objects@12.6.0
-
-## 12.5.0
-
-### Patch Changes
-
-- Updated dependencies [8b3d363]
- - @objectstack/spec@12.5.0
- - @objectstack/core@12.5.0
- - @objectstack/platform-objects@12.5.0
-
-## 12.4.0
-
-### Patch Changes
-
-- Updated dependencies [60dc3ba]
- - @objectstack/spec@12.4.0
- - @objectstack/core@12.4.0
- - @objectstack/platform-objects@12.4.0
-
-## 12.3.0
-
-### Patch Changes
-
-- Updated dependencies [e7eceec]
- - @objectstack/spec@12.3.0
- - @objectstack/core@12.3.0
- - @objectstack/platform-objects@12.3.0
-
-## 12.2.0
-
-### Patch Changes
-
-- Updated dependencies [fce8ff4]
-- Updated dependencies [3962023]
-- Updated dependencies [2bb193d]
-- Updated dependencies [0426d27]
-- Updated dependencies [da807f7]
-- Updated dependencies [4f5b791]
- - @objectstack/spec@12.2.0
- - @objectstack/core@12.2.0
- - @objectstack/platform-objects@12.2.0
-
-## 12.1.0
-
-### Patch Changes
-
-- Updated dependencies [93e6d02]
- - @objectstack/spec@12.1.0
- - @objectstack/core@12.1.0
- - @objectstack/platform-objects@12.1.0
-
-## 12.0.0
-
-### Patch Changes
-
-- Updated dependencies [a8df396]
-- Updated dependencies [e695fe0]
-- Updated dependencies [07f055c]
-- Updated dependencies [7c09621]
-- Updated dependencies [7709db4]
-- Updated dependencies [2082109]
-- Updated dependencies [7c09621]
-- Updated dependencies [9860de4]
-- Updated dependencies [069c205]
- - @objectstack/spec@12.0.0
- - @objectstack/platform-objects@12.0.0
- - @objectstack/core@12.0.0
-
-## 11.10.0
-
-### Patch Changes
-
-- Updated dependencies [6a9397e]
-- Updated dependencies [c0efe5d]
- - @objectstack/spec@11.10.0
- - @objectstack/core@11.10.0
- - @objectstack/platform-objects@11.10.0
-
-## 11.9.0
-
-### Patch Changes
-
-- Updated dependencies [d3595d9]
- - @objectstack/spec@11.9.0
- - @objectstack/core@11.9.0
- - @objectstack/platform-objects@11.9.0
-
-## 11.8.0
-
-### Patch Changes
-
-- Updated dependencies [53d491a]
-- Updated dependencies [b84726b]
- - @objectstack/platform-objects@11.8.0
- - @objectstack/spec@11.8.0
- - @objectstack/core@11.8.0
-
-## 11.7.0
-
-### Patch Changes
-
-- Updated dependencies [5178906]
- - @objectstack/spec@11.7.0
- - @objectstack/platform-objects@11.7.0
- - @objectstack/core@11.7.0
-
-## 11.6.0
-
-### Patch Changes
-
-- @objectstack/spec@11.6.0
-- @objectstack/core@11.6.0
-- @objectstack/platform-objects@11.6.0
-
-## 11.5.0
-
-### Patch Changes
-
-- Updated dependencies [6ee4f04]
-- Updated dependencies [c1e3a65]
- - @objectstack/spec@11.5.0
- - @objectstack/core@11.5.0
- - @objectstack/platform-objects@11.5.0
-
-## 11.4.0
-
-### Patch Changes
-
-- Updated dependencies [5821c51]
-- Updated dependencies [a0fce3f]
- - @objectstack/spec@11.4.0
- - @objectstack/core@11.4.0
- - @objectstack/platform-objects@11.4.0
-
-## 11.3.0
-
-### Patch Changes
-
-- Updated dependencies [58e8e31]
-- Updated dependencies [b4a5df0]
- - @objectstack/spec@11.3.0
- - @objectstack/core@11.3.0
- - @objectstack/platform-objects@11.3.0
-
-## 11.2.0
-
-### Patch Changes
-
-- Updated dependencies [d0f4b13]
-- Updated dependencies [302bdab]
- - @objectstack/spec@11.2.0
- - @objectstack/core@11.2.0
- - @objectstack/platform-objects@11.2.0
-
-## 11.1.0
-
-### Patch Changes
-
-- Updated dependencies [cbc8c02]
-- Updated dependencies [07c2773]
-- Updated dependencies [d7a88df]
-- Updated dependencies [4f8f108]
-- Updated dependencies [ce0b4f6]
-- Updated dependencies [90bce88]
-- Updated dependencies [3209ec6]
-- Updated dependencies [e011d42]
-- Updated dependencies [6e5bdd5]
-- Updated dependencies [9ccfcd6]
-- Updated dependencies [ecf193f]
-- Updated dependencies [51bec81]
-- Updated dependencies [3e593a7]
-- Updated dependencies [63d5403]
- - @objectstack/platform-objects@11.1.0
- - @objectstack/core@11.1.0
- - @objectstack/spec@11.1.0
-
-## 11.0.0
-
-### Patch Changes
-
-- Updated dependencies [9b5bf3d]
-- Updated dependencies [cb5b393]
-- Updated dependencies [ab5718a]
-- Updated dependencies [4845c12]
-- Updated dependencies [c1a754a]
-- Updated dependencies [6fbe91f]
-- Updated dependencies [715d667]
-- Updated dependencies [5eef4cf]
-- Updated dependencies [72759e1]
-- Updated dependencies [6c4fbd9]
-- Updated dependencies [ef3ed67]
-- Updated dependencies [cd51229]
-- Updated dependencies [7697a0e]
-- Updated dependencies [e7e04f1]
-- Updated dependencies [cfd5ac4]
-- Updated dependencies [2be5c1f]
-- Updated dependencies [ad143ce]
-- Updated dependencies [5c4a8c8]
-- Updated dependencies [3afaeed]
-- Updated dependencies [5737261]
-- Updated dependencies [a619a3a]
-- Updated dependencies [f44c1bd]
-- Updated dependencies [8801c02]
-- Updated dependencies [3d04e06]
-- Updated dependencies [4a84c98]
-- Updated dependencies [c715d25]
-- Updated dependencies [aa33b02]
-- Updated dependencies [d980f0d]
-- Updated dependencies [a658523]
-- Updated dependencies [82ff91c]
-- Updated dependencies [638f472]
- - @objectstack/platform-objects@11.0.0
- - @objectstack/spec@11.0.0
- - @objectstack/core@11.0.0
-
-## 10.3.0
-
-### Patch Changes
-
-- @objectstack/spec@10.3.0
-- @objectstack/core@10.3.0
-- @objectstack/platform-objects@10.3.0
-
-## 10.2.0
-
-### Patch Changes
-
-- Updated dependencies [b496498]
- - @objectstack/spec@10.2.0
- - @objectstack/core@10.2.0
- - @objectstack/platform-objects@10.2.0
-
-## 10.1.0
-
-### Patch Changes
-
-- Updated dependencies [49da36e]
-- Updated dependencies [ac79f16]
- - @objectstack/spec@10.1.0
- - @objectstack/core@10.1.0
- - @objectstack/platform-objects@10.1.0
-
-## 10.0.0
-
-### Patch Changes
-
-- Updated dependencies [d7ff626]
-- Updated dependencies [2a1b16b]
-- Updated dependencies [2256e93]
-- Updated dependencies [7108ff3]
-- Updated dependencies [30c0313]
-- Updated dependencies [e16f2a8]
-- Updated dependencies [e411a82]
-- Updated dependencies [ae271d0]
-- Updated dependencies [61ed5c7]
-- Updated dependencies [a581385]
-- Updated dependencies [d5f6d29]
-- Updated dependencies [220ce5b]
-- Updated dependencies [3efe334]
-- Updated dependencies [0df063e]
-- Updated dependencies [ce13bb8]
-- Updated dependencies [feead7e]
-- Updated dependencies [6ca20b3]
-- Updated dependencies [5f875fe]
-- Updated dependencies [b469950]
-- Updated dependencies [47d978a]
- - @objectstack/spec@10.0.0
- - @objectstack/platform-objects@10.0.0
- - @objectstack/core@10.0.0
-
-## 9.11.0
-
-### Patch Changes
-
-- Updated dependencies [e7f6539]
-- Updated dependencies [2365d07]
-- Updated dependencies [6595b53]
-- Updated dependencies [fa8964d]
-- Updated dependencies [36138c7]
-- Updated dependencies [a8e4f3b]
-- Updated dependencies [4c213c2]
-- Updated dependencies [2afb612]
- - @objectstack/spec@9.11.0
- - @objectstack/core@9.11.0
- - @objectstack/platform-objects@9.11.0
-
-## 9.10.0
-
-### Patch Changes
-
-- Updated dependencies [db02bd5]
-- Updated dependencies [641675d]
-- Updated dependencies [94e9040]
-- Updated dependencies [4331adb]
-- Updated dependencies [1f88fd9]
-- Updated dependencies [1f88fd9]
- - @objectstack/spec@9.10.0
- - @objectstack/platform-objects@9.10.0
- - @objectstack/core@9.10.0
-
-## 9.9.1
-
-### Patch Changes
-
-- @objectstack/spec@9.9.1
-- @objectstack/core@9.9.1
-- @objectstack/platform-objects@9.9.1
-
-## 9.9.0
-
-### Minor Changes
-
-- d42004b: feat(reports): report schedules honor `cron_expression` + `timezone`
-
- `sys_report_schedule` has long carried `cron_expression` and `timezone` fields, but `ReportService` only ever advanced `next_run_at` by `interval_minutes` — both fields were stored and ignored. Scheduling now computes `next_run_at` from the cron expression (when present) in the schedule's timezone via `croner` — the same library the job scheduler uses — so "every weekday at 09:00 local" is expressible. `interval_minutes` remains the fallback.
-
- - `cron_expression` wins over `interval_minutes` when set (the field's documented contract).
- - Evaluated in `timezone` (default `UTC`).
- - `scheduleReport` validates the cron expression eagerly and rejects an invalid one with `VALIDATION_FAILED`, rather than silently falling back at sweep time. A cron that becomes unschedulable later is logged and falls back to the interval — it never throws into the dispatch sweep.
-
- The DB-polling dispatch model is unchanged; only the next-run computation is cron-aware. Part of ADR-0053 Phase 2 (#1983) but self-contained — report schedules run under a system context, so the timezone source is the schedule's own field, independent of the reference-timezone resolver.
-
-### Patch Changes
-
-- Updated dependencies [84249a4]
-- Updated dependencies [11af299]
-- Updated dependencies [d5774b5]
-- Updated dependencies [134043a]
-- Updated dependencies [90108e0]
-- Updated dependencies [9afeb2d]
-- Updated dependencies [6bec07e]
-- Updated dependencies [601cc11]
-- Updated dependencies [575448d]
- - @objectstack/spec@9.9.0
- - @objectstack/core@9.9.0
- - @objectstack/platform-objects@9.9.0
-
-## 9.8.0
-
-### Patch Changes
-
-- Updated dependencies [97c55b3]
-- Updated dependencies [1b1f490]
- - @objectstack/spec@9.8.0
- - @objectstack/core@9.8.0
- - @objectstack/platform-objects@9.8.0
-
-## 9.7.0
-
-### Patch Changes
-
-- @objectstack/spec@9.7.0
-- @objectstack/core@9.7.0
-- @objectstack/platform-objects@9.7.0
-
-## 9.6.0
-
-### Patch Changes
-
-- Updated dependencies [d1e930a]
-- Updated dependencies [71578f2]
-- Updated dependencies [5e3a301]
-- Updated dependencies [5db2742]
- - @objectstack/spec@9.6.0
- - @objectstack/core@9.6.0
- - @objectstack/platform-objects@9.6.0
-
-## 9.5.1
-
-### Patch Changes
-
-- Updated dependencies [ee72aae]
- - @objectstack/spec@9.5.1
- - @objectstack/core@9.5.1
- - @objectstack/platform-objects@9.5.1
-
-## 9.5.0
-
-### Patch Changes
-
-- Updated dependencies [d08551c]
-- Updated dependencies [5be7102]
-- Updated dependencies [707aeed]
-- Updated dependencies [7a103d4]
-- Updated dependencies [4b01250]
- - @objectstack/spec@9.5.0
- - @objectstack/platform-objects@9.5.0
- - @objectstack/core@9.5.0
-
-## 9.4.0
-
-### Patch Changes
-
-- Updated dependencies [060467a]
-- Updated dependencies [0856476]
-- Updated dependencies [b678d8c]
-- Updated dependencies [b678d8c]
-- Updated dependencies [b678d8c]
- - @objectstack/spec@9.4.0
- - @objectstack/core@9.4.0
- - @objectstack/platform-objects@9.4.0
-
-## 9.3.0
-
-### Patch Changes
-
-- Updated dependencies [1ada658]
-- Updated dependencies [3219191]
-- Updated dependencies [290f631]
-- Updated dependencies [50b7b47]
-- Updated dependencies [f15d6f6]
-- Updated dependencies [f8684ea]
-- Updated dependencies [c802327]
-- Updated dependencies [b4765be]
- - @objectstack/spec@9.3.0
- - @objectstack/platform-objects@9.3.0
- - @objectstack/core@9.3.0
-
-## 9.2.0
-
-### Patch Changes
-
-- Updated dependencies [2f57b75]
-- Updated dependencies [2f57b75]
- - @objectstack/spec@9.2.0
- - @objectstack/core@9.2.0
- - @objectstack/platform-objects@9.2.0
-
-## 9.1.0
-
-### Patch Changes
-
-- Updated dependencies [b9062c9]
- - @objectstack/spec@9.1.0
- - @objectstack/core@9.1.0
- - @objectstack/platform-objects@9.1.0
-
-## 9.0.1
-
-### Patch Changes
-
-- Updated dependencies [1817845]
- - @objectstack/spec@9.0.1
- - @objectstack/core@9.0.1
- - @objectstack/platform-objects@9.0.1
-
-## 9.0.0
-
-### Patch Changes
-
-- Updated dependencies [4c3f693]
-- Updated dependencies [0bf39f1]
-- Updated dependencies [f533f42]
-- Updated dependencies [1c83ee8]
- - @objectstack/spec@9.0.0
- - @objectstack/core@9.0.0
- - @objectstack/platform-objects@9.0.0
-
-## 8.0.1
-
-### Patch Changes
-
-- @objectstack/spec@8.0.1
-- @objectstack/core@8.0.1
-- @objectstack/platform-objects@8.0.1
-
-## 8.0.0
-
-### Patch Changes
-
-- Updated dependencies [a46c017]
-- Updated dependencies [b990b89]
-- Updated dependencies [99111ec]
-- Updated dependencies [d5a8161]
-- Updated dependencies [5cf1f1b]
-- Updated dependencies [9ef89d4]
-- Updated dependencies [3306d2f]
-- Updated dependencies [c262301]
-- Updated dependencies [bc44195]
-- Updated dependencies [9e2e229]
- - @objectstack/spec@8.0.0
- - @objectstack/core@8.0.0
- - @objectstack/platform-objects@8.0.0
-
-## 7.9.0
-
-### Patch Changes
-
-- @objectstack/spec@7.9.0
-- @objectstack/core@7.9.0
-- @objectstack/platform-objects@7.9.0
-
-## 7.8.0
-
-### Patch Changes
-
-- Updated dependencies [06f2bbb]
-- Updated dependencies [36719db]
-- Updated dependencies [424ab26]
- - @objectstack/spec@7.8.0
- - @objectstack/core@7.8.0
- - @objectstack/platform-objects@7.8.0
-
-## 7.7.0
-
-### Patch Changes
-
-- Updated dependencies [b391955]
-- Updated dependencies [f06b64e]
-- Updated dependencies [023bf93]
-- Updated dependencies [764c747]
- - @objectstack/spec@7.7.0
- - @objectstack/platform-objects@7.7.0
- - @objectstack/core@7.7.0
-
-## 7.6.0
-
-### Patch Changes
-
-- Updated dependencies [955d4c8]
-- Updated dependencies [c4a4cbd]
-- Updated dependencies [b046ec2]
-- Updated dependencies [2170ad9]
-- Updated dependencies [02d6359]
-- Updated dependencies [7648242]
-- Updated dependencies [8fa1e7f]
-- Updated dependencies [7ae6abc]
-- Updated dependencies [55866f5]
-- Updated dependencies [60f9c45]
- - @objectstack/spec@7.6.0
- - @objectstack/platform-objects@7.6.0
- - @objectstack/core@7.6.0
-
-## 7.5.0
-
-### Patch Changes
-
-- @objectstack/spec@7.5.0
-- @objectstack/core@7.5.0
-- @objectstack/platform-objects@7.5.0
-
-## 7.4.1
-
-### Patch Changes
-
-- @objectstack/spec@7.4.1
-- @objectstack/core@7.4.1
-- @objectstack/platform-objects@7.4.1
-
-## 7.4.0
-
-### Patch Changes
-
-- Updated dependencies [23c7107]
-- Updated dependencies [c72daad]
-- Updated dependencies [4404572]
-- Updated dependencies [eea3f1b]
-- Updated dependencies [e478e0c]
-- Updated dependencies [4cc2ced]
-- Updated dependencies [13632b1]
-- Updated dependencies [f115182]
-- Updated dependencies [2faf9f2]
-- Updated dependencies [2faf9f2]
-- Updated dependencies [2faf9f2]
-- Updated dependencies [58b450b]
-- Updated dependencies [82eb6cf]
-- Updated dependencies [c381977]
-- Updated dependencies [13d8653]
-- Updated dependencies [ff3d006]
-- Updated dependencies [5e831de]
- - @objectstack/spec@7.4.0
- - @objectstack/platform-objects@7.4.0
- - @objectstack/core@7.4.0
-
-## 7.3.0
-
-### Patch Changes
-
-- Updated dependencies [5e7c554]
- - @objectstack/spec@7.3.0
- - @objectstack/core@7.3.0
- - @objectstack/platform-objects@7.3.0
-
-## 7.2.1
-
-### Patch Changes
-
-- @objectstack/spec@7.2.1
-- @objectstack/core@7.2.1
-- @objectstack/platform-objects@7.2.1
-
-## 7.2.0
-
-### Patch Changes
-
-- @objectstack/spec@7.2.0
-- @objectstack/core@7.2.0
-- @objectstack/platform-objects@7.2.0
-
-## 7.1.0
-
-### Patch Changes
-
-- Updated dependencies [6228609]
-- Updated dependencies [47a92f4]
- - @objectstack/platform-objects@7.1.0
- - @objectstack/spec@7.1.0
- - @objectstack/core@7.1.0
-
-## 7.0.0
-
-### Patch Changes
-
-- Updated dependencies [74470ad]
-- Updated dependencies [d29617e]
-- Updated dependencies [dc72172]
-- Updated dependencies [d29617e]
-- Updated dependencies [010757b]
-- Updated dependencies [257954d]
- - @objectstack/spec@7.0.0
- - @objectstack/platform-objects@7.0.0
- - @objectstack/core@7.0.0
-
-## 6.9.0
-
-### Patch Changes
-
-- @objectstack/spec@6.9.0
-- @objectstack/core@6.9.0
-- @objectstack/platform-objects@6.9.0
-
-## 6.8.1
-
-### Patch Changes
-
-- @objectstack/spec@6.8.1
-- @objectstack/core@6.8.1
-- @objectstack/platform-objects@6.8.1
-
-## 6.8.0
-
-### Patch Changes
-
-- Updated dependencies [6e88f77]
-- Updated dependencies [c8b9f57]
-- Updated dependencies [45d27c5]
- - @objectstack/spec@6.8.0
- - @objectstack/platform-objects@6.8.0
- - @objectstack/core@6.8.0
-
-## 6.7.1
-
-### Patch Changes
-
-- @objectstack/spec@6.7.1
-- @objectstack/core@6.7.1
-- @objectstack/platform-objects@6.7.1
-
-## 6.7.0
-
-### Patch Changes
-
-- Updated dependencies [430067b]
-- Updated dependencies [4f9e9d4]
-- Updated dependencies [4f9e9d4]
- - @objectstack/spec@6.7.0
- - @objectstack/platform-objects@6.7.0
- - @objectstack/core@6.7.0
-
-## 6.6.0
-
-### Patch Changes
-
-- Updated dependencies [a49cfc2]
- - @objectstack/spec@6.6.0
- - @objectstack/core@6.6.0
- - @objectstack/platform-objects@6.6.0
-
-## 6.5.1
-
-### Patch Changes
-
-- @objectstack/spec@6.5.1
-- @objectstack/core@6.5.1
-- @objectstack/platform-objects@6.5.1
-
-## 6.5.0
-
-### Patch Changes
-
-- @objectstack/spec@6.5.0
-- @objectstack/core@6.5.0
-- @objectstack/platform-objects@6.5.0
-
-## 6.4.0
-
-### Patch Changes
-
-- Updated dependencies [f8651cc]
-- Updated dependencies [f8651cc]
-- Updated dependencies [0bf6f9a]
- - @objectstack/spec@6.4.0
- - @objectstack/core@6.4.0
- - @objectstack/platform-objects@6.4.0
-
-## 6.3.0
-
-### Patch Changes
-
-- @objectstack/spec@6.3.0
-- @objectstack/core@6.3.0
-- @objectstack/platform-objects@6.3.0
-
-## 6.2.0
-
-### Patch Changes
-
-- Updated dependencies [b4c74a9]
- - @objectstack/spec@6.2.0
- - @objectstack/core@6.2.0
- - @objectstack/platform-objects@6.2.0
-
-## 6.1.1
-
-### Patch Changes
-
-- @objectstack/spec@6.1.1
-- @objectstack/core@6.1.1
-- @objectstack/platform-objects@6.1.1
-
-## 6.1.0
-
-### Patch Changes
-
-- Updated dependencies [93c0589]
- - @objectstack/spec@6.1.0
- - @objectstack/core@6.1.0
- - @objectstack/platform-objects@6.1.0
-
-## 6.0.0
-
-### Patch Changes
-
-- Updated dependencies [629a716]
-- Updated dependencies [dbc4f7d]
-- Updated dependencies [944f187]
- - @objectstack/spec@6.0.0
- - @objectstack/platform-objects@6.0.0
- - @objectstack/core@6.0.0
-
-## 5.2.0
-
-### Patch Changes
-
-- Updated dependencies [bab2b20]
-- Updated dependencies [fa011d8]
-- Updated dependencies [f0f7c27]
-- Updated dependencies [b806f58]
- - @objectstack/platform-objects@5.2.0
- - @objectstack/spec@5.2.0
- - @objectstack/core@5.2.0
-
-## 5.1.0
-
-### Patch Changes
-
-- Updated dependencies [75f4ee6]
-- Updated dependencies [823d559]
- - @objectstack/spec@5.1.0
- - @objectstack/platform-objects@5.1.0
- - @objectstack/core@5.1.0
-
-## 5.0.0
-
-### Patch Changes
-
-- Updated dependencies [888a5c1]
-- Updated dependencies [2f9073a]
- - @objectstack/platform-objects@5.0.0
- - @objectstack/spec@5.0.0
- - @objectstack/core@5.0.0
-
-## 4.2.0
-
-### Patch Changes
-
-- Updated dependencies [2869891]
- - @objectstack/spec@4.2.0
- - @objectstack/core@4.2.0
- - @objectstack/platform-objects@4.2.0
-
-## 4.1.1
-
-### Patch Changes
-
-- @objectstack/spec@4.1.1
-- @objectstack/core@4.1.1
-- @objectstack/platform-objects@4.1.1
-
-## 4.0.1
-
-### Patch Changes
-
-- Updated dependencies [2108c30]
-- Updated dependencies [23db640]
- - @objectstack/spec@4.1.0
- - @objectstack/core@4.1.0
- - @objectstack/platform-objects@4.1.0
diff --git a/packages/plugins/plugin-reports/LICENSE b/packages/plugins/plugin-reports/LICENSE
deleted file mode 100644
index 16bc23f4043..00000000000
--- a/packages/plugins/plugin-reports/LICENSE
+++ /dev/null
@@ -1,202 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute
- must include a readable copy of the attribution notices
- contained within such NOTICE file, excluding those notices
- that do not pertain to any part of the Derivative Works,
- in at least one of the following places: within a NOTICE
- text file distributed as part of the Derivative Works; within
- the Source form or documentation, if provided along with
- the Derivative Works; or, within a display generated by the
- Derivative Works, if and wherever such third-party notices
- normally appear. The contents of the NOTICE file are for
- informational purposes only and do not modify the License.
- You may add Your own attribution notices within Derivative
- Works that You distribute, alongside or as an addendum to
- the NOTICE text from the Work, provided that such additional
- attribution notices cannot be construed as modifying the
- License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2026 ObjectStack
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json
deleted file mode 100644
index 184649861d0..00000000000
--- a/packages/plugins/plugin-reports/package.json
+++ /dev/null
@@ -1,52 +0,0 @@
-{
- "name": "@objectstack/plugin-reports",
- "version": "17.4.0",
- "license": "Apache-2.0",
- "description": "Saved reports + scheduled email digests for ObjectStack — sys_saved_report + sys_report_schedule + IReportService.",
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
- "exports": {
- ".": {
- "types": "./dist/index.d.ts",
- "import": "./dist/index.mjs",
- "require": "./dist/index.js"
- }
- },
- "scripts": {
- "build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
- "test": "vitest run --passWithNoTests",
- "typecheck": "tsc --noEmit && pnpm check:test-typecheck",
- "check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/plugins/plugin-reports --project tsconfig.test.json"
- },
- "dependencies": {
- "@objectstack/core": "workspace:*",
- "@objectstack/platform-objects": "workspace:*",
- "@objectstack/spec": "workspace:*",
- "croner": "^10.0.1"
- },
- "devDependencies": {
- "@objectstack/driver-sql": "workspace:*",
- "@objectstack/objectql": "workspace:*",
- "@types/node": "^26.2.0",
- "tsx": "^4.23.12",
- "typescript": "^6.0.3",
- "vitest": "^4.1.11"
- },
- "keywords": [
- "objectstack",
- "plugin",
- "reports",
- "scheduling",
- "email"
- ],
- "repository": {
- "type": "git",
- "url": "https://github.com/objectstack-ai/objectstack.git",
- "directory": "packages/plugins/plugin-reports"
- },
- "files": [
- "dist",
- "README.md",
- "CHANGELOG.md"
- ]
-}
diff --git a/packages/plugins/plugin-reports/src/dispatcher-runs-on-object-kernel.test.ts b/packages/plugins/plugin-reports/src/dispatcher-runs-on-object-kernel.test.ts
deleted file mode 100644
index 7106abd72aa..00000000000
--- a/packages/plugins/plugin-reports/src/dispatcher-runs-on-object-kernel.test.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
-
-/**
- * [#10746] Scheduled reports must actually RUN on `ObjectKernel` — the kernel
- * real deployments use — when no job plugin is installed.
- *
- * THE DEFECT THIS PINS. `ReportsServicePlugin.start()` prefers the platform
- * job service and only falls through to its own `setInterval` when
- * `ctx.getService('job')` yields nothing with a `schedule` method.
- * `ObjectKernel.preInjectCoreFallbacks()` used to register `createMemoryJob()`
- * for every unprovided `core` service before Phase 2 — and `createMemoryJob()`
- * is honest in its own docblock that a `schedule()`d job NEVER fires on its
- * own (its `schedule()` is `jobs.set(...)` and nothing else). So on an
- * `ObjectKernel` without `@objectstack/service-job` the plugin logged
- * `dispatcher registered with job service` and then dispatched nothing, ever:
- * measured as 0 reads of `sys_report_schedule` in 5600 ms with the success
- * line present. The `setInterval` branch the plugin's docblock offers as the
- * single-kernel answer was dead code on the kernel real deployments use.
- *
- * THE REPAIR (maintainer ruling 2026-08-22, Option A — a fallback must not
- * fake capability). `job` came off the kernel's pre-injection list
- * (`CORE_FALLBACK_FACTORIES` in `@objectstack/core`), so `getService('job')`
- * now throws when no job plugin is installed, the plugin's existing catch
- * falls through to `setInterval`, and single-kernel deployments actually
- * dispatch scheduled reports.
- *
- * WHY THIS FILE EXISTS BESIDE `plugin-shutdown-releases-dispatcher.test.ts`.
- * That file pins RELEASE at shutdown, and its running-dispatcher leg runs on
- * `LiteKernel` — which injects no fallbacks, so it could never see this
- * defect. Nothing pinned that the dispatcher RUNS on `ObjectKernel`; that gap
- * is exactly why the defect shipped invisibly. This pin is the acceptance
- * evidence for the fix: same composition a real single-kernel deployment
- * boots, no job plugin anywhere, and the poll traffic itself is the assertion.
- */
-
-import { describe, it, expect, afterEach } from 'vitest';
-import { ObjectKernel } from '@objectstack/core';
-import { ObjectQLPlugin } from '@objectstack/objectql';
-import type { ObjectQL } from '@objectstack/objectql';
-import { SqlDriver } from '@objectstack/driver-sql';
-import type { IDataEngine } from '@objectstack/spec/contracts';
-import { ReportsServicePlugin } from './reports-plugin.js';
-
-/**
- * The plugin floors its own interval at 5s (`Math.max(5_000, …)`), so this is
- * the fastest REAL clock the dispatcher can be driven at.
- */
-const DISPATCH_INTERVAL_MS = 5_000;
-/** Comfortably past one tick boundary, so a window that sees zero is silence. */
-const OBSERVE_MS = 5_600;
-
-const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
-
-const openKernels: Array<{ shutdown(): Promise }> = [];
-const openDrivers: Array<{ disconnect?: () => Promise }> = [];
-
-afterEach(async () => {
- // Kernels first, drivers second: the kernel's own teardown still wants a
- // live driver to drain against.
- while (openKernels.length) {
- try { await openKernels.pop()?.shutdown(); } catch { /* already stopped */ }
- }
- while (openDrivers.length) {
- try { await openDrivers.pop()?.disconnect?.(); } catch { /* noop */ }
- }
-});
-
-/**
- * Installs the read counter on the engine instance the dispatcher captured at
- * `kernel:ready` (`ctx.getService('objectql')` resolves to this same object),
- * so the tally is of real `ReportService.dispatchDue()` traffic, not a
- * stand-in.
- */
-function countScheduleReads(engineHolder: { getService: (n: string) => T }): () => number {
- type EngineCall = (name: string, ...rest: unknown[]) => unknown;
- const engine = engineHolder.getService('objectql') as unknown as
- Record<'find', EngineCall>;
- let reads = 0;
- const orig = engine.find.bind(engine);
- engine.find = (name: string, ...rest: unknown[]) => {
- if (String(name) === 'sys_report_schedule') reads++;
- return orig(name, ...rest);
- };
- return () => reads;
-}
-
-/** A real in-memory SQL driver, connected and registered on `engine`. */
-async function attachSqlite(objectql: any): Promise {
- const driver: any = new SqlDriver({
- client: 'better-sqlite3',
- connection: { filename: ':memory:' },
- useNullAsDefault: true,
- });
- await driver.connect();
- objectql.registerDriver(driver, true);
- openDrivers.push(driver);
- await objectql.syncSchemas();
-}
-
-describe('#10746 the dispatcher RUNS on ObjectKernel with no job plugin', () => {
- it(
- 'polls sys_report_schedule — the setInterval fallback is reachable on the production kernel',
- { timeout: 60_000 },
- async () => {
- // The composition a real single-kernel deployment boots: ObjectKernel
- // (NOT LiteKernel), the engine, the reports plugin — and no job plugin.
- const kernel = new ObjectKernel({ logger: { level: 'silent' } });
- openKernels.push(kernel);
-
- await kernel.use(new ObjectQLPlugin());
- await kernel.use(new ReportsServicePlugin({ dispatchIntervalMs: DISPATCH_INTERVAL_MS }));
- await kernel.bootstrap();
-
- await attachSqlite(kernel.getService('objectql'));
- const scheduleReads = countScheduleReads(kernel as any);
-
- await sleep(OBSERVE_MS);
-
- // THE PIN. Before the fix this was 0 — the kernel pre-injected a `job`
- // fallback whose `schedule()` recorded the dispatcher and never fired
- // it, while the plugin logged success. One tick boundary has passed, so
- // silence here is the defect, not timing.
- expect(scheduleReads()).toBeGreaterThan(0);
- },
- );
-});
diff --git a/packages/plugins/plugin-reports/src/exec-context-annotation.pin.ts b/packages/plugins/plugin-reports/src/exec-context-annotation.pin.ts
deleted file mode 100644
index 0a7e0b11560..00000000000
--- a/packages/plugins/plugin-reports/src/exec-context-annotation.pin.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
-
-/**
- * #7135 — compile-time pin for the CONTEXT type this plugin's report methods
- * accept, and for what `OwnerContextResolver` is required to return.
- *
- * #6523 converged 36 contract signatures onto the full `ExecutionContext` (the
- * #6206 ruling: enforcement adjudicates on the whole `resolveAuthzContext`
- * envelope, never a per-site subset). #7135 is the services half of the #7070
- * consumer split — the implementations here now annotate their own parameters
- * with that same envelope instead of the six-field shape they used to name.
- *
- * WHY THIS FILE EXISTS AT ALL. That convergence has no runtime behaviour and
- * no compiler pressure in either direction: the values were always complete,
- * and the narrow annotation is STRUCTURALLY ASSIGNABLE to the wide one, so
- * re-narrowing any of these parameters compiles, ships, and passes every test
- * in this package. Nothing would notice. This module is the one thing that
- * does — every declaration below is red exactly when a parameter narrows back.
- *
- * HOW IT BITES, part 1: TypeScript's excess-property check on a FRESH object
- * literal. `posture` (ADR-0095 D2), `accessible_org_ids` (ADR-0105 D2) and
- * `org_user_ids` are fields of the envelope that the retired six-field shape
- * did not carry, so a literal naming them is rejected the moment the parameter
- * is annotated with anything that lacks them. Note this is the ONLY direction
- * that works: a `@ts-expect-error` asserting the reverse would be unsatisfied
- * and fail the build, because a narrow context IS assignable to a wide
- * parameter — see item 3 of the module doc on
- * `@objectstack/spec/contracts/sharing-service` for that boundary.
- *
- * HOW IT BITES, part 2 (#7218): type IDENTITY against the retired shape itself.
- * #7135's failure story was "the parameter narrows back to the six-field
- * `SharingExecutionContext`" — a type that no longer exists, since #7218
- * deleted it from the contract surface once all three implementations had been
- * re-annotated. Deleting the type does NOT delete the failure mode: the six
- * fields can be re-declared here under any name, and the literal checks above
- * only fire on the fields a given literal happens to spell. So the retired
- * shape is kept below as a local SPECIMEN and each parameter is refuted
- * against it. A re-narrowing is then red twice over, and neither check depends
- * on the retired export coming back.
- *
- * WHY A `.pin.ts` AND NOT A `*.test.ts`: unlike its sibling packages,
- * `packages/plugins/plugin-reports/tsconfig.json` does NOT exclude
- * `**\/*.test.ts` (measured on this card — `plugin-approvals` and
- * `plugin-sharing` both do), so a pin in a test file here would in fact be
- * read by `tsc --noEmit`. The `.pin.ts` convention is kept anyway: it does not
- * depend on that exclusion staying absent, it survives a vitest-only run that
- * never type-checks, and it keeps both halves of this card's pin identical in
- * shape. It is imported by nothing, so tsup (entry `src/index.ts`) never
- * bundles it into `dist`.
- */
-
-import type { ReportService, OwnerContextResolver } from './report-service.js';
-
-type SaveReportContext = Parameters[1];
-type RunContext = Parameters[1];
-type GetReportContext = Parameters[1];
-type ScheduleReportContext = Parameters[1];
-type ListSchedulesContext = Parameters[1];
-
-/**
- * What a scheduled run executes AS. `resolveOwnerContext` resolves a saved
- * report's owner into a real, RLS-bearing context so the digest sees the rows
- * the owner would see interactively (#2849 / #2980); typing its result as the
- * envelope is what lets `executeReport` read the whole thing it was handed.
- */
-type ResolvedOwnerContext = NonNullable>>;
-
-/**
- * [#7218] The RETIRED six-field shape, kept here as a SPECIMEN — a deliberate
- * COPY of the type `@objectstack/spec` exported as `SharingExecutionContext`
- * until #7218 deleted it. Copied rather than imported on purpose: nothing may
- * depend on the retired name again, and a local copy is what lets this pin keep
- * naming the shape it refuses after the export is gone.
- *
- * ⛔ Not a vocabulary to reach for, and not exported.
- */
-type RetiredSharingContextSpecimen = {
- userId?: string;
- tenantId?: string;
- positions?: string[];
- permissions?: string[];
- systemPermissions?: string[];
- isSystem?: boolean;
-};
-
-/** Type-level identity: true iff A and B are the same type. */
-type Eq = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2)
- ? true
- : false;
-/** Compile error when the argument is not `false`. */
-type Refute = T;
-
-/**
- * NEGATIVE, at the type level: neither the method parameters nor what the owner
- * resolver hands back IS the retired shape. Red the moment one is re-annotated
- * with those six fields under any spelling — the failure #7135's pin told as
- * "narrows back to `SharingExecutionContext`", restated so it no longer needs
- * the deleted name to be checkable. `ResolvedOwnerContext` is included because a
- * scheduled run adjudicates on whatever the resolver's RETURN type says it has.
- */
-type _NotTheRetiredShape = [
- Refute>,
- Refute>,
- Refute>,
- Refute>,
- Refute>,
- Refute>,
-];
-
-/**
- * Never called — every line below is a type-level assertion evaluated by
- * `tsc --noEmit`. The parameters are taken as arguments rather than read off a
- * live service so the pin needs no instance and no import cycle.
- */
-export function __pinReportsTakesTheFullEnvelope(
- saveReport: (input: never, context: SaveReportContext) => unknown,
- run: (reportId: string, context: RunContext) => unknown,
- getReport: (reportId: string, context: GetReportContext) => unknown,
- scheduleReport: (input: never, context: ScheduleReportContext) => unknown,
- listSchedules: (filter: undefined, context: ListSchedulesContext) => unknown,
- ownerContext: ResolvedOwnerContext,
-): void {
- // ── POSITIVE: fields that exist ONLY on the full envelope, no cast. ───────
- saveReport(undefined as never, { userId: 'u1', posture: 'MEMBER' });
- run('rep_1', { userId: 'u1', posture: 'TENANT_ADMIN', accessible_org_ids: ['org_a'] });
- getReport('rep_1', { userId: 'u1', org_user_ids: ['u1', 'u2'] });
- scheduleReport(undefined as never, { userId: 'u1', accessible_org_ids: ['org_a'] });
- listSchedules(undefined, { userId: 'u1', posture: 'PLATFORM_ADMIN' });
-
- // The resolver hands back what it RESOLVED. Reading a field the six-field
- // shape never had is what pins that: a scheduled run adjudicates on the
- // whole envelope or it is not running as the owner at all.
- const posture: ResolvedOwnerContext['posture'] = ownerContext.posture;
- void posture;
-
- // ── NEGATIVE: none of these types IS the retired six-field shape. ────────
- // The tuple is all-`false` exactly when every `Refute` above holds; a
- // parameter (or the resolver's return type) re-narrowed to the specimen makes
- // its slot `true` and this assignment stops compiling.
- const notTheRetiredShape: _NotTheRetiredShape = [false, false, false, false, false, false];
- void notTheRetiredShape;
-
- // ── NEGATIVE: widening must not have degenerated into `any`. ─────────────
- // A parameter erased to `any` would swallow every positive above just as
- // happily, so the pin is only worth its weight if wrong input still fails.
- // @ts-expect-error 'SUPERUSER' is not an ADR-0095 posture rung
- run('rep_1', { userId: 'u1', posture: 'SUPERUSER' });
- // @ts-expect-error `userId` is a string on the envelope, not a number
- getReport('rep_1', { userId: 42 });
- // @ts-expect-error `accessible_org_ids` is a string[], not a bare string
- listSchedules(undefined, { accessible_org_ids: 'org_a' });
- // @ts-expect-error `organizationId` is NOT a field of the envelope — that
- // spelling has its own history (#5858 / `check:org-identifier`) and was held
- // out of #7135 on purpose.
- saveReport(undefined as never, { organizationId: 'org_a' });
-}
diff --git a/packages/plugins/plugin-reports/src/index.ts b/packages/plugins/plugin-reports/src/index.ts
deleted file mode 100644
index f06d91d6291..00000000000
--- a/packages/plugins/plugin-reports/src/index.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-/**
- * @objectstack/plugin-reports
- *
- * Saved reports + scheduled email digests for ObjectStack.
- * Persists `sys_saved_report` definitions and `sys_report_schedule`
- * rows, then drives a dispatcher that runs due schedules and emails
- * the rendered output via the configured `email` service.
- */
-
-export { SysSavedReport, SysReportSchedule } from '@objectstack/platform-objects/audit';
-export {
- ReportService,
- renderReport,
- type ReportEngine,
- type ReportEmail,
- type ReportClock,
- type ReportServiceOptions,
-} from './report-service.js';
-export {
- ReportsServicePlugin,
- type ReportsPluginOptions,
-} from './reports-plugin.js';
-export type {
- IReportService,
- SavedReport,
- ReportSchedule,
- ReportQuery,
- ReportRunResult,
- ReportFormat,
- SaveReportInput,
- ScheduleReportInput,
-} from '@objectstack/spec/contracts';
diff --git a/packages/plugins/plugin-reports/src/plugin-shutdown-releases-dispatcher.test.ts b/packages/plugins/plugin-reports/src/plugin-shutdown-releases-dispatcher.test.ts
deleted file mode 100644
index bc0b61f1b19..00000000000
--- a/packages/plugins/plugin-reports/src/plugin-shutdown-releases-dispatcher.test.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
-
-/**
- * [#10371] `await kernel.shutdown()` must leave NOTHING of this plugin still
- * running.
- *
- * THE DEFECT. `ReportsServicePlugin` arms its schedule dispatcher at
- * `kernel:ready` — either a `setInterval` over `sys_report_schedule`, or, when
- * `service-job` is installed, a scheduled `reports.dispatch` job. The teardown
- * that released both was spelled `stop()`. The kernel's plugin teardown hook is
- * `destroy()` (`Plugin.destroy?()` in `@objectstack/core`'s `types.ts` — the
- * only teardown `ObjectKernel.performShutdown()` and `LiteKernel.destroy()`
- * invoke), and `stop()` is not part of that interface, so nothing in the repo
- * ever called it and the dispatcher went on ticking after shutdown resolved.
- *
- * THE ASYMMETRY THAT HID IT. `start?(ctx)` IS on the interface and does fire.
- * A `start`/`stop` pair where only one half is wired reads as symmetric in
- * review — which is why the identical shape survived in six packages at once,
- * and why #9371 found it in `service-messaging` only after it had already cost
- * something.
- *
- * WHY IT WENT UNNOTICED, AND WHERE THE BILL LANDED. `start()` `unref()`s the
- * interval, so a long-lived host process still exits and the leak is silent in
- * production. Under vitest the worker is alive throughout teardown, so a tick
- * fires AFTER the test file is over, reads through a driver the suite has
- * already disconnected, and the driver's console fallback warns. `console.*`
- * inside a 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
- * surfaces 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).
- *
- * WHAT THIS PINS, AND WHY IN THIS SHAPE. The assertions are behavioural —
- * "after shutdown the plugin issues no further schedule reads", "after shutdown
- * the scheduled job has been cancelled" — and not
- * `expect(plugin.destroy).toBeDefined()`, because the hook merely EXISTING is
- * not the property that was missing; being REACHED BY THE KERNEL is.
- *
- * Every pre-shutdown leg is a POSITIVE CONTROL and load-bearing: without it a
- * dispatcher that never started would satisfy the post-shutdown assertion
- * vacuously, and this file would pass on a plugin that does nothing at all.
- *
- * The `stop()` legs are the other direction, and they are not decoration: the
- * repair keeps `stop()` as a delegating alias because it is public API of an
- * exported class and an embedder may have learned to call it directly PRECISELY
- * BECAUSE the kernel never did. Pinning only the shutdown direction would go
- * green on an implementation that simply deletes `stop()`, which breaks them.
- */
-
-import { describe, it, expect, afterEach } from 'vitest';
-import { ObjectKernel, LiteKernel } from '@objectstack/core';
-import type { Plugin, PluginContext } from '@objectstack/core';
-import { ObjectQLPlugin } from '@objectstack/objectql';
-import type { ObjectQL } from '@objectstack/objectql';
-import { SqlDriver } from '@objectstack/driver-sql';
-import type { IDataEngine } from '@objectstack/spec/contracts';
-import { ReportsServicePlugin } from './reports-plugin.js';
-
-/**
- * The plugin floors its own interval at 5s (`Math.max(5_000, …)`), so this is
- * the fastest REAL clock the dispatcher can be driven at. Windows below are
- * sized off it rather than off a wish.
- */
-const DISPATCH_INTERVAL_MS = 5_000;
-/** Comfortably past one tick boundary, so a window that sees zero is silence. */
-const OBSERVE_MS = 5_600;
-
-const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
-
-const openKernels: Array<{ shutdown(): Promise }> = [];
-const openDrivers: Array<{ disconnect?: () => Promise }> = [];
-
-afterEach(async () => {
- // Kernels first, drivers second: the kernel's own teardown still wants a live
- // driver to drain against.
- while (openKernels.length) {
- try { await openKernels.pop()?.shutdown(); } catch { /* already stopped */ }
- }
- while (openDrivers.length) {
- try { await openDrivers.pop()?.disconnect?.(); } catch { /* noop */ }
- }
-});
-
-/** Records what the plugin asked the platform job service to run and cancel. */
-interface JobLog {
- scheduled: string[];
- cancelled: string[];
-}
-
-/**
- * Publishes a minimal `job` service so the plugin takes its job-service branch
- * instead of the `setInterval` fallback. A plugin rather than a bare
- * `registerService` call because the branch is only taken if the service is
- * resolvable at `kernel:ready`, which is the kernel's business, not ours.
- */
-class FakeJobServicePlugin implements Plugin {
- name = 'test.fake.job';
- version = '1.0.0';
- type = 'standard' as const;
-
- constructor(private readonly log: JobLog) {}
-
- async init(ctx: PluginContext): Promise {
- ctx.registerService('job', {
- schedule: async (name: string) => { this.log.scheduled.push(name); },
- cancel: async (name: string) => { this.log.cancelled.push(name); },
- });
- }
-}
-
-interface Booted {
- kernel: { shutdown(): Promise };
- plugin: ReportsServicePlugin;
- /** Reads the dispatcher has made against `sys_report_schedule`. */
- scheduleReads: () => number;
-}
-
-/**
- * Installs the read counter on the engine instance the dispatcher captured at
- * `kernel:ready` (`ctx.getService('objectql')` resolves to this same object), so
- * the tally is of real `ReportService.dispatchDue()` traffic and not of a
- * stand-in.
- */
-function countScheduleReads(engineHolder: { getService: (n: string) => T }): () => number {
- type EngineCall = (name: string, ...rest: unknown[]) => unknown;
- const engine = engineHolder.getService('objectql') as unknown as
- Record<'find', EngineCall>;
- let reads = 0;
- const orig = engine.find.bind(engine);
- engine.find = (name: string, ...rest: unknown[]) => {
- if (String(name) === 'sys_report_schedule') reads++;
- return orig(name, ...rest);
- };
- return () => reads;
-}
-
-/** A real in-memory SQL driver, connected and registered on `engine`. */
-async function attachSqlite(objectql: any): Promise {
- const driver: any = new SqlDriver({
- client: 'better-sqlite3',
- connection: { filename: ':memory:' },
- useNullAsDefault: true,
- });
- await driver.connect();
- objectql.registerDriver(driver, true);
- openDrivers.push(driver);
- await objectql.syncSchemas();
-}
-
-/**
- * The `setInterval` branch, and why it needs `LiteKernel` — MEASURED, not
- * assumed. `ReportsServicePlugin.start()` prefers the platform job service and
- * only falls through to `setInterval` when `ctx.getService('job')` yields
- * nothing with a `schedule` method. `ObjectKernel.preInjectCoreFallbacks()`
- * registers `createMemoryJob()` for every unprovided `core` service before
- * Phase 2, so on an `ObjectKernel` a `job` service ALWAYS resolves and this
- * branch is unreachable. `LiteKernel` injects no fallbacks, which is exactly
- * the "single-kernel deployment without `service-job`" the plugin's own
- * docblock names as the reason the `setInterval` path exists.
- */
-async function bootReportsLiteKernel(): Promise {
- const kernel = new LiteKernel({ logger: { level: 'silent' } });
- openKernels.push(kernel);
-
- kernel.use(new ObjectQLPlugin());
- const plugin = new ReportsServicePlugin({ dispatchIntervalMs: DISPATCH_INTERVAL_MS });
- kernel.use(plugin);
- await kernel.bootstrap();
-
- await attachSqlite(kernel.getService('objectql'));
- return { kernel, plugin, scheduleReads: countScheduleReads(kernel as any) };
-}
-
-async function bootReportsKernel(extra?: Plugin): Promise {
- const kernel = new ObjectKernel({ logger: { level: 'silent' } });
- openKernels.push(kernel);
-
- await kernel.use(new ObjectQLPlugin());
- if (extra) await kernel.use(extra);
- const plugin = new ReportsServicePlugin({ dispatchIntervalMs: DISPATCH_INTERVAL_MS });
- await kernel.use(plugin);
- await kernel.bootstrap();
-
- await attachSqlite(kernel.getService('objectql'));
- return { kernel, plugin, scheduleReads: countScheduleReads(kernel as any) };
-}
-
-describe('#10371 ReportsServicePlugin releases its dispatcher on kernel shutdown', () => {
- it(
- 'stops reading sys_report_schedule once shutdown() has resolved',
- { timeout: 60_000 },
- async () => {
- const { kernel, scheduleReads } = await bootReportsLiteKernel();
-
- // POSITIVE CONTROL — the setInterval dispatcher really is ticking, so the
- // post-shutdown assertion below measures silence and not absence.
- await sleep(OBSERVE_MS);
- expect(scheduleReads()).toBeGreaterThan(0);
-
- await kernel.shutdown();
-
- const atShutdown = scheduleReads();
- await sleep(OBSERVE_MS);
-
- // THE PIN. shutdown() resolving means the plugin is done with the
- // database. Before the fix this count kept climbing.
- expect(scheduleReads()).toBe(atShutdown);
- },
- );
-
- it('cancels its scheduled job once shutdown() has resolved', async () => {
- const log: JobLog = { scheduled: [], cancelled: [] };
- const { kernel } = await bootReportsKernel(new FakeJobServicePlugin(log));
-
- // POSITIVE CONTROL — the job branch was taken and nothing has cancelled it.
- expect(log.scheduled).toContain('reports.dispatch');
- expect(log.cancelled).toEqual([]);
-
- await kernel.shutdown();
-
- // THE PIN. Before the fix the cancel lived in `stop()`, which the kernel
- // never called, so the job outlived the kernel that scheduled it.
- expect(log.cancelled).toContain('reports.dispatch');
- });
-
- it('the retained stop() alias still tears down for an embedder that calls it directly', async () => {
- const log: JobLog = { scheduled: [], cancelled: [] };
- const { plugin } = await bootReportsKernel(new FakeJobServicePlugin(log));
-
- expect(log.scheduled).toContain('reports.dispatch');
- expect(log.cancelled).toEqual([]);
-
- // No argument — the shape an embedder writes today against a method whose
- // parameter the repair made optional.
- await plugin.stop();
-
- expect(log.cancelled).toContain('reports.dispatch');
- });
-
- it('the stop() alias still accepts the PluginContext argument it used to require', async () => {
- const log: JobLog = { scheduled: [], cancelled: [] };
- const { plugin } = await bootReportsKernel(new FakeJobServicePlugin(log));
-
- expect(log.scheduled).toContain('reports.dispatch');
-
- // The pre-repair signature was `stop(ctx: PluginContext)`. An embedder
- // holding that call shape must keep compiling AND keep working, which is
- // the entire reason the alias was retained rather than deleted.
- const ctx = {
- logger: { info() {}, warn() {}, error() {}, debug() {} },
- } as unknown as PluginContext;
- await plugin.stop(ctx);
-
- expect(log.cancelled).toContain('reports.dispatch');
- });
-
- it('a teardown on a plugin that never started is a no-op rather than a throw', async () => {
- // Idempotence matters because `destroy()` clears the handles it released; a
- // teardown that only works once is a teardown that fails inside a suite,
- // and the kernel calls it on every plugin it walks.
- const plugin = new ReportsServicePlugin();
- await expect(plugin.destroy()).resolves.toBeUndefined();
- await expect(plugin.destroy()).resolves.toBeUndefined();
- await expect(plugin.stop()).resolves.toBeUndefined();
- });
-});
diff --git a/packages/plugins/plugin-reports/src/report-export-axis.test.ts b/packages/plugins/plugin-reports/src/report-export-axis.test.ts
deleted file mode 100644
index cef177423b2..00000000000
--- a/packages/plugins/plugin-reports/src/report-export-axis.test.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
-
-/**
- * #3710 — the reports side door around the user-level export axis (#3544).
- *
- * `GET /data/:object/export` answers 403 for a caller without the export grant.
- * A report over the SAME object rendered as CSV is the same bulk copy of the
- * same rows, so before this gate a refused caller could simply save a report,
- * run it as CSV — or schedule one to their own inbox — and get the data anyway.
- *
- * The gate lives in `executeReport`, which every path funnels through (run,
- * ad-hoc, and the scheduled dispatch), so these exercise all three rather than
- * trusting one call site.
- */
-
-import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { ReportService, type ReportEmail } from './report-service.js';
-
-function makeFakeEngine() {
- const tables: Record = {};
- const ensure = (n: string) => (tables[n] ??= []);
- const matches = (row: any, filter: any): boolean => {
- if (!filter || typeof filter !== 'object') return true;
- for (const [k, v] of Object.entries(filter)) {
- if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`);
- if (row[k] !== v) return false;
- }
- return true;
- };
- return {
- _tables: tables,
- async find(object: string, options?: any) {
- return ensure(object)
- .filter((r) => matches(r, options?.filter ?? options?.where))
- .slice(0, options?.limit ?? 1000);
- },
- async insert(object: string, data: any) { ensure(object).push({ ...data }); return { ...data }; },
- async update(object: string, idOrData: any, _opts?: any) {
- const data = typeof idOrData === 'object' ? idOrData : _opts;
- const id = typeof idOrData === 'object' ? idOrData.id : idOrData;
- const table = ensure(object);
- const i = table.findIndex((r) => r.id === id);
- if (i >= 0) table[i] = { ...table[i], ...data };
- return table[i];
- },
- async delete(object: string, options?: any) {
- const table = ensure(object);
- const i = table.findIndex((r) => r.id === (options?.where?.id ?? options?.id));
- if (i >= 0) table.splice(i, 1);
- return {};
- },
- };
-}
-
-function makeFakeEmail() {
- const sent: any[] = [];
- const email: ReportEmail & { _sent: any[] } = {
- _sent: sent,
- async send(input) { sent.push(input); return { status: 'sent' }; },
- };
- return email;
-}
-
-const CTX = { userId: 'u1', tenantId: 't1', positions: [], permissions: [] };
-const now = new Date('2026-01-15T10:00:00Z');
-
-describe('reports × the user-level export axis (#3710)', () => {
- let engine: ReturnType;
- let email: ReturnType;
- let canExport: ReturnType;
-
- const build = (opts: { canExport?: any } = {}) =>
- new ReportService({
- engine: engine as any,
- email,
- clock: { now: () => now },
- maxRows: 5000,
- resolveOwnerContext: async (ownerId: string) =>
- ownerId ? { userId: ownerId, tenantId: 't1', positions: [], permissions: [] } : null,
- ...('canExport' in opts ? { canExport: opts.canExport } : { canExport }),
- });
-
- beforeEach(() => {
- engine = makeFakeEngine();
- email = makeFakeEmail();
- canExport = vi.fn().mockResolvedValue(true);
- engine._tables['lead'] = [
- { id: 'l1', name: 'Acme', status: 'open' },
- { id: 'l2', name: 'Globex', status: 'won' },
- ];
- });
-
- const saveReport = async (svc: ReportService, format: 'csv' | 'json' | 'html_table') =>
- svc.saveReport(
- { name: 'leads', object: 'lead', query: { fields: ['id', 'name'] }, format },
- CTX,
- );
-
- describe('interactive run', () => {
- it('denies a CSV report run when the caller may not export the object', async () => {
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'csv');
- await expect(svc.run(report.id, CTX)).rejects.toThrow(/EXPORT_NOT_PERMITTED/);
- });
-
- it('denies a JSON report run too — json is a bulk machine-readable copy', async () => {
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'json');
- await expect(svc.run(report.id, CTX)).rejects.toThrow(/EXPORT_NOT_PERMITTED/);
- });
-
- it('ALLOWS html_table — a rendered view is reading, not exporting', async () => {
- // The axis must not become a second read permission: a caller holding
- // allowRead may already see these rows on screen.
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'html_table');
- const result = await svc.run(report.id, CTX);
- expect(result.rowCount).toBe(2);
- });
-
- it('allows CSV when the grant is held, and asks about the REPORT object', async () => {
- const svc = build();
- const report = await saveReport(svc, 'csv');
- const result = await svc.run(report.id, CTX);
- expect(result.rowCount).toBe(2);
- expect(canExport).toHaveBeenCalledWith('lead', expect.objectContaining({ userId: 'u1' }));
- });
-
- it('refuses BEFORE reading any row', async () => {
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'csv');
- const findSpy = vi.spyOn(engine, 'find');
- await expect(svc.run(report.id, CTX)).rejects.toThrow(/EXPORT_NOT_PERMITTED/);
- // `sys_saved_report` lookups are fine; the OBJECT must never be queried.
- expect(findSpy.mock.calls.some(([obj]) => obj === 'lead')).toBe(false);
- });
-
- it('a throwing canExport denies (fail closed, ADR-0049)', async () => {
- const svc = build({ canExport: vi.fn().mockRejectedValue(new Error('resolution failed')) });
- const report = await saveReport(svc, 'csv');
- await expect(svc.run(report.id, CTX)).rejects.toThrow(/EXPORT_NOT_PERMITTED/);
- });
-
- it('no canExport wired → axis does not apply (no plugin-security deployment)', async () => {
- const svc = build({ canExport: undefined });
- const report = await saveReport(svc, 'csv');
- const result = await svc.run(report.id, CTX);
- expect(result.rowCount).toBe(2);
- });
- });
-
- describe('scheduling', () => {
- it('refuses to CREATE a csv schedule the author could not run', async () => {
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'html_table');
- await expect(
- svc.scheduleReport({ reportId: report.id, recipients: ['a@b.c'], format: 'csv' }, CTX),
- ).rejects.toThrow(/EXPORT_NOT_PERMITTED/);
- });
-
- it('allows an html_table schedule for the same caller', async () => {
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'html_table');
- const sched = await svc.scheduleReport(
- { reportId: report.id, recipients: ['a@b.c'], format: 'html_table' },
- CTX,
- );
- expect(sched.id).toBeTruthy();
- });
- });
-
- describe('scheduled dispatch — the original side door', () => {
- it('a csv schedule created while granted STOPS delivering once the grant is revoked', async () => {
- // The reason the dispatch re-checks instead of trusting the create-time
- // check: permissions change after a schedule exists.
- const gate = vi.fn().mockResolvedValue(true);
- const svc = build({ canExport: gate });
- const report = await saveReport(svc, 'html_table');
- await svc.scheduleReport(
- { reportId: report.id, recipients: ['a@b.c'], format: 'csv', intervalMinutes: 1 },
- CTX,
- );
-
- gate.mockResolvedValue(false); // grant revoked
- // Force the schedule due (mirrors the existing dispatch suite).
- engine._tables['sys_report_schedule'][0].next_run_at = new Date(now.getTime() - 1000).toISOString();
- const out = await svc.dispatchDue();
-
- expect(out.failed).toBe(1);
- expect(email._sent).toHaveLength(0);
- const sched = engine._tables['sys_report_schedule'][0];
- expect(String(sched.last_error)).toMatch(/EXPORT_NOT_PERMITTED/);
- });
-
- it('an html_table schedule still delivers for a caller without the grant', async () => {
- const svc = build({ canExport: vi.fn().mockResolvedValue(false) });
- const report = await saveReport(svc, 'html_table');
- await svc.scheduleReport(
- { reportId: report.id, recipients: ['a@b.c'], format: 'html_table', intervalMinutes: 1 },
- CTX,
- );
- // Force the schedule due (mirrors the existing dispatch suite).
- engine._tables['sys_report_schedule'][0].next_run_at = new Date(now.getTime() - 1000).toISOString();
- const out = await svc.dispatchDue();
- expect(out.fired).toBe(1);
- expect(email._sent).toHaveLength(1);
- });
-
- it('a csv schedule delivers when the owner holds the grant', async () => {
- const svc = build();
- const report = await saveReport(svc, 'html_table');
- await svc.scheduleReport(
- { reportId: report.id, recipients: ['a@b.c'], format: 'csv', intervalMinutes: 1 },
- CTX,
- );
- // Force the schedule due (mirrors the existing dispatch suite).
- engine._tables['sys_report_schedule'][0].next_run_at = new Date(now.getTime() - 1000).toISOString();
- const out = await svc.dispatchDue();
- expect(out.fired).toBe(1);
- expect(email._sent[0].attachments?.[0]?.contentType).toBe('text/csv');
- });
- });
-});
diff --git a/packages/plugins/plugin-reports/src/report-group-posture-scope.integration.test.ts b/packages/plugins/plugin-reports/src/report-group-posture-scope.integration.test.ts
deleted file mode 100644
index 7566fe2c1d2..00000000000
--- a/packages/plugins/plugin-reports/src/report-group-posture-scope.integration.test.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
-
-/**
- * #7204 — a `group`-posture report returns the caller's membership union, the
- * same row set the caller sees interactively.
- *
- * `executeReport` used to rebuild a five-field projection of the caller's
- * execution envelope (`userId` / `tenantId` / `positions` / `permissions` /
- * `isSystem`) before handing it to the engine read that produces the report.
- * `accessible_org_ids` was not in that projection, and `buildDriverOptions`
- * reads it BY NAME (`engine.ts`, ADR-0105 D2 / #3623) to widen the driver's
- * native tenant scope to the caller's whole membership set under the `group`
- * posture. Absent, drivers "fall back to equality: fail toward isolation" — so
- * the report silently returned FEWER rows than the identical interactive query,
- * with no error and nothing in the output saying so.
- *
- * WHY THIS FILE REFUSES TO STUB THE ENGINE. The defect is invisible one layer
- * up: a fake engine that records the context it was handed can only assert that
- * a key is present, and "the key is on the object" is exactly what the previous
- * shape of this bug looked like from inside the service. The consumer is the
- * REAL `buildDriverOptions` → real `@objectstack/driver-sql` native scope, so
- * the assertions below land on ROW SETS: the union of the rows in both orgs, or
- * the equality subset. Backend is better-sqlite3 `:memory:`, the canonical
- * in-repo integration stack (PR #5715).
- *
- * The posture matrix is load-bearing, not decoration. `group` is the only
- * posture the widening applies to; `isolated`, "no provider wired" and "group
- * with an empty accessible set" must all still collapse to active-org equality
- * after the fix, or the change traded under-reporting for exposure.
- */
-
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import { ObjectKernel } from '@objectstack/core';
-import { ObjectQLPlugin } from '@objectstack/objectql';
-import type { IDataEngine } from '@objectstack/spec/contracts';
-import { SqlDriver } from '@objectstack/driver-sql';
-import { SysSavedReport, SysReportSchedule } from '@objectstack/platform-objects/audit';
-import { ReportService, type ReportEngine, type ReportEmail } from './report-service.js';
-
-/**
- * A tenant-scoped business object. `organization_id` is what makes the driver's
- * native tenant scope engage at all (`isTenancyDisabled` / the SQL driver's
- * tenant column), and `day` is a READ-TIME formula field: `applyFormulaPlan`
- * evaluates it with `execCtx.timezone`, which the same projection also dropped.
- */
-const account = {
- name: 'account',
- label: 'Account',
- fields: {
- organization_id: { name: 'organization_id', label: 'Org', type: 'text' },
- name: { name: 'name', label: 'Name', type: 'text' },
- day: {
- name: 'day',
- label: 'Calendar day',
- type: 'formula',
- expression: { dialect: 'cel', source: 'today()' },
- },
- },
-};
-
-/** The caller: active org `org_a`, membership union `org_a` + `org_b`. */
-const GROUP_CTX = {
- userId: 'u1',
- tenantId: 'org_a',
- positions: [],
- permissions: [],
- posture: 'MEMBER',
- accessible_org_ids: ['org_a', 'org_b'],
-} as any;
-
-const ids = (rows: any[]): string[] => rows.map((r) => String(r.id)).sort();
-
-/**
- * The engine surface this harness drives. `getService('objectql')` is declared
- * as the data-plane contract `IDataEngine`; the boot-time knobs below
- * (`registerDriver`, `registry`, `syncSchemas`) and the posture provider
- * plugin-security wires in production sit outside it, so they are named here
- * rather than erased with `any`.
- */
-interface TestEngine extends IDataEngine {
- registerDriver(driver: unknown, isDefault?: boolean): void;
- registry: { registerObject(def: unknown, packageId: string, namespace: string): void };
- syncSchemas(): Promise;
- setTenancyPostureProvider(provider: () => string | undefined): void;
- destroy(): Promise;
-}
-
-describe('#7204 a group-posture report reads the caller\'s whole membership union', () => {
- let objectql: TestEngine | undefined;
- let svc: ReportService;
- let email: ReportEmail & { _sent: any[] };
-
- afterEach(async () => {
- vi.useRealTimers();
- try { await objectql?.destroy(); } catch { /* noop */ }
- });
-
- beforeEach(async () => {
- const kernel = new ObjectKernel({ logger: { level: 'error' } });
- await kernel.use(new ObjectQLPlugin());
- await kernel.bootstrap();
- objectql = kernel.getService('objectql') as TestEngine;
-
- // The engine's own `init()` ran during bootstrap, before this driver
- // existed, so the connect the engine would have done is done here.
- const driver = new SqlDriver({
- client: 'better-sqlite3',
- connection: { filename: ':memory:' },
- useNullAsDefault: true,
- });
- await driver.connect();
- engine().registerDriver(driver, true);
- for (const def of [account, SysSavedReport, SysReportSchedule]) {
- engine().registry.registerObject(def, 'reports-test', 'reports-test');
- }
- await engine().syncSchemas();
-
- // Two rows in the active org, one in the other org the caller belongs to.
- for (const row of [
- { id: 'a1', organization_id: 'org_a', name: 'A1' },
- { id: 'a2', organization_id: 'org_a', name: 'A2' },
- { id: 'b1', organization_id: 'org_b', name: 'B1' },
- ]) {
- await engine().insert('account', row, { context: { isSystem: true } });
- }
-
- const sent: any[] = [];
- email = {
- _sent: sent,
- async send(input) { sent.push(input); return { status: 'sent' as const }; },
- };
- svc = new ReportService({
- engine: engine() as unknown as ReportEngine,
- email,
- // A scheduled run executes as the report's OWNER (#2849 / #2980) — the
- // resolver hands back a real RLS-bearing envelope, membership set and all.
- resolveOwnerContext: async (ownerId: string) =>
- (ownerId === 'u1' ? ({ ...GROUP_CTX } as any) : null),
- });
- });
-
- /** What plugin-security's wiring reports in a deployment of this posture. */
- const posture = (p: string | undefined) => engine().setTenancyPostureProvider(() => p);
-
- /** The booted engine — narrowed once so every call site stays typed. */
- const engine = (): TestEngine => objectql as TestEngine;
-
- const saveAccountReport = async (format = 'csv') =>
- svc.saveReport(
- { name: 'Accounts', object: 'account', query: {}, format } as any,
- GROUP_CTX,
- );
-
- describe('group posture', () => {
- beforeEach(() => posture('group'));
-
- it('a SAVED report returns the same rows as the identical interactive query', async () => {
- const interactive = await engine().find('account', {}, { context: GROUP_CTX });
- expect(ids(interactive), 'the interactive baseline is the union').toEqual(['a1', 'a2', 'b1']);
-
- const report = await svc.run((await saveAccountReport()).id, GROUP_CTX);
-
- // The card's exact claim: the saved-report path used to return FEWER rows
- // (['a1','a2'] — active-org equality) than the interactive query above.
- expect(ids(report.rows)).toEqual(ids(interactive));
- expect(report.rowCount).toBe(3);
- });
-
- it('an AD-HOC report returns the union too', async () => {
- const report = await svc.runAdHoc(
- { name: 'Ad hoc', object: 'account', query: {} } as any,
- GROUP_CTX,
- );
- expect(ids(report.rows)).toEqual(['a1', 'a2', 'b1']);
- });
-
- it('a SCHEDULED run emails the owner the union, not the active org', async () => {
- const report = await saveAccountReport();
- await svc.scheduleReport(
- { reportId: report.id, recipients: ['ops@example.com'], format: 'csv', intervalMinutes: 60 },
- GROUP_CTX,
- );
-
- const result = await svc.dispatchDue(new Date(Date.now() + 3 * 60 * 60 * 1000));
- expect(result, 'the sweep must actually fire').toMatchObject({ fired: 1, failed: 0 });
-
- const csv: string = email._sent[email._sent.length - 1]?.attachments?.[0]?.content ?? '';
- const dataRows = csv.split('\r\n').slice(1).filter(Boolean);
- expect(dataRows).toHaveLength(3);
- expect(csv).toContain('B1');
- });
-
- it('the report also sees rows the ACTIVE org has none of', async () => {
- // Nothing in org_a at all: under equality the report is empty, under the
- // union it is the one org_b row. Isolates the widening from "the active
- // org happened to hold most of the rows".
- const report = await svc.runAdHoc(
- { name: 'B only', object: 'account', query: { filter: { name: 'B1' } } } as any,
- GROUP_CTX,
- );
- expect(ids(report.rows)).toEqual(['b1']);
- });
-
- it('an EMPTY accessible set still collapses to active-org equality (fail toward isolation)', async () => {
- const ctx = { ...GROUP_CTX, accessible_org_ids: [] };
- const interactive = await engine().find('account', {}, { context: ctx });
- const report = await svc.runAdHoc({ name: 'r', object: 'account', query: {} } as any, ctx);
- expect(ids(report.rows)).toEqual(['a1', 'a2']);
- expect(ids(report.rows)).toEqual(ids(interactive));
- });
-
- it('forwards the business timezone, so a read-time formula field resolves the caller\'s calendar day', async () => {
- // 20:00Z is the day BEFORE in UTC and the day AFTER in UTC+14, so the two
- // timezones disagree deterministically at this instant. `today()` is a
- // read-time formula evaluated by `applyFormulaPlan` with `execCtx.timezone`
- // — dropped by the same projection, and observable on the row's VALUE.
- vi.useFakeTimers({ shouldAdvanceTime: true });
- vi.setSystemTime(new Date('2026-08-10T20:00:00Z'));
- const ctx = { ...GROUP_CTX, timezone: 'Pacific/Kiritimati' };
-
- const interactive = await engine().find('account', { where: { id: 'a1' } }, { context: ctx });
- const report = await svc.runAdHoc(
- { name: 'r', object: 'account', query: { filter: { id: 'a1' } } } as any,
- ctx,
- );
-
- const dayOf = (v: unknown): string =>
- String(v instanceof Date ? v.toISOString() : v).slice(0, 10);
- expect(dayOf(interactive[0]?.day), 'UTC+14 is already on the 11th').toBe('2026-08-11');
- expect(dayOf((report.rows[0] as any)?.day)).toBe(dayOf(interactive[0]?.day));
- });
- });
-
- describe('every other posture is unchanged — the widening is group-only', () => {
- it('isolated posture: the report stays at active-org equality', async () => {
- posture('isolated');
- const interactive = await engine().find('account', {}, { context: GROUP_CTX });
- const report = await svc.runAdHoc({ name: 'r', object: 'account', query: {} } as any, GROUP_CTX);
- expect(ids(report.rows)).toEqual(['a1', 'a2']);
- expect(ids(report.rows)).toEqual(ids(interactive));
- });
-
- it('no posture provider (no enforcement layer): equality, never widened', async () => {
- const interactive = await engine().find('account', {}, { context: GROUP_CTX });
- const report = await svc.runAdHoc({ name: 'r', object: 'account', query: {} } as any, GROUP_CTX);
- expect(ids(report.rows)).toEqual(['a1', 'a2']);
- expect(ids(report.rows)).toEqual(ids(interactive));
- });
- });
-});
diff --git a/packages/plugins/plugin-reports/src/report-service.test.ts b/packages/plugins/plugin-reports/src/report-service.test.ts
deleted file mode 100644
index dc0f2238949..00000000000
--- a/packages/plugins/plugin-reports/src/report-service.test.ts
+++ /dev/null
@@ -1,945 +0,0 @@
-// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-
-import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { ReportService, renderReport, type ReportEmail } from './report-service.js';
-
-// ─── Fake engine ──────────────────────────────────────────────────
-
-interface FakeRow { [k: string]: any }
-
-function makeFakeEngine() {
- const tables: Record = {};
- const ensure = (n: string) => (tables[n] ??= []);
-
- function matches(row: FakeRow, filter: any): boolean {
- if (!filter || typeof filter !== 'object') return true;
- for (const [k, v] of Object.entries(filter)) {
- if (k.startsWith('$')) throw new Error(`fake driver: unsupported operator ${k}`);
- if (row[k] !== v) return false;
- }
- return true;
- }
-
- return {
- _tables: tables,
- async find(object: string, options?: any) {
- const rows = ensure(object).filter(r => matches(r, options?.filter ?? options?.where));
- if (options?.orderBy?.[0]) {
- // Canonical SortNode key only (spec/data/query.zod.ts): the real
- // engine strips an unknown `direction:` key and defaults to asc, so
- // the mock must too — honoring both keys masks wrong-key sorts.
- const { field, order } = options.orderBy[0];
- rows.sort((a, b) => {
- const av = a[field]; const bv = b[field];
- if (av === bv) return 0;
- const cmp = av > bv ? 1 : -1;
- return order === 'desc' ? -cmp : cmp;
- });
- }
- return rows.slice(0, options?.limit ?? 1000);
- },
- async insert(object: string, data: any) {
- ensure(object).push({ ...data });
- return { ...data };
- },
- async update(object: string, idOrData: any, _opts?: any) {
- const data = typeof idOrData === 'object' ? idOrData : _opts;
- const id = typeof idOrData === 'object' ? idOrData.id : idOrData;
- const table = ensure(object);
- const i = table.findIndex(r => r.id === id);
- if (i >= 0) table[i] = { ...table[i], ...data };
- return table[i];
- },
- async delete(object: string, options?: any) {
- const table = ensure(object);
- const id = options?.where?.id ?? options?.id;
- const i = table.findIndex(r => r.id === id);
- if (i >= 0) table.splice(i, 1);
- return { id };
- },
- };
-}
-
-function makeFakeEmail() {
- const sent: any[] = [];
- const email: ReportEmail & { _sent: any[] } = {
- _sent: sent,
- async send(input) { sent.push(input); return { status: 'sent' }; },
- };
- return email;
-}
-
-const CTX = { userId: 'u1', tenantId: 't1', positions: [], permissions: [] };
-
-// ─── Rendering ─────────────────────────────────────────────────────
-
-describe('renderReport', () => {
- it('csv: escapes quotes / commas / newlines per RFC 4180', () => {
- const out = renderReport(
- [{ a: 'x,y', b: 'has "q"', c: 'line\n2' }],
- 'csv',
- ['a', 'b', 'c'],
- );
- expect(out).toBe('a,b,c\r\n"x,y","has ""q""","line\n2"');
- });
-
- it('csv: header-only when no rows', () => {
- expect(renderReport([], 'csv', ['a', 'b'])).toBe('a,b');
- });
-
- it('html_table: escapes HTML entities', () => {
- const out = renderReport([{ name: '