@happyvertical/smrt-reports 0.55.11 → 0.55.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +8 -8
package/dist/manifest.json
CHANGED
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-reports",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.13",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "7365754271fb1090d83947daf01484144c8244d2ec2771581c915f62f3b203ab",
|
|
11
|
+
"packageJson": "344c58f1fbf70202a224aa189cde1fc9495e9b3e445455b8c11e4e6b39ca4e7c",
|
|
12
12
|
"agents": "b7d07b1b9812cdc09f3e34f086f1ec33dc9d8ca53eb6dd070e91bd4783642956",
|
|
13
|
-
"moduleDoc:agents/runtime-reports.md": "
|
|
13
|
+
"moduleDoc:agents/runtime-reports.md": "0a4349b36e09dd02fda091f172973d767581e6201d486ea79a5be484e65ef18a"
|
|
14
14
|
},
|
|
15
15
|
"exports": [
|
|
16
16
|
".",
|
|
@@ -1159,7 +1159,7 @@
|
|
|
1159
1159
|
{
|
|
1160
1160
|
"path": "agents/runtime-reports.md",
|
|
1161
1161
|
"module": "runtime-reports",
|
|
1162
|
-
"content": "# Runtime reports (#3711)\n\nReports the assistant (or any caller) defines at runtime from a user request.\nA report is a **declarative spec stored as data**; there is no codegen, no new\ntable per report, and no SQL, expression, join or raw-identifier syntax in the\ngrammar. Declared (`@report`) reports are unchanged and remain the way to get a\n**materialized** report.\n\n## Files\n\n| File | Role |\n| --- | --- |\n| `src/runtime-spec.ts` | `RuntimeReportSpec` grammar, strict parser, limits, stable hash |\n| `src/runtime-compiler.ts` | `compileRuntimeReportSpec()`, `describeRuntimeReportSource()`, `runRuntimeReport()` |\n| `src/runtime-report.ts` | `RuntimeReport` model (`runtime_reports`), `saveRuntimeReport()`, list/get/archive, `runStoredRuntimeReport()` |\n\nAssistant tools live in `@happyvertical/smrt-chat` (`createRuntimeReportTools()`,\n`src/runtime-report-tools.ts`).\n\n## Spec\n\n`version: 1`, `title`, optional `description`, `source` (a server-owned source\nid), `dimensions[]` (`field`, `as?`, `bucket?`), `measures[]` (`fn`: `count`,\n`countDistinct`, `sum`, `avg`, `min`, `max`; `field?`; `as?`), `filters[]`\n(AND-ed; ops `eq ne gt gte lt lte in notIn contains isNull isNotNull`),\n`having[]` (measure alias, numeric compare), `sort[]`, `limit` (1-1000, default\n100) and a `chart` hint (`table|bar|line|pie|stat`, `x`, `y[]`). Unknown keys\nat any level are rejected. Bounds (entries, string length, `in` list size,\nalias charset, reserved words) are in `RUNTIME_REPORT_LIMITS`.\n\nThe parser normalizes defaults and derives aliases, so equal specs hash equally\n(`runtimeReportSpecHash`). The hash is what a human confirmation binds to.\n\n## Authority model\n\nA spec carries **no authority**. Everything is decided at compile time from\ninputs the caller cannot supply through the spec:\n\n- **Sources.** `RuntimeReportCompileContext.sources` is a server allow-list of\n `{ id, className, collection }`. The spec names an id; unknown and\n not-offered answer identically (`unknown_source`). `authorizeSource` is the\n host's RBAC hook (the chat tools call `run.assertOperation(collection,\n 'read')`) and runs before any field is enumerated. A host should also narrow\n `sources` to those the principal may read before compiling (the chat tools do),\n because `authorizeSource` only runs for an id that is in the list: otherwise\n an RBAC refusal for a configured id and `unknown_source` for an absent one are\n different answers, and a caller can enumerate the configured source ids. `collection` is the\n permission-catalog collection **slug** (the `<collection>` of\n `<collection>.read`, for example `runtimereports`), or the registered model\n class, which resolves to that slug through the catalog's own helper. It is\n never a class name string: `Invoice` derives `Invoice.read`, which no\n catalog contains, so every read is denied and the source is silently not\n offered.\n- **Field policy.** Names are resolved from the ObjectRegistry, never from the\n spec. Not nameable: `sensitive` / `sensitivity: sensitive|secret` fields,\n `readPermission` fields unless `context.permissions` holds that permission,\n `transient`, `json`/relationship fields, `_`-prefixed internals and the tenant\n column. A hidden field is reported exactly like an absent one\n (`unknown_field`), so the compiler is not an existence oracle.\n- **Types.** `sum`/`avg` need numeric fields, `min`/`max` numeric or datetime,\n `bucket` needs datetime, `contains` needs text, id fields accept only UUIDs\n and equality-style ops. Values are coerced to the field type, then bound.\n- **Tenancy.** Raw aggregates bypass collection interceptors, so the compiler\n adds the tenant predicate itself (same column resolution as refresh).\n Tenant-scoped source with no tenant in scope reads only NULL-tenant rows;\n there is no cross-tenant option and `withSystemContext()`/super-admin bypass\n are deliberately not honored. Sources without a tenant column are global by\n definition. `deletedAt` and STI-child discriminators are applied when the\n source has them.\n- **SQL safety.** The plan is an `AggregateSpec` executed by the SDK\n `buildAggregate()`: identifiers are registry-derived and re-validated by the\n builder, values are `$N` parameters, and output aliases are prefixed `r_`\n inside the SQL so an alias can never shadow a source column.\n- **Re-validation.** A stored report is re-parsed (hash-checked) and\n re-compiled against the **running** principal on every run, so policy changes\n apply immediately and a viewer without a permission cannot run a report saved\n by someone who had it.\n- **Stored-report storage.** The tenancy interceptor filters by the AMBIENT\n tenant, while compilation uses the principal's tenant. The chat tools bind the\n two: they enter the principal's tenant context for `list`/`get`/`save` (as the\n data-surface guard does), refuse when an ambient tenant disagrees with the\n principal, require a tenant for any stored-report use, and pass the principal\n tenant as an explicit owner filter (`getRuntimeReport`/`listRuntimeReports`\n accept `tenantId`) that still holds under a system context.\n\n`listRuntimeReports` also accepts exact `createdByUserId` and `specHash`\npredicates. They are ANDed with status and explicit/ambient tenant filters before\nlimiting results, so hosts can list a principal's saved reports or find a previous\nsave of the compiled normalized spec without retrieving other authors' reports.\nThese filters never replace authorization. See the\n[list-filter contract and test matrix](runtime-report-list-filters.md).\n\n## Storage and surface\n\n`RuntimeReport` is `@TenantScoped({ mode: 'required' })` with `spec` (normalized\nJSON text), `specHash`, denormalized `title`/`description`/`sourceId`,\n`status` (`active|archived`) and `createdByUserId`. `validateBeforeSave()` parses\nwhatever was written and re-derives every denormalized column, so columns\ncannot disagree with the spec. `getSpec()` throws if the JSON no longer matches\n`specHash` (out-of-band edit). Adopting apps need a `db:migrate` for the new\n`runtime_reports` table; runtime never creates schema.\n\n**Invariant: no row is inserted, and no spec/title/source/description/author is\nchanged, except through the confirmed save.** It is enforced in the model layer,\nnot by which surfaces exist. `validateBeforeSave()` refuses (`confirmation_required`,\n403) every insert, and every update that changes those columns, unless the\ninstance carries a confirmation proof. The proof is a symbol-keyed constructor\noption plus a module-private `WeakMap`; only `saveRuntimeReport()` can mint it,\nand only when `confirmedSpecHash` equals the compiled plan's hash (the value\nhanded to the application's `RuntimeReportConfirmationHost`). It is single use\nand tied to that exact spec hash. A REST body, a manifest tool call or\n`new RuntimeReport({...}).save()` cannot carry it, so it is refused whatever\nsurface delivered it. A reused id on an insert is refused the same way, so it\ncannot overwrite a stored spec. Archiving or unarchiving (a `status` change with\nthe stored columns untouched) is allowed for an authorised writer.\n\n**Exposed operations** (these decide the permission catalog and RLS grants):\n\n| Surface | Operations |\n| --- | --- |\n| REST (`api`) | `list`, `get`, `create` |\n| CLI | `list`, `get` |\n| MCP | `list`, `get` |\n\nThe permission catalog therefore holds exactly `<collection>.read` and\n`<collection>.create` for `RuntimeReport` (the collection slug is\n`runtimereports`). `update` and `delete` exist on no surface, so they are not in\nthe catalog, not grantable to a role and not nameable in a persona's\n`allowedTools`. REST `create` exists only so the catalog knows the operation the\nconfirmed save performs (`assertOperation(RuntimeReport, 'create')`); its body is\nalways refused by the invariant above. The generic manifest tool dispatcher in\n`@happyvertical/smrt-chat` additionally never offers or runs a write operation on\n`RuntimeReport` (`isRuntimeReportWriteOperation`), so the assistant persists a\nreport only through `reports.runtime.define` (apply).\n\n**Result rows are never persisted.** A result computed for one principal must\nnot be served to another, so the table stores definitions only.\n\n## Why not materialize\n\nDeclared reports materialize into a build-time `@smrt()` table and refresh\nthrough the lock/watermark engine. Runtime schema creation is forbidden here, so\na runtime spec runs as a bounded live aggregate on the same `AggregateSpec`\ncompile target instead. Promoting a proven runtime spec to a declared,\nmaterialized `@report` (codegen plus a migration) is a maintainer decision and\nis tracked as a follow-up.\n\n## Money\n\nMoney fields are integer minor units; `sum`/`min`/`max` keep the integer type and\nthe source field's `format` hint is carried onto the result column. Integer\nresults beyond the JavaScript safe range throw `invalid_result` rather than\nlosing precision. `avg` is decimal.\n\n## Assistant flow (`@happyvertical/smrt-chat`)\n\n`reports.runtime.sources` (field catalogue for the principal) ->\n`reports.runtime.define` `phase: 'preview'` (validate, compile, bounded sample;\nnothing stored) -> a human confirms -> `phase: 'apply'` with the previewed\n`specHash` -> `reports.runtime.list` / `reports.runtime.run`. `apply` needs an\napplication-owned `RuntimeReportConfirmationHost.confirmSave()` that resolves\nonly after a person approved that exact spec; without a host the tools are\npropose-only, and a model-supplied `confirmed` flag is ignored. Offer the tools\nvia the persona conversation `extraTools` seam; they use the same fail-closed\n`allowedTools` gates, and `RuntimeReportError.status` (422/403/500) lets\n`classifyToolError` return repairable spec problems to the model.\n\n## Not done (needs #3708 or a design call)\n\n- A `reports.runtime` recipe for `RuntimeReport` itself. `reports.materialized`\n (see `AGENTS.md`) covers declared reports only and its help points at the\n `reports.runtime.*` tools; `RuntimeReport` has no recipe of its own yet.\n- A browser UI for the confirmation host and for rendering the chart hint.\n- Archive/rename tools, scheduling a runtime report, exporting its rows.\n"
|
|
1162
|
+
"content": "# Runtime reports (#3711)\n\nReports the assistant (or any caller) defines at runtime from a user request.\nA report is a **declarative spec stored as data**; there is no codegen, no new\ntable per report, and no SQL, expression, join or raw-identifier syntax in the\ngrammar. Declared (`@report`) reports are unchanged and remain the way to get a\n**materialized** report.\n\n## Files\n\n| File | Role |\n| --- | --- |\n| `src/runtime-spec.ts` | `RuntimeReportSpec` grammar, strict parser, limits, stable hash |\n| `src/runtime-compiler.ts` | `compileRuntimeReportSpec()`, `describeRuntimeReportSource()`, `runRuntimeReport()` |\n| `src/runtime-report.ts` | `RuntimeReport` model (`runtime_reports`), `saveRuntimeReport()`, list/get/archive, `runStoredRuntimeReport()` |\n\nAssistant tools live in `@happyvertical/smrt-chat` (`createRuntimeReportTools()`,\n`src/runtime-report-tools.ts`).\n\n## Spec\n\n`version: 1`, `title`, optional `description`, `source` (a server-owned source\nid), `dimensions[]` (`field`, `as?`, `bucket?`), `measures[]` (`fn`: `count`,\n`countDistinct`, `sum`, `avg`, `min`, `max`; `field?`; `as?`), `filters[]`\n(AND-ed; ops `eq ne gt gte lt lte in notIn contains isNull isNotNull`),\n`having[]` (measure alias, numeric compare), `sort[]`, `limit` (1-1000, default\n100) and a `chart` hint (`table|bar|line|pie|stat`, `x`, `y[]`). Unknown keys\nat any level are rejected. Bounds (entries, string length, `in` list size,\nalias charset, reserved words) are in `RUNTIME_REPORT_LIMITS`.\n\nThe parser normalizes defaults and derives aliases, so equal specs hash equally\n(`runtimeReportSpecHash`). The hash is what a human confirmation binds to.\n\n## Authority model\n\nA spec carries **no authority**. Everything is decided at compile time from\ninputs the caller cannot supply through the spec:\n\n- **Sources.** `RuntimeReportCompileContext.sources` is a server allow-list of\n `{ id, className, collection }`. The spec names an id; unknown and\n not-offered answer identically (`unknown_source`). `authorizeSource` is the\n host's RBAC hook (the chat tools call `run.assertOperation(collection,\n 'read')`) and runs before any field is enumerated. A host should also narrow\n `sources` to those the principal may read before compiling (the chat tools do),\n because `authorizeSource` only runs for an id that is in the list: otherwise\n an RBAC refusal for a configured id and `unknown_source` for an absent one are\n different answers, and a caller can enumerate the configured source ids. `collection` is the\n permission-catalog collection **slug** (the `<collection>` of\n `<collection>.read`, for example `runtimereports`), or the registered model\n class, which resolves to that slug through the catalog's own helper. It is\n never a class name string: `Invoice` derives `Invoice.read`, which no\n catalog contains, so every read is denied and the source is silently not\n offered.\n- **Field policy.** Names are resolved from the ObjectRegistry, never from the\n spec. Not nameable: `sensitive` / `sensitivity: sensitive|secret` fields,\n `readPermission` fields unless `context.permissions` holds that permission,\n `transient`, `json`/relationship fields, `_`-prefixed internals and the tenant\n column. A hidden field is reported exactly like an absent one\n (`unknown_field`), so the compiler is not an existence oracle.\n- **Types.** `sum`/`avg` need numeric fields, `min`/`max` numeric or datetime,\n `bucket` needs datetime, `contains` needs text, id fields accept only UUIDs\n and equality-style ops. Values are coerced to the field type, then bound.\n- **Tenancy.** Raw aggregates bypass collection interceptors, so the compiler\n adds the tenant predicate itself (same column resolution as refresh).\n Tenant-scoped source with no tenant in scope reads only NULL-tenant rows;\n there is no cross-tenant option and `withSystemContext()`/super-admin bypass\n are deliberately not honored. Sources without a tenant column are global by\n definition. `deletedAt` and STI-child discriminators are applied when the\n source has them.\n- **SQL safety.** The plan is an `AggregateSpec` executed by the SDK\n `buildAggregate()`: identifiers are registry-derived and re-validated by the\n builder, values are `$N` parameters, and output aliases are prefixed `r_`\n inside the SQL so an alias can never shadow a source column.\n- **Re-validation.** A stored report is re-parsed (hash-checked) and\n re-compiled against the **running** principal on every run, so policy changes\n apply immediately and a viewer without a permission cannot run a report saved\n by someone who had it.\n- **Stored-report storage.** The tenancy interceptor filters by the AMBIENT\n tenant, while compilation uses the principal's tenant. The chat tools bind the\n two: they enter the principal's tenant context for `list`/`get`/`save` (as the\n data-surface guard does), refuse when an ambient tenant disagrees with the\n principal (on `apply`, before the human is asked to confirm, #3768), require\n a tenant for any stored-report use, and pass the principal\n tenant as an explicit owner filter (`getRuntimeReport`/`listRuntimeReports`\n accept `tenantId`) that still holds under a system context.\n\n`listRuntimeReports` also accepts exact `createdByUserId` and `specHash`\npredicates. They are ANDed with status and explicit/ambient tenant filters before\nlimiting results, so hosts can list a principal's saved reports or find a previous\nsave of the compiled normalized spec without retrieving other authors' reports.\nThese filters never replace authorization. See the\n[list-filter contract and test matrix](runtime-report-list-filters.md).\n\n## Storage and surface\n\n`RuntimeReport` is `@TenantScoped({ mode: 'required' })` with `spec` (normalized\nJSON text), `specHash`, denormalized `title`/`description`/`sourceId`,\n`status` (`active|archived`) and `createdByUserId`. `validateBeforeSave()` parses\nwhatever was written and re-derives every denormalized column, so columns\ncannot disagree with the spec. `getSpec()` throws if the JSON no longer matches\n`specHash` (out-of-band edit). Adopting apps need a `db:migrate` for the new\n`runtime_reports` table; runtime never creates schema.\n\n**Invariant: no row is inserted, and no spec/title/source/description/author is\nchanged, except through the confirmed save.** It is enforced in the model layer,\nnot by which surfaces exist. `validateBeforeSave()` refuses (`confirmation_required`,\n403) every insert, and every update that changes those columns, unless the\ninstance carries a confirmation proof. The proof is a symbol-keyed constructor\noption plus a module-private `WeakMap`; only `saveRuntimeReport()` can mint it,\nand only when `confirmedSpecHash` equals the compiled plan's hash (the value\nhanded to the application's `RuntimeReportConfirmationHost`). It is single use\nand tied to that exact spec hash. A REST body, a manifest tool call or\n`new RuntimeReport({...}).save()` cannot carry it, so it is refused whatever\nsurface delivered it. A reused id on an insert is refused the same way, so it\ncannot overwrite a stored spec. Archiving or unarchiving (a `status` change with\nthe stored columns untouched) is allowed for an authorised writer.\n\n**Exposed operations** (these decide the permission catalog and RLS grants):\n\n| Surface | Operations |\n| --- | --- |\n| REST (`api`) | `list`, `get`, `create` |\n| CLI | `list`, `get` |\n| MCP | `list`, `get` |\n\nThe permission catalog therefore holds exactly `<collection>.read` and\n`<collection>.create` for `RuntimeReport` (the collection slug is\n`runtimereports`). `update` and `delete` exist on no surface, so they are not in\nthe catalog, not grantable to a role and not nameable in a persona's\n`allowedTools`. REST `create` exists only so the catalog knows the operation the\nconfirmed save performs (`assertOperation(RuntimeReport, 'create')`); its body is\nalways refused by the invariant above. The generic manifest tool dispatcher in\n`@happyvertical/smrt-chat` additionally never offers or runs a write operation on\n`RuntimeReport` (`isRuntimeReportWriteOperation`), so the assistant persists a\nreport only through `reports.runtime.define` (apply).\n\n**Result rows are never persisted.** A result computed for one principal must\nnot be served to another, so the table stores definitions only.\n\n## Why not materialize\n\nDeclared reports materialize into a build-time `@smrt()` table and refresh\nthrough the lock/watermark engine. Runtime schema creation is forbidden here, so\na runtime spec runs as a bounded live aggregate on the same `AggregateSpec`\ncompile target instead. Promoting a proven runtime spec to a declared,\nmaterialized `@report` (codegen plus a migration) is a maintainer decision and\nis tracked as a follow-up.\n\n## Money\n\nMoney fields are integer minor units; `sum`/`min`/`max` keep the integer type and\nthe source field's `format` hint is carried onto the result column. Integer\nresults beyond the JavaScript safe range throw `invalid_result` rather than\nlosing precision. `avg` is decimal.\n\n## Assistant flow (`@happyvertical/smrt-chat`)\n\n`reports.runtime.sources` (field catalogue for the principal) ->\n`reports.runtime.define` `phase: 'preview'` (validate, compile, bounded sample;\nnothing stored) -> a human confirms -> `phase: 'apply'` with the previewed\n`specHash` -> `reports.runtime.list` / `reports.runtime.run`. `apply` needs an\napplication-owned `RuntimeReportConfirmationHost.confirmSave()` that resolves\nonly after a person approved that exact spec; without a host the tools are\npropose-only, and a model-supplied `confirmed` flag is ignored. Offer the tools\nvia the persona conversation `extraTools` seam; they use the same fail-closed\n`allowedTools` gates, and `RuntimeReportError.status` (422/403/500) lets\n`classifyToolError` return repairable spec problems to the model.\n\n## Not done (needs #3708 or a design call)\n\n- A `reports.runtime` recipe for `RuntimeReport` itself. `reports.materialized`\n (see `AGENTS.md`) covers declared reports only and its help points at the\n `reports.runtime.*` tools; `RuntimeReport` has no recipe of its own yet.\n- A browser UI for the confirmation host and for rendering the chart hint.\n- Archive/rename tools, scheduling a runtime report, exporting its rows.\n"
|
|
1163
1163
|
}
|
|
1164
1164
|
],
|
|
1165
1165
|
"recipes": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-reports",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.13",
|
|
4
4
|
"description": "Materialized aggregate report models for SMRT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"./manifest.json": "./dist/manifest.json"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@happyvertical/smrt-core": "0.55.
|
|
47
|
-
"@happyvertical/smrt-jobs": "0.55.
|
|
48
|
-
"@happyvertical/smrt-types": "0.55.
|
|
49
|
-
"@happyvertical/smrt-tenancy": "0.55.
|
|
50
|
-
"@happyvertical/sql": "^0.103.
|
|
46
|
+
"@happyvertical/smrt-core": "0.55.13",
|
|
47
|
+
"@happyvertical/smrt-jobs": "0.55.13",
|
|
48
|
+
"@happyvertical/smrt-types": "0.55.13",
|
|
49
|
+
"@happyvertical/smrt-tenancy": "0.55.13",
|
|
50
|
+
"@happyvertical/sql": "^0.103.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@happyvertical/smrt-scanner": "0.55.
|
|
54
|
-
"@happyvertical/smrt-vitest": "0.55.
|
|
53
|
+
"@happyvertical/smrt-scanner": "0.55.13",
|
|
54
|
+
"@happyvertical/smrt-vitest": "0.55.13",
|
|
55
55
|
"@types/node": "24.13.6",
|
|
56
56
|
"typescript": "6.0.3",
|
|
57
57
|
"vite": "8.1.4",
|