@happyvertical/smrt-reports 0.55.24 → 0.56.0
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/AGENTS.md +26 -3
- package/README.md +9 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +471 -112
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +174 -5
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +154 -0
- package/dist/playground.js.map +1 -0
- package/dist/recipes.d.ts +55 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/runtime-report.d.ts +9 -0
- package/dist/runtime-report.d.ts.map +1 -1
- package/dist/runtime-server.d.ts +77 -0
- package/dist/runtime-server.d.ts.map +1 -0
- package/dist/runtime-spec.d.ts +2 -1
- package/dist/runtime-spec.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +203 -11
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/svelte/ReportConfirmation.svelte +99 -0
- package/dist/svelte/ReportConfirmation.svelte.d.ts +15 -0
- package/dist/svelte/ReportConfirmation.svelte.d.ts.map +1 -0
- package/dist/svelte/ReportDetail.svelte +69 -0
- package/dist/svelte/ReportDetail.svelte.d.ts +11 -0
- package/dist/svelte/ReportDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/ReportPicker.svelte +73 -0
- package/dist/svelte/ReportPicker.svelte.d.ts +16 -0
- package/dist/svelte/ReportPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/ReportResult.svelte +69 -0
- package/dist/svelte/ReportResult.svelte.d.ts +10 -0
- package/dist/svelte/ReportResult.svelte.d.ts.map +1 -0
- package/dist/svelte/RuntimeReportChartWidget.svelte +20 -0
- package/dist/svelte/RuntimeReportChartWidget.svelte.d.ts +11 -0
- package/dist/svelte/RuntimeReportChartWidget.svelte.d.ts.map +1 -0
- package/dist/svelte/RuntimeReportDetailRoute.svelte +67 -0
- package/dist/svelte/RuntimeReportDetailRoute.svelte.d.ts +21 -0
- package/dist/svelte/RuntimeReportDetailRoute.svelte.d.ts.map +1 -0
- package/dist/svelte/RuntimeReportToolResult.svelte +26 -0
- package/dist/svelte/RuntimeReportToolResult.svelte.d.ts +8 -0
- package/dist/svelte/RuntimeReportToolResult.svelte.d.ts.map +1 -0
- package/dist/svelte/RuntimeReportWidgetOptions.svelte +58 -0
- package/dist/svelte/RuntimeReportWidgetOptions.svelte.d.ts +14 -0
- package/dist/svelte/RuntimeReportWidgetOptions.svelte.d.ts.map +1 -0
- package/dist/svelte/RuntimeReportsRoute.svelte +67 -0
- package/dist/svelte/RuntimeReportsRoute.svelte.d.ts +19 -0
- package/dist/svelte/RuntimeReportsRoute.svelte.d.ts.map +1 -0
- package/dist/svelte/SavedReports.svelte +114 -0
- package/dist/svelte/SavedReports.svelte.d.ts +11 -0
- package/dist/svelte/SavedReports.svelte.d.ts.map +1 -0
- package/dist/svelte/browser-client.d.ts +10 -0
- package/dist/svelte/browser-client.d.ts.map +1 -0
- package/dist/svelte/browser-client.js +16 -0
- package/dist/svelte/client.d.ts +15 -0
- package/dist/svelte/client.d.ts.map +1 -0
- package/dist/svelte/client.js +179 -0
- package/dist/svelte/index.d.ts +15 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +13 -0
- package/dist/svelte/messages.d.ts +72 -0
- package/dist/svelte/messages.d.ts.map +1 -0
- package/dist/svelte/messages.js +72 -0
- package/dist/svelte/playground.d.ts +132 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +62 -0
- package/dist/svelte/preview-data.d.ts +7 -0
- package/dist/svelte/preview-data.d.ts.map +1 -0
- package/dist/svelte/preview-data.js +59 -0
- package/dist/svelte/result.d.ts +8 -0
- package/dist/svelte/result.d.ts.map +1 -0
- package/dist/svelte/result.js +99 -0
- package/dist/svelte/spec.d.ts +5 -0
- package/dist/svelte/spec.d.ts.map +1 -0
- package/dist/svelte/spec.js +282 -0
- package/dist/svelte/tool-result.d.ts +11 -0
- package/dist/svelte/tool-result.d.ts.map +1 -0
- package/dist/svelte/tool-result.js +19 -0
- package/dist/svelte/types.d.ts +38 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +1 -0
- package/package.json +36 -12
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-reports",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.56.0",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/runtime-
|
|
14
|
-
"moduleDoc:
|
|
10
|
+
"manifest": "78e7207f77b8a88d0c99a8490057f9b555b1a51fe95f6648857731178a6e3bb2",
|
|
11
|
+
"packageJson": "d8c9bd2772716d67e8fbd28655d4aa7e56c7985f3fb1b8fb5d54f89c1861fbd4",
|
|
12
|
+
"agents": "c134555effb7869d3fbd3b5fb2be79bca10d0644425aacd9654869a517cc78a3",
|
|
13
|
+
"moduleDoc:agents/runtime-report-browser.md": "7b35c4b84c8a7dfc733198f793c701743ba6d7cb586ec9b671bddd645ecf0e8d",
|
|
14
|
+
"moduleDoc:agents/runtime-reports.md": "04cc898425ae1af4541056e6abd2f48c381ba20b7322b87cc45fd1afb1392313",
|
|
15
|
+
"moduleDoc:README.md": "a9c29e114a9de9e3090690ba9744fa3118e7ef710c44c8e1cc13e2b87901ec35"
|
|
15
16
|
},
|
|
16
17
|
"exports": [
|
|
17
18
|
".",
|
|
@@ -20,9 +21,11 @@
|
|
|
20
21
|
"./decorators",
|
|
21
22
|
"./manifest",
|
|
22
23
|
"./manifest.json",
|
|
24
|
+
"./playground",
|
|
23
25
|
"./refresh",
|
|
24
26
|
"./scheduler",
|
|
25
|
-
"./state"
|
|
27
|
+
"./state",
|
|
28
|
+
"./svelte"
|
|
26
29
|
],
|
|
27
30
|
"dependencies": {
|
|
28
31
|
"@happyvertical/smrt-core": "workspace:*",
|
|
@@ -30,19 +33,28 @@
|
|
|
30
33
|
"@happyvertical/smrt-types": "workspace:*",
|
|
31
34
|
"@happyvertical/smrt-tenancy": "workspace:*",
|
|
32
35
|
"@happyvertical/sql": "catalog:",
|
|
36
|
+
"@happyvertical/smrt-config": "workspace:*",
|
|
37
|
+
"@happyvertical/smrt-ui": "workspace:*",
|
|
33
38
|
"@happyvertical/smrt-scanner": "workspace:*",
|
|
34
39
|
"@happyvertical/smrt-vitest": "workspace:*",
|
|
35
40
|
"@types/node": "24.13.6",
|
|
36
41
|
"typescript": "6.0.3",
|
|
37
42
|
"vite": "8.1.4",
|
|
38
|
-
"vitest": "4.1.10"
|
|
43
|
+
"vitest": "4.1.10",
|
|
44
|
+
"@sveltejs/package": "^2.5.8",
|
|
45
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
46
|
+
"@testing-library/svelte": "^5.4.2",
|
|
47
|
+
"svelte-check": "^4.7.6",
|
|
48
|
+
"svelte": "^5.57.0"
|
|
39
49
|
},
|
|
40
50
|
"smrtDependencies": [
|
|
51
|
+
"@happyvertical/smrt-config",
|
|
41
52
|
"@happyvertical/smrt-core",
|
|
42
53
|
"@happyvertical/smrt-jobs",
|
|
43
54
|
"@happyvertical/smrt-scanner",
|
|
44
55
|
"@happyvertical/smrt-tenancy",
|
|
45
56
|
"@happyvertical/smrt-types",
|
|
57
|
+
"@happyvertical/smrt-ui",
|
|
46
58
|
"@happyvertical/smrt-vitest"
|
|
47
59
|
],
|
|
48
60
|
"sdkDependencies": [
|
|
@@ -205,14 +217,29 @@
|
|
|
205
217
|
],
|
|
206
218
|
"relationships": [],
|
|
207
219
|
"methods": [
|
|
220
|
+
"archive",
|
|
208
221
|
"getSpec",
|
|
222
|
+
"run",
|
|
209
223
|
"setSpec"
|
|
210
224
|
],
|
|
211
225
|
"methodSignatures": [
|
|
226
|
+
{
|
|
227
|
+
"name": "archive",
|
|
228
|
+
"async": true,
|
|
229
|
+
"params": [
|
|
230
|
+
"options: object"
|
|
231
|
+
],
|
|
232
|
+
"returns": "Promise<object>"
|
|
233
|
+
},
|
|
212
234
|
{
|
|
213
235
|
"name": "getSpec",
|
|
214
236
|
"returns": "RuntimeReportSpec"
|
|
215
237
|
},
|
|
238
|
+
{
|
|
239
|
+
"name": "run",
|
|
240
|
+
"async": true,
|
|
241
|
+
"returns": "Promise<RuntimeReportResult>"
|
|
242
|
+
},
|
|
216
243
|
{
|
|
217
244
|
"name": "setSpec",
|
|
218
245
|
"params": [
|
|
@@ -256,6 +283,22 @@
|
|
|
256
283
|
"path": "/runtimereports",
|
|
257
284
|
"method": "POST"
|
|
258
285
|
},
|
|
286
|
+
{
|
|
287
|
+
"kind": "api",
|
|
288
|
+
"name": "runtimereports.run",
|
|
289
|
+
"operation": "run",
|
|
290
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport",
|
|
291
|
+
"path": "/runtimereports/[id]/run",
|
|
292
|
+
"method": "POST"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"kind": "api",
|
|
296
|
+
"name": "runtimereports.archive",
|
|
297
|
+
"operation": "archive",
|
|
298
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport",
|
|
299
|
+
"path": "/runtimereports/[id]/archive",
|
|
300
|
+
"method": "POST"
|
|
301
|
+
},
|
|
259
302
|
{
|
|
260
303
|
"kind": "cli",
|
|
261
304
|
"name": "runtimereport_list",
|
|
@@ -268,6 +311,18 @@
|
|
|
268
311
|
"operation": "get",
|
|
269
312
|
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
270
313
|
},
|
|
314
|
+
{
|
|
315
|
+
"kind": "cli",
|
|
316
|
+
"name": "runtimereport_run",
|
|
317
|
+
"operation": "run",
|
|
318
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"kind": "cli",
|
|
322
|
+
"name": "runtimereport_archive",
|
|
323
|
+
"operation": "archive",
|
|
324
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
325
|
+
},
|
|
271
326
|
{
|
|
272
327
|
"kind": "mcp",
|
|
273
328
|
"name": "runtimereport_list",
|
|
@@ -279,6 +334,18 @@
|
|
|
279
334
|
"name": "runtimereport_get",
|
|
280
335
|
"operation": "get",
|
|
281
336
|
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"kind": "mcp",
|
|
340
|
+
"name": "runtimereport_run",
|
|
341
|
+
"operation": "run",
|
|
342
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"kind": "mcp",
|
|
346
|
+
"name": "runtimereport_archive",
|
|
347
|
+
"operation": "archive",
|
|
348
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
282
349
|
}
|
|
283
350
|
],
|
|
284
351
|
"withheldSurfaces": [
|
|
@@ -1049,6 +1116,22 @@
|
|
|
1049
1116
|
"path": "/runtimereports",
|
|
1050
1117
|
"method": "POST"
|
|
1051
1118
|
},
|
|
1119
|
+
{
|
|
1120
|
+
"kind": "api",
|
|
1121
|
+
"name": "runtimereports.run",
|
|
1122
|
+
"operation": "run",
|
|
1123
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport",
|
|
1124
|
+
"path": "/runtimereports/[id]/run",
|
|
1125
|
+
"method": "POST"
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
"kind": "api",
|
|
1129
|
+
"name": "runtimereports.archive",
|
|
1130
|
+
"operation": "archive",
|
|
1131
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport",
|
|
1132
|
+
"path": "/runtimereports/[id]/archive",
|
|
1133
|
+
"method": "POST"
|
|
1134
|
+
},
|
|
1052
1135
|
{
|
|
1053
1136
|
"kind": "cli",
|
|
1054
1137
|
"name": "runtimereport_list",
|
|
@@ -1061,6 +1144,18 @@
|
|
|
1061
1144
|
"operation": "get",
|
|
1062
1145
|
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
1063
1146
|
},
|
|
1147
|
+
{
|
|
1148
|
+
"kind": "cli",
|
|
1149
|
+
"name": "runtimereport_run",
|
|
1150
|
+
"operation": "run",
|
|
1151
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
"kind": "cli",
|
|
1155
|
+
"name": "runtimereport_archive",
|
|
1156
|
+
"operation": "archive",
|
|
1157
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
1158
|
+
},
|
|
1064
1159
|
{
|
|
1065
1160
|
"kind": "mcp",
|
|
1066
1161
|
"name": "runtimereport_list",
|
|
@@ -1073,6 +1168,18 @@
|
|
|
1073
1168
|
"operation": "get",
|
|
1074
1169
|
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
1075
1170
|
},
|
|
1171
|
+
{
|
|
1172
|
+
"kind": "mcp",
|
|
1173
|
+
"name": "runtimereport_run",
|
|
1174
|
+
"operation": "run",
|
|
1175
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
"kind": "mcp",
|
|
1179
|
+
"name": "runtimereport_archive",
|
|
1180
|
+
"operation": "archive",
|
|
1181
|
+
"objectName": "@happyvertical/smrt-reports:RuntimeReport"
|
|
1182
|
+
},
|
|
1076
1183
|
{
|
|
1077
1184
|
"kind": "cli",
|
|
1078
1185
|
"name": "smrtreportrefreshtask_list",
|
|
@@ -1155,17 +1262,22 @@
|
|
|
1155
1262
|
"polymorphicAssociations": 0,
|
|
1156
1263
|
"uuidColumns": 24
|
|
1157
1264
|
},
|
|
1158
|
-
"agentDoc": "# @happyvertical/smrt-reports\n\nMaterialized aggregate report models for SMRT.\n\n## Key Pieces\n\n| Module | Purpose |\n| --- | --- |\n| `SmrtReport` | Abstract report row base with `refreshedAt`, `isStale()`, and manual `refresh()` |\n| decorators | `@report`, grouping decorators, time buckets, and aggregate measure decorators |\n| compiler | Pure `ReportDefinition -> AggregateSpec` compiler and ObjectRegistry adapter |\n| aggregate | Compatibility re-export of the SDK aggregate query builder |\n| refresh | Rebuild and incremental refresh engine with run tracking, watermarks, locks, and tenant scoping |\n| state | Internal `_smrt_report_*` system models for runs, watermarks, locks, schedules, and refresh tasks |\n| scheduler | Cron schedule runner, durable refresh job enqueueing, and `onChange` interceptor registration |\n| adapter | Transport-neutral report descriptor, canonical materialized-row reads, and stable `id` row identity |\n| lifecycle | Tenant-safe freshness, run, lock, failure, and manual refresh preview/apply surfaces |\n| views | Policy-revalidated saved views, snapshot-bound exports, bounded job handoff, and artifact metadata validation |\n| runtime reports | Declarative `RuntimeReportSpec` stored as data (`RuntimeReport`), compiled per principal with field-policy, RBAC and tenant checks; see [agents/runtime-reports.md](agents/runtime-reports.md) |\n\n## Adapter contract\n\n- `buildReportAdapterDescriptor()` returns deterministic, serializable metadata\n for a report surface: a stable resource id, typed persisted report columns,\n the canonical `DataQuerySchema`, and UI-neutral DataTable hints. It must not\n import `smrt-ui` or expose a report-domain class to the consumer.\n- `queryReportMaterializedRows()` owns only the bounded read slice for rows that\n are already materialized. It supports projection, offset/limit paging,\n validated filters, deterministic multi-sort with an `id` tie-breaker, exact\n totals, and dimension facets. At source-query compilation,\n dimension and bucket filters compile to `WHERE`, aggregate-measure filters\n compile to `HAVING`, and mixed `OR`/`NOT` filter scopes fail closed.\n- `id` is the only row identity. It must be a non-empty persisted string and is\n never replaced by a display index or page position.\n- The descriptor is an exposure boundary. Sensitive/secret fields, fields with\n `readPermission`, and transient, system, or non-column fields fail closed and\n do not become public columns when no principal is available.\n- `tenantScoped`/`tenantField` reflect actual registered tenant metadata. A\n `tenantScope` option only contributes to the stable resource id; it is not\n authorization. Pass the same `adapter` options to\n `queryReportMaterializedRows()` that were used to build the selected\n descriptor, so results and background tasks retain that stable resource id.\n The default query path resolves the registered collection via `ObjectRegistry`,\n so normal collection tenancy interceptors apply. An injected collection is\n application-owned and must preserve the same boundary.\n- `refresh` is a declaration, not execution. It describes configured mode,\n triggers, positive-TTL stale-read behavior, and a permissioned/audited action\n with preview/apply phases. The adapter remains read-only, while\n `getReportLifecycle()` provides an explicit, tenant-safe lifecycle snapshot\n and `previewReportRefresh()` / `applyReportRefresh()` delegate authorization,\n audit, and queueing through an application action host. Only a registered\n `SmrtReportCollection` may synchronously refresh stale reads, when its TTL is\n positive and the report is not manual.\n- `views` accepts no authority. Persisted views must be normalized again through\n the current descriptor before restoring them, so changed column policy or a\n changed report definition fails closed. Treat unversioned persisted layouts as\n legacy v0 and migrate them to v1 before normalization; unknown versions must\n fail rather than being inferred. Export snapshots retain the canonical\n query fingerprint, projection, sort, as-of/freshness state, exact row count,\n and the fixed principal/tenant/report-definition/field-policy inheritance\n contract. A snapshot needs an application-host-issued opaque binding and an\n explicit offset-page read plan: workers advance it from offset zero instead\n of reusing a visible page. Preview, apply, worker, and artifact-serving\n boundaries must call the validators, including the host's immutable-snapshot\n assertion; application hosts own authorization, audit records, durable\n storage, download tokens, and queue execution.\n\n## Runtime reports (#3711)\n\nA runtime report is data, not code: `parseRuntimeReportSpec()` accepts a closed\ngrammar (no SQL, expressions, joins, tables, or tenants), and\n`compileRuntimeReportSpec()` resolves it against a server-owned source\nallow-list (including optional `fields`/`excludeFields`), the ObjectRegistry field policy, the live principal's permissions,\nand an explicit fail-closed tenant predicate. Stored `RuntimeReport` specs are\nre-compiled for the running principal on every run, and result rows are never\npersisted. Assistant tools are in `@happyvertical/smrt-chat`. The full\ncontract, authority model, and deferred work are in\n[agents/runtime-reports.md](agents/runtime-reports.md).\n\n## Recipes (#3719)\n\n`src/recipes.ts` declares `reports.materialized` (`MaterializedReportsRecipe`),\nemitted into the built `manifest.json` and `smrt-knowledge.json` `recipes`. It\nbrings only the internal refresh bookkeeping tables (runs, watermarks, locks,\nschedules, refresh tasks); an app's own `@report` classes are not listed because\nthey belong to the app. Those tables have no REST or MCP surface, so the recipe\nhas no `nav`. It declares `runtime: 'server'` (SQL aggregation, jobs) and no\n`surfaces`: this package ships no Svelte components, and declaring one that does\nnot exist is a scan-time lie. Do not add a surface until a real export exists.\n\nUser-facing help is `src/materialized.recipe.md` (#3591). It explains how the\nassistant reads declared reports (`data.discover`/`data.inspect` and\n`reports.query`/`reports.refresh` from `@happyvertical/smrt-agents`) and how\nruntime reports (`reports.runtime.*` in `@happyvertical/smrt-chat`) differ: live,\nnever stored, saved only after a person confirms. Keep that split accurate when\neither tool set changes. `{field:...}` references need a `@field({ description })`\non `state.ts` so the glossary can explain them. `src/__tests__/recipes.test.ts`\nruns the real scan, manifest and generation passes in memory and renders the help.\nSee [core recipes](../core/agents/recipes.md).\n\n## Conventions\n\n- Report cache tables are normal `@smrt()` tables. Runtime refresh must not create schema.\n- Store report metadata under field `_meta.__report`; scanner and runtime decorators must stay aligned.\n- Keep SQL generation portable. Use the SDK `buildAggregate()`/`bucketExpr()` helpers for time buckets and `$N` placeholders.\n- Do not add a local aggregate SQL builder here; the implementation lives in `@happyvertical/sql`.\n- Refresh runtime tables are schema-managed. Runtime refresh must fail clearly when `_smrt_report_runs`, `_smrt_report_watermarks`, or `_smrt_report_locks` have not been migrated.\n- Incremental refresh requires a source watermark column (default `updatedAt`) and soft-delete column (default `deletedAt`); it recomputes affected groups and deletes empty report groups instead of applying aggregate deltas.\n- Raw aggregate refreshes must explicitly filter `tenant_id`; the tenancy interceptor only protects normal collection reads.\n- Scheduled/on-change refreshes enqueue `SmrtReportRefreshTask.run()` through `@happyvertical/smrt-jobs`; do not add a separate report queue.\n- `SmrtReportCollection` is a framework collection base (core's `SMRT_COLLECTION_BASE_NAMES`): a report collection never owns its model's table, even when an app manifest attributes the two to different packages. `pnpm test:postgres` runs the `*.optional.test.ts` lane (#3110).\n\n## Package registration\n\n`src/__smrt-register__.ts` embeds the provider manifest during library builds.\nImport it before model decorators, including model-bearing subpaths (`state`\nand `scheduler`), so consumer SSR bundles retain reports-qualified identities.\nThe bundle gate executes these public entries in a fresh consumer process.\nAudit #3525 found no other published domain model package missing its shim;\npackages without one contain framework utilities or example/test decorators.\n\n## Schedule lifecycle\n\n`tenantFanout: 'discover'` resolves non-null tenants in the source and existing\nmaterialization at execution time only for signed global scheduled maintenance.\nInclude materialized scopes so rebuilds clear groups after their last source\nrow is deleted; discovery does not change incremental watermark semantics. Empty discovery must not call\nrefresh with an empty tenant array, which would select the global slice.\nSchedule provisioning preserves existing state. Hosts bind terminal job events\nwith `ReportScheduleRunner.bindTaskRunner()` and dispose the returned binding\non shutdown; see [scheduled refresh lifecycle](README.md#scheduled-refresh-lifecycle).\n",
|
|
1265
|
+
"agentDoc": "# @happyvertical/smrt-reports\n\nMaterialized aggregate report models and runtime report browser surfaces for SMRT.\n\n## Browser surfaces (#3895)\n\n`./svelte` publishes `SavedReports`, `ReportDetail`, `ReportResult`,\n`ReportConfirmation`, and `ReportPicker`; visual chart renderers come from\n`@happyvertical/smrt-ui/charts`. Keep runtime imports out of this subpath:\nserver report types are type-only imports. See [browser contract](agents/runtime-report-browser.md).\n`pnpm typecheck` includes Svelte diagnostics and `pnpm test` includes browser\ncomponent tests. `./playground` supplies each screen's preview.\n\n## Key Pieces\n\n| Module | Purpose |\n| --- | --- |\n| `SmrtReport` | Abstract report row base with `refreshedAt`, `isStale()`, and manual `refresh()` |\n| decorators | `@report`, grouping decorators, time buckets, and aggregate measure decorators |\n| compiler | Pure `ReportDefinition -> AggregateSpec` compiler and ObjectRegistry adapter |\n| aggregate | Compatibility re-export of the SDK aggregate query builder |\n| refresh | Rebuild and incremental refresh engine with run tracking, watermarks, locks, and tenant scoping |\n| state | Internal `_smrt_report_*` system models for runs, watermarks, locks, schedules, and refresh tasks |\n| scheduler | Cron schedule runner, durable refresh job enqueueing, and `onChange` interceptor registration |\n| adapter | Transport-neutral report descriptor, canonical materialized-row reads, and stable `id` row identity |\n| lifecycle | Tenant-safe freshness, run, lock, failure, and manual refresh preview/apply surfaces |\n| views | Policy-revalidated saved views, snapshot-bound exports, bounded job handoff, and artifact metadata validation |\n| runtime reports | Declarative `RuntimeReportSpec` stored as data (`RuntimeReport`), compiled per principal with field-policy, RBAC and tenant checks; see [agents/runtime-reports.md](agents/runtime-reports.md) |\n\n## Adapter contract\n\n- `buildReportAdapterDescriptor()` returns deterministic, serializable metadata\n for a report surface: a stable resource id, typed persisted report columns,\n the canonical `DataQuerySchema`, and UI-neutral DataTable hints. It must not\n import `smrt-ui` or expose a report-domain class to the consumer.\n- `queryReportMaterializedRows()` owns only the bounded read slice for rows that\n are already materialized. It supports projection, offset/limit paging,\n validated filters, deterministic multi-sort with an `id` tie-breaker, exact\n totals, and dimension facets. At source-query compilation,\n dimension and bucket filters compile to `WHERE`, aggregate-measure filters\n compile to `HAVING`, and mixed `OR`/`NOT` filter scopes fail closed.\n- `id` is the only row identity. It must be a non-empty persisted string and is\n never replaced by a display index or page position.\n- The descriptor is an exposure boundary. Sensitive/secret fields, fields with\n `readPermission`, and transient, system, or non-column fields fail closed and\n do not become public columns when no principal is available.\n- `tenantScoped`/`tenantField` reflect actual registered tenant metadata. A\n `tenantScope` option only contributes to the stable resource id; it is not\n authorization. Pass the same `adapter` options to\n `queryReportMaterializedRows()` that were used to build the selected\n descriptor, so results and background tasks retain that stable resource id.\n The default query path resolves the registered collection via `ObjectRegistry`,\n so normal collection tenancy interceptors apply. An injected collection is\n application-owned and must preserve the same boundary.\n- `refresh` is a declaration, not execution. It describes configured mode,\n triggers, positive-TTL stale-read behavior, and a permissioned/audited action\n with preview/apply phases. The adapter remains read-only, while\n `getReportLifecycle()` provides an explicit, tenant-safe lifecycle snapshot\n and `previewReportRefresh()` / `applyReportRefresh()` delegate authorization,\n audit, and queueing through an application action host. Only a registered\n `SmrtReportCollection` may synchronously refresh stale reads, when its TTL is\n positive and the report is not manual.\n- `views` accepts no authority. Persisted views must be normalized again through\n the current descriptor before restoring them, so changed column policy or a\n changed report definition fails closed. Treat unversioned persisted layouts as\n legacy v0 and migrate them to v1 before normalization; unknown versions must\n fail rather than being inferred. Export snapshots retain the canonical\n query fingerprint, projection, sort, as-of/freshness state, exact row count,\n and the fixed principal/tenant/report-definition/field-policy inheritance\n contract. A snapshot needs an application-host-issued opaque binding and an\n explicit offset-page read plan: workers advance it from offset zero instead\n of reusing a visible page. Preview, apply, worker, and artifact-serving\n boundaries must call the validators, including the host's immutable-snapshot\n assertion; application hosts own authorization, audit records, durable\n storage, download tokens, and queue execution.\n\n## Runtime reports (#3711)\n\nA runtime report is data, not code: `parseRuntimeReportSpec()` accepts a closed\ngrammar (no SQL, expressions, joins, tables, or tenants), and\n`compileRuntimeReportSpec()` resolves it against a server-owned source\nallow-list (including optional `fields`/`excludeFields`), the ObjectRegistry field policy, the live principal's permissions,\nand an explicit fail-closed tenant predicate. Stored `RuntimeReport` specs are\nre-compiled for the running principal on every run, and result rows are never\npersisted. Assistant tools are in `@happyvertical/smrt-chat`. The full\ncontract, authority model, and deferred work are in\n[agents/runtime-reports.md](agents/runtime-reports.md).\n\n## Recipes (#3719)\n\n`src/recipes.ts` declares `reports.materialized` (`MaterializedReportsRecipe`),\nemitted into the built `manifest.json` and `smrt-knowledge.json` `recipes`. It\nbrings only the internal refresh bookkeeping tables (runs, watermarks, locks,\nschedules, refresh tasks); an app's own `@report` classes are not listed because\nthey belong to the app. Those tables have no REST or MCP surface, so the recipe\nhas no `nav`. It declares `runtime: 'server'` (SQL aggregation, jobs) and no\n`surfaces`: materialized reports do not yet have a browser surface. Runtime\nreport browser components stay on the `./svelte` subpath, while\n`RuntimeReportsRecipe` names their route, widget, and options-editor exports.\n\nUser-facing help is `src/materialized.recipe.md` (#3591). It explains how the\nassistant reads declared reports (`data.discover`/`data.inspect` and\n`reports.query`/`reports.refresh` from `@happyvertical/smrt-agents`) and how\nruntime reports (`reports.runtime.*` in `@happyvertical/smrt-chat`) differ: live,\nnever stored, saved only after a person confirms. Keep that split accurate when\neither tool set changes. `{field:...}` references need a `@field({ description })`\non `state.ts` so the glossary can explain them. `src/__tests__/recipes.test.ts`\nruns the real scan, manifest and generation passes in memory and renders the help.\nSee [core recipes](../core/agents/recipes.md).\n\n## Conventions\n\n- Report cache tables are normal `@smrt()` tables. Runtime refresh must not create schema.\n- Store report metadata under field `_meta.__report`; scanner and runtime decorators must stay aligned.\n- Keep SQL generation portable. Use the SDK `buildAggregate()`/`bucketExpr()` helpers for time buckets and `$N` placeholders.\n- Do not add a local aggregate SQL builder here; the implementation lives in `@happyvertical/sql`.\n- Refresh runtime tables are schema-managed. Runtime refresh must fail clearly when `_smrt_report_runs`, `_smrt_report_watermarks`, or `_smrt_report_locks` have not been migrated.\n- Incremental refresh requires a source watermark column (default `updatedAt`) and soft-delete column (default `deletedAt`); it recomputes affected groups and deletes empty report groups instead of applying aggregate deltas.\n- Raw aggregate refreshes must explicitly filter `tenant_id`; the tenancy interceptor only protects normal collection reads.\n- Scheduled/on-change refreshes enqueue `SmrtReportRefreshTask.run()` through `@happyvertical/smrt-jobs`; do not add a separate report queue.\n- `SmrtReportCollection` is a framework collection base (core's `SMRT_COLLECTION_BASE_NAMES`): a report collection never owns its model's table, even when an app manifest attributes the two to different packages. `pnpm test:postgres` runs the `*.optional.test.ts` lane (#3110).\n\n## Package registration\n\n`src/__smrt-register__.ts` embeds the provider manifest during library builds.\nImport it before model decorators, including model-bearing subpaths (`state`\nand `scheduler`), so consumer SSR bundles retain reports-qualified identities.\nThe bundle gate executes these public entries in a fresh consumer process.\nAudit #3525 found no other published domain model package missing its shim;\npackages without one contain framework utilities or example/test decorators.\n\n## Schedule lifecycle\n\n`tenantFanout: 'discover'` resolves non-null tenants in the source and existing\nmaterialization at execution time only for signed global scheduled maintenance.\nInclude materialized scopes so rebuilds clear groups after their last source\nrow is deleted; discovery does not change incremental watermark semantics. Empty discovery must not call\nrefresh with an empty tenant array, which would select the global slice.\nSchedule provisioning preserves existing state. Hosts bind terminal job events\nwith `ReportScheduleRunner.bindTaskRunner()` and dispose the returned binding\non shutdown; see [scheduled refresh lifecycle](README.md#scheduled-refresh-lifecycle).\n\n## Runtime server surfaces (#3878)\n\n`runtime-server.ts` owns the framework-neutral confirmation handler, current\nprincipal run/archive helpers and chart loader. Source allow-lists come from\n`packages.reports.runtime.sources`; no configuration means no sources. Never\naccept identity or source configuration from JSON. The RuntimeReport run/archive\nmethods reuse read/create grants through `operationPermissions`, including the\narchive UPDATE RLS binding. `RuntimeReportsRecipe` declares nav, route and\n`runtime-report-chart` surfaces, and server runtime. The chart loader receives\n`reportPrincipal` only from authenticated host context. See\n[runtime contract](agents/runtime-reports.md) and\n[browser surfaces](agents/runtime-report-browser.md).\n",
|
|
1159
1266
|
"moduleDocs": [
|
|
1267
|
+
{
|
|
1268
|
+
"path": "agents/runtime-report-browser.md",
|
|
1269
|
+
"module": "runtime-report-browser",
|
|
1270
|
+
"content": "# Runtime report browser surfaces (#3895)\n\nThe browser entry is `@happyvertical/smrt-reports/svelte`. It imports visual and\nform primitives from smrt-ui, never smrt-svelte or the reports server barrel.\n`@happyvertical/smrt-ui/charts` owns the five shared renderers; the overview\nchart and report results both consume it.\n\n- `SavedReports`: a tenant-shared list, Only mine and archived filters, author,\n source and optional last-run hint. The host resolves author ids and supplies\n an archive capability hint; the server authorizes every archive again.\n- `ReportDetail`: gets a definition and runs it on open and each re-run. The\n transport is principal-scoped; replace it when viewer/tenant changes. Pending\n old responses are discarded, and result rows live only in component state.\n- `ReportResult`: validates the result shape and safe integers, renders the\n chart hint and offers the complete table. Money fields use integer minor\n units and the host's ISO currency (default USD); locale defaults to `en`.\n- `ReportConfirmation`: shows the source label, groupings, measures,\n filters, and a bounded preview. Approve requires a trusted local click and\n posts the exact displayed `{ spec, specHash }` to the proposal's same-origin\n `confirmPath` once while pending. Decline never calls the server. The server\n recompiles as the confirming principal and idempotently saves the approved\n spec; the hash carries no authority by itself.\n- `ReportPicker`: offers readable active reports and their measures and\n dimensions; changing the report clears dependent choices.\n\n`createRuntimeReportsHttpClient()` implements the generated same-origin reads,\nthe enveloped `run`/`archive` actions, and the confirmation POST. It has no\ndatabase, tenant, or permission authority. Successful run timestamps live only\nin that browser client's session map; route hosts include an opaque active\ntenant/session scope so hints are discarded when the principal context changes. Omit the client (or set\n`serverAvailable={false}` on a route component) for a static host: the screens\nexplain that reports need a server and make no requests.\n\n`runtimeReportToolResultComponent` is the reports adapter for AssistantDock's\ngeneric component seam. Recipe routes export `RuntimeReportsRoute` and\n`RuntimeReportDetailRoute`; the `runtime-report-chart` surface exports its\nrenderer and dependent report/measure/dimension editor. The server host builds\nthe chart loader context from request-scoped `reportPrincipal`; widget options\nnever carry identity, permissions, tenant, or database authority.\n\nThe `./playground` entry has saved-list, detail, confirmation, picker, empty and\nunavailable previews. They use deterministic sample data, not a production\nbackend. All component copy registers through `smrt-ui/i18n`.\n\nThe template reference application exercises the generic dock selector, a real\ntrusted browser approval click, authenticated confirmation and generated reads,\ndetail execution, and the recipe-registered overview loader/renderer without\nmocking an endpoint.\n"
|
|
1271
|
+
},
|
|
1160
1272
|
{
|
|
1161
1273
|
"path": "agents/runtime-reports.md",
|
|
1162
1274
|
"module": "runtime-reports",
|
|
1163
|
-
"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\nSource definitions may set `fields` (an allow-list) and `excludeFields` (a deny-list).\nBoth use exact public model field names. Omitted `fields` retains all eligible\nfields; `fields: []` exposes none. Exclusions win, and neither option bypasses\nmodel sensitivity, field permissions, or tenant restrictions. Invalid lists and\nunknown names reject the source with `unsupported_source`, including misspelled\nexclusions. Descriptions, assistant tools, confirmation and saved-report runs\nresolve this same policy on every invocation.\n\nStandard timestamps have one public spelling: `createdAt` and `updatedAt`.\nSnake-case `created_at`/`updated_at` remain physical SQL columns, not accepted\nspec or configuration field names. Existing saved specs using those aliases must\nbe proposed again with canonical names and confirmed. Model restrictions on any\nalias protect the whole physical column; host exclusions also cover aliases.\n\n```typescript\n{ id: 'incidents', className: 'Incident', collection: 'incidents',\n fields: ['id', 'title', 'createdAt', 'status'],\n excludeFields: ['signatureAssetId'] }\n```\nThe example assumes `signatureAssetId` is a declared model field; exclusions\nare validated even if an allow-list already omits that field.\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"
|
|
1275
|
+
"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-server.ts` | Same-origin confirmation, principal-bound run/archive, chart loader |\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\nSource definitions may set `fields` (an allow-list) and `excludeFields` (a deny-list).\nBoth use exact public model field names. Omitted `fields` retains all eligible\nfields; `fields: []` exposes none. Exclusions win, and neither option bypasses\nmodel sensitivity, field permissions, or tenant restrictions. Invalid lists and\nunknown names reject the source with `unsupported_source`, including misspelled\nexclusions. Descriptions, assistant tools, confirmation and saved-report runs\nresolve this same policy on every invocation.\n\nStandard timestamps have one public spelling: `createdAt` and `updatedAt`.\nSnake-case `created_at`/`updated_at` remain physical SQL columns, not accepted\nspec or configuration field names. Existing saved specs using those aliases must\nbe proposed again with canonical names and confirmed. Model restrictions on any\nalias protect the whole physical column; host exclusions also cover aliases.\n\n```typescript\n{ id: 'incidents', className: 'Incident', collection: 'incidents',\n fields: ['id', 'title', 'createdAt', 'status'],\n excludeFields: ['signatureAssetId'] }\n```\nThe example assumes `signatureAssetId` is a declared model field; exclusions\nare validated even if an allow-list already omits that field.\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. Existing deployments also\nrun `db:migrate` for the confirmation lookup index `idx_runtime_reports_confirm`\nbefore enabling browser confirmation.\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`, `run`, `archive` |\n| CLI | `list`, `get`, `run`, `archive` |\n| MCP | `list`, `get`, `run`, `archive` |\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 and browser flow (#3878)\n\n`reports.runtime.sources` describes fields available to the assistant principal.\n`reports.runtime.define` with `phase: 'preview'` compiles and runs a bounded\nsample. `phase: 'apply'` requires the preview's exact normalized `specHash`.\nWithout an in-process confirmation host it returns:\n\n```ts\n{ status: 'awaiting_confirmation', specHash, spec, sourceLabel, preview,\n confirmPath: '/api/_reports/runtime/confirm' }\n```\n\nNothing is saved by that response, and model-supplied consent is ignored. The\nbrowser shows the proposal and only a person's approval posts `{ spec, specHash }`\nto `createRuntimeReportConfirmHandler()`. The handler checks same origin, the\nverified session user and tenant, `runtimereports.create`, the normalized hash,\nand compilation under the **confirming** principal's current permissions.\nAnother principal may approve the same public spec only under their own\npermissions and tenant; a hash is data, not a transferable authorization token.\n\nThe template mounts `POST /api/_reports/runtime/confirm`. Hosts pass the verified\nprincipal and the request's database (including the RLS transaction), never\nbody-supplied identity. Success is `201 { id, specHash, title }`; a retry of an\nactive definition with the same principal/hash returns the same response with\n200. Compilation happens again even on retries. Concurrent approvals use core's\ntransactional `runOnce` claim, keyed by tenant, principal and normalized spec.\nA failed write rolls back the claim; an audit\ncallback failure returns a generic 500 and a retry finds the already saved row.\nNo result rows are stored in the idempotency record. After archiving, approving\nthe same definition returns 409 `report_archived`; restore it through the archive\naction before retrying.\n\nErrors are JSON `{ code, message }`: 401 without a session; 403 for cross-origin,\nmissing permission, mismatched ambient tenant, or an active retry invalidated by\ncurrent source/field access; 409 for an archived retry; 422 for a malformed new\nspec or `spec_hash_mismatch`; generic 500 for unexpected provider failures. There is\nno cross-tenant or system bypass. Configure an optional `audit` callback on the\nfactory to receive confirmed principal/hash/report identity and whether reused.\n\nFor CLI and other in-process hosts, `RuntimeReportConfirmationHost.confirmSave()`\nremains available: it must resolve only after a person approves that exact hash.\nThe single-use model proof is unchanged in both paths. The assistant tools use\nthe persona's fail-closed `allowedTools` gates.\n\n## Server configuration and actions\n\nConfigure `packages.reports.runtime.sources` in `smrt.config.ts`:\n\n```ts\npackages: {\n reports: { runtime: { sources: [\n { id: 'invoices', className: '@acme/app:Invoice', collection: 'invoices', label: 'Invoices' }\n ] } }\n}\n```\n\nAn absent source list exposes nothing. A source's `collection` is a catalog slug\nand requires `<collection>.read`; the server narrows the allow-list before\ncompiling. Give the assistant the same server-owned source definitions. The\nconfirm factory can instead take explicit `{ sources, audit? }` options.\n\n`POST /api/runtimereports/:id/run` takes no body and returns\n`{ action: 'run', result: RuntimeReportResult }`. Its CLI/MCP counterparts call\nthe same model action. It requires `runtimereports.read`, reloads the definition\nin the principal's tenant, and recompiles on every call. Tenant peers can run\nshared reports; use `createdByUserId` for an Only mine list filter. Missing and\nforeign-tenant ids return 404; revoked source/field access returns 403; archived\nreports return 409 `report_archived`; invalid results return 422. Rows stay\nephemeral and integer money stays in minor units.\n\n`POST /api/runtimereports/:id/archive` takes `{ archived: boolean }` and returns\n`{ action: 'archive', result: { id, status } }`. It requires\n`runtimereports.create`, changes status only, and\nrepeating the same state does not write again. `operationPermissions` metadata\naliases run to read and archive to create consistently across generated routes,\nCLI/MCP guards and the permission catalog. The archive alias adds SELECT and\nUPDATE SQL-policy bindings to create: the authorized action must look up the\nsame-tenant row before changing its status. Adopted PostgreSQL RLS policies must\nbe regenerated. Neither `.run`, `.archive`, `.update` nor `.delete` becomes a\nnew grantable permission for this model.\n\n`loadRuntimeReportChart(options, context)` is the server loader for the dependent\nbrowser recipe widget. Options are `{ reportId, measure, dimension? }`; the host\nsupplies `context.reportPrincipal` from its authenticated request and optionally\n`currency`. It runs afresh as the viewer and returns structural chart data:\n`{ points: [{label, value}], format, currency?, href }`. The loader has no\nsmrt-svelte dependency and receives no authority from widget options.\n\n## Recipe UI\n\n`reports.runtime` declares the RuntimeReport model, Reports navigation, the\n`/reports` routes and `runtime-report-chart` widget. The widget options are only\n`{ reportId, measure, dimension? }`; the authenticated server host supplies the\nloader's `reportPrincipal`. Static hosts render the Reports route unavailable.\nRename, scheduling and manual report construction remain deferred.\n"
|
|
1164
1276
|
},
|
|
1165
1277
|
{
|
|
1166
1278
|
"path": "README.md",
|
|
1167
1279
|
"module": "README",
|
|
1168
|
-
"content": "# @happyvertical/smrt-reports\n\nMaterialized aggregate reports for s-m-r-t. Define a report as a decorated class,\ncompile it to a portable aggregate query, and refresh its normal s-m-r-t table\nmanually, on a schedule, after source changes, or when a TTL expires.\n\nRuntime refresh checks that its tables exist (existence only, not columns or\nindexes) and fails clearly if they do not; it does not create report or system\ntables. Include report models in normal manifest-driven migrations before\nrefreshing.\n\n## Installation\n\n```bash\npnpm add @happyvertical/smrt-reports\n```\n\n## Define a report\n\n```ts\nimport { SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n groupBy,\n month,\n report,\n SmrtReport,\n SmrtReportCollection,\n sum,\n} from '@happyvertical/smrt-reports';\n\n@smrt()\nclass Invoice extends SmrtObject {\n customerId = '';\n issuedAt = new Date();\n totalAmount = 0.0;\n status = '';\n}\n\n@report({\n source: Invoice,\n where: { status: 'paid' },\n refresh: { manual: true },\n})\n@smrt()\nclass MonthlyRevenue extends SmrtReport {\n @groupBy('customerId') customerId = '';\n @month('issuedAt') issuedMonth = new Date();\n @sum('totalAmount') revenue = 0.0;\n}\n\nclass MonthlyRevenueCollection extends SmrtReportCollection<MonthlyRevenue> {\n static readonly _itemClass = MonthlyRevenue;\n}\n\nconst reports = await MonthlyRevenueCollection.create({ db: 'app.db' });\nawait reports.refresh();\nconst rows = await reports.list({ orderBy: 'issued_month DESC' });\n```\n\n## Decorators\n\n| Decorator | Meaning |\n| --- | --- |\n| `@report({ source, ... })` | Declare the source and refresh policy |\n| `@groupBy(column?)` | Group by a source column |\n| `@minute()` through `@year()` | Bucket a source timestamp |\n| `@sum()`, `@avg()`, `@min()`, `@max()` | Aggregate a source column |\n| `@count()` | Count rows or distinct values |\n| `@aggregate()` | Define a custom supported aggregate shape |\n\nEvery non-system report field must be a grouping key, time bucket, or aggregate.\nMetadata is stored under the field's `_meta.__report` contract so scanner and\nruntime compilation stay aligned.\n\n## Refresh modes\n\n- `rebuild` replaces the materialized result.\n- `incremental` recomputes groups affected after a source watermark and removes\n groups that become empty.\n- TTL-backed collections refresh before reads when the report is stale.\n- `ReportScheduleRunner` and `registerReportRefreshInterceptor()` enqueue\n durable refresh jobs through [`smrt-jobs`](../jobs/README.md).\n\nIncremental sources need watermark and soft-delete columns, normally\n`updatedAt` and `deletedAt`. Tenant-scoped aggregate SQL explicitly filters\n`tenant_id`; normal collection interceptors cannot secure a raw aggregate.\n\n## Public entry points\n\nThe package exposes focused subpaths for consumers that want only the compiler,\ndecorators, refresh engine, scheduler, state models, or aggregate compatibility\nhelpers:\n\n```text\n@happyvertical/smrt-reports\n@happyvertical/smrt-reports/compiler\n@happyvertical/smrt-reports/decorators\n@happyvertical/smrt-reports/refresh\n@happyvertical/smrt-reports/scheduler\n@happyvertical/smrt-reports/state\n@happyvertical/smrt-reports/aggregate\n```\n\n## Report adapter\n\nThe package also exports a transport-neutral adapter for report surfaces:\n\n```ts\nimport {\n buildReportAdapterDescriptor,\n buildReportDrilldownQuery,\n queryReportMaterializedRows,\n reportMaterializedRowKey,\n splitReportFilterScopes,\n} from '@happyvertical/smrt-reports';\n\nconst descriptor = await buildReportAdapterDescriptor(MonthlyRevenue, {\n tenantScope: 'current',\n});\nconst reports = await MonthlyRevenueCollection.create({ db: 'app.db' });\nconst result = await queryReportMaterializedRows(\n MonthlyRevenue,\n {\n version: 1,\n requestId: 'monthly-revenue-first-page',\n mode: 'rows',\n projection: ['id', 'customer_id', 'revenue'],\n filter: {\n kind: 'all',\n filters: [\n {\n kind: 'condition',\n field: 'customer_id',\n operator: 'eq',\n value: 'customer-42',\n },\n {\n kind: 'condition',\n field: 'revenue',\n operator: 'gte',\n value: 1000,\n },\n ],\n },\n page: { kind: 'offset', offset: 0, limit: 25 },\n sort: [{ field: 'revenue', direction: 'desc' }],\n },\n {\n collection: reports,\n // Preserve the resource identity of the descriptor selected above.\n adapter: { tenantScope: 'current' },\n },\n);\nconst rowKey = reportMaterializedRowKey(result.rows[0]);\nconst drilldown = await buildReportDrilldownQuery(MonthlyRevenue, result.rows[0]);\n```\n\n`ReportAdapterDescriptor` is deterministic JSON with a stable resource id,\n`id` as the identity field, typed report columns, a canonical `DataQuerySchema`,\nand structural DataTable hints. The hints are deliberately not a `smrt-ui`\ndependency. Consumers may map the descriptor to their own presentation layer.\nThe adapter exposes only persisted report columns and the primary key: transient,\nsystem, and non-column fields are not surfaced. Sensitive/secret fields and\nfields with a `readPermission` are excluded without a principal, so the\ndescriptor fails closed.\n\nThe `dataTable.columns` entries carry neutral `headerPath`, `valueFormat`,\nalignment, role, and responsive hints. Grouping fields, time buckets, and\naggregate measures receive deterministic multi-level header ancestry; a\nconsumer can override any column by stable id with\n`buildReportAdapterDescriptor(..., { dataTable: { columns: { ... } } })`.\n`valueFormat` is an instruction for the rendering boundary only: rows returned\nby `queryReportMaterializedRows()` retain their raw JSON-safe values for\nsorting, exports, and agents. Use `dataTable.structuralRows` for a computed\nsummary, subtotal, aggregate, or footer. Each is marked `selection: 'excluded'`\nand `actions: 'excluded'`, and must be passed to the consumer table's structural\nrow surface rather than its selectable data rows.\n\n`queryReportMaterializedRows()` is the bounded read slice for already-materialized\nrows. It supports projection, offset/limit paging (default limit 50),\ndeterministic multi-sort (with `id` as the final tie-break), typed filters, and\ndatabase-backed dimension facets. A descriptor marks group/time fields as\n`filterScope: 'where'` and aggregate measures as `filterScope: 'having'`; use\n`splitReportFilterScopes()` when constructing a live source query. An AND may\ncombine the two scopes, but an OR or NOT cannot mix them, because moving either\nside across a source `WHERE`/`HAVING` boundary would change its meaning.\n\nMaterialized query results normalize hydrated values against their declared\ncolumn types. Objects in scalar columns become JSON text; JSON columns retain\nstructured values. Date-only and timezone-less timestamps use UTC, matching\nSQLite storage, and temporal facet buckets follow the same rule. Strings over\n4096 characters are shortened with `truncated` and a warning, except row IDs:\nidentities are never rewritten, and unsupported IDs fail validation. Unsafe bigint\nvalues fail instead of silently losing precision.\n\nThe materialized collection executes the normalized, allowlisted predicate as\nparameterized SQL and applies it identically to rows, totals, and facets. It\nnever accepts raw SQL, source field paths, tenant ids, or principal ids.\nThe descriptor's `queryExecution` contract declares three delivery choices:\n`visible` (the default) returns rows for an already-authorized surface,\n`silent` returns the same bounded result while making no visible-surface change,\nand `background` delegates a normalized, authority-free task to the application's\n`enqueueBackgroundQuery` host. A background result is only a queue handle, never\nmaterialized rows. The host retains the authenticated principal, tenant, report\ndefinition, field policy, database, and eventual job execution; none can be\nsupplied in a query request or background task.\nWhen a consumer selects a descriptor with `ReportAdapterOptions`, pass those\nsame options through the query's `adapter` option so returned and background\ntask resource ids stay aligned with the selected descriptor.\n`buildReportDrilldownQuery()` carries only the row's declared groups/buckets\nand a fixed inheritance contract for the current principal, tenant, report\ndefinition, and field policy; an authenticated source adapter must enforce that\ncontract before it reads source records. Time buckets remain declarative so the\nsource adapter keeps the report's database/timezone semantics. Every returned\nrow must have a non-empty string `id`; use that value, never a display or page\nindex, for row identity.\n\nWhen no collection is injected, reads resolve the registered report collection\nthrough `ObjectRegistry`, allowing normal s-m-r-t collection interceptors to enforce\ntenant filtering. `tenantScoped` and `tenantField` in the descriptor are true\nonly when the report is actually registered with tenant metadata; a scope string\nalone is not an authority boundary. An injected collection is application-owned\nand must provide the equivalent tenant boundary. Raw aggregate refreshes still\nneed explicit tenant predicates as described above.\n\nThe descriptor's `refresh` section declares mode, triggers, stale-read behavior,\nand a permissioned, audited `refresh` action with `preview` and `apply` phases.\nThe adapter itself stays read-only. Call `getReportLifecycle()` for a tenant-safe\nsnapshot of current, stale, refreshing, lock-skipped, or failed materialization\nstate; it redacts lock owners, raw errors, and tenant-fanout identifiers. Pass a\n`lifecycle` option to `queryReportMaterializedRows()` only when a consumer needs\nthat context; the result then distinguishes a current, stale, or read-triggered\nrefresh. `previewReportRefresh()` and `applyReportRefresh()` require an\napplication action host to authorize and audit the caller before a durable\nreport-refresh job is queued. Manual refresh hosts must also return a versioned,\nnon-secret `executionAuthority` binding. Each worker process registers the named\nhost with `registerReportRefreshExecutionAuthorityHost()`; the task checks the\npersisted tenant binding, resolves current membership and permission, and records\nan allowed or denied execution audit immediately before refresh. Every enqueue\npath also receives a `DurableJobPayloadSigner`, and every worker registers it\nwith `registerReportRefreshJobIntegritySigner()`. This authenticates the task\ntarget, trigger, tenant scope, authority reference, and payload across storage;\nthe signing key is never persisted. Scheduled and on-change maintenance jobs\nremain system-authority jobs and do not carry an initiating user, but their job\npayloads use the same integrity boundary. Only a registered `SmrtReportCollection` can\nsynchronously refresh a stale read, and only when its TTL policy is positive and\nnot manual.\n\n### Scheduled refresh lifecycle\n\nDeclare `refresh: { schedule: '*/5 * * * *', tenantFanout: 'discover' }`\non a tenant-scoped report to discover distinct non-null source tenants each\nscheduled run. Include scopes still present in the materialized table so a\nthe next rebuild removes stale groups for tenants losing their final source row.\nDiscovery preserves the configured refresh mode. Incremental reports still need\na `fullRebuildSchedule` to catch hard deletions that do not advance a source\nwatermark. Provision schedules with `ensureReportRefreshSchedules({ db,\nreports })`; no discovered scopes means no work, and discovery never refreshes\nthe global slice.\nThe worker uses the registered source/report tenant columns and its supplied\nmaintenance executor. Discovery does not grant database privileges or widen a\ntenant-bound or manual job. Keep `tenantFanout: true` with explicit `tenantIds`\nwhen the host owns a fixed tenant list.\n\nInitialize a `ReportScheduleRunner` and a jobs `TaskRunner` against the migrated\ndatabase, then call `const unbind = schedules.bindTaskRunner(tasks)` before\nstarting them. This connects terminal job events to schedule counters, freeing\nthe concurrency slot after success or failure. Repeated binding is idempotent;\ncall `unbind()` on shutdown. Listen for `runner:error` to detect failed counter\nupdates. Persisted failure messages use the jobs package redaction policy.\nDurable payload signing and execution-time authority checks remain\nrequired; completion accounting uses the signed schedule identity.\n\nRepeated schedule provisioning inserts missing identities and preserves existing\nhistory, next-run time, running count, and operator settings. A different cron,\nmode, or tenant scope identifies a different schedule; explicitly disable obsolete\nschedules when changing those settings.\n\n### Saved views and snapshot exports\n\n`normalizeReportSavedView()` and `restoreReportSavedView()` provide the\nserializable view boundary. A storage host owns the saved view's tenant and\nowner; on every restore it must pass the stored payload through the current\ndescriptor. That reapplies field, projection, sorting, grouping, and definition\npolicy, so a stale view cannot reveal a field that is no longer allowed.\n`migrateReportSavedView()` upgrades the original unversioned (or explicit v0)\nlayout to v1 before that current-policy validation; unsupported future versions\nfail clearly rather than being guessed.\n\nBuild an export from a completed materialized-row read with\n`createReportExportSnapshot()`, supplying an opaque binding from the\napplication's immutable materialization-snapshot host, then call\n`createReportExportRequest()`. The snapshot fixes the canonical query\nfingerprint, normalized projection and sort, exact row count, `asOf`,\n`refreshedAt`, stale state, and definition fingerprint. Every request also\ncontains a deterministic offset-page read plan; a renderer begins at offset zero\nand advances by its validated page size until the exact bounded row count is\nreached. Use `createReportExportPageRequest()` for each page; it preserves the\nfrozen query semantics while replacing only visible pagination.\n\nEvery request is bounded by rows, bytes, and deadline; exports over the\nforeground row limit become an authority-free background handoff. Use\n`previewReportExport()` and `applyReportExport()` with the same application\naction host for human and agent callers. The host authorizes and audits the\nfixed `reports.export` action; exports containing personal, sensitive, or secret\ncolumns require explicit confirmation.\n\nPreview, apply, and every worker call `validateReportExportExecution()`, which\nrequires the host to prove that the opaque binding still resolves to the exact\nimmutable materialization snapshot under its current principal, tenant, report\ndefinition, and field policy. If it cannot, the operation fails rather than\nrelabelling newer rows with an old `asOf`. Artifact metadata has no URL or\ndownload token; before serving it, call `validateReportExportArtifact()` to\nreject expiry, definition drift, and out-of-bounds progress, then apply the\nhost's current authorization and snapshot validation again.\n\n## Runtime reports\n\nA report can also be defined at runtime as data. `RuntimeReportSpec` is a closed,\nversioned structure (source id, dimensions, measures, filters, having, sort,\nchart hint) with no free-form query text. `compileRuntimeReportSpec()` checks it\nagainst a server-owned source allow-list, field policy (sensitive and\npermission-gated fields are not nameable), the caller's permissions, and tenant\nisolation, then targets the same aggregate plan as declared reports.\n`RuntimeReport` stores validated specs per tenant, and a stored spec is\nre-compiled for the running user on every run. Result rows are not stored. A row\ncan only be inserted, or have its spec changed, through `saveRuntimeReport()`\nwith the spec hash a human confirmed; the model layer refuses every other write. The\nchat package offers matching assistant tools that propose a spec, preview it,\nand save only after a human confirms. See\n[`agents/runtime-reports.md`](./agents/runtime-reports.md).\n\n## Development\n\n```bash\npnpm --filter @happyvertical/smrt-reports test\npnpm --filter @happyvertical/smrt-reports typecheck\npnpm --filter @happyvertical/smrt-reports build\n```\n\nSee [`AGENTS.md`](./AGENTS.md) for refresh, schema, and tenancy invariants.\n"
|
|
1280
|
+
"content": "# @happyvertical/smrt-reports\n\nMaterialized aggregate reports for s-m-r-t. Define a report as a decorated class,\ncompile it to a portable aggregate query, and refresh its normal s-m-r-t table\nmanually, on a schedule, after source changes, or when a TTL expires.\n\nRuntime refresh checks that its tables exist (existence only, not columns or\nindexes) and fails clearly if they do not; it does not create report or system\ntables. Include report models in normal manifest-driven migrations before\nrefreshing.\n\n## Installation\n\n```bash\npnpm add @happyvertical/smrt-reports\n```\n\n## Define a report\n\n```ts\nimport { SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n groupBy,\n month,\n report,\n SmrtReport,\n SmrtReportCollection,\n sum,\n} from '@happyvertical/smrt-reports';\n\n@smrt()\nclass Invoice extends SmrtObject {\n customerId = '';\n issuedAt = new Date();\n totalAmount = 0.0;\n status = '';\n}\n\n@report({\n source: Invoice,\n where: { status: 'paid' },\n refresh: { manual: true },\n})\n@smrt()\nclass MonthlyRevenue extends SmrtReport {\n @groupBy('customerId') customerId = '';\n @month('issuedAt') issuedMonth = new Date();\n @sum('totalAmount') revenue = 0.0;\n}\n\nclass MonthlyRevenueCollection extends SmrtReportCollection<MonthlyRevenue> {\n static readonly _itemClass = MonthlyRevenue;\n}\n\nconst reports = await MonthlyRevenueCollection.create({ db: 'app.db' });\nawait reports.refresh();\nconst rows = await reports.list({ orderBy: 'issued_month DESC' });\n```\n\n## Decorators\n\n| Decorator | Meaning |\n| --- | --- |\n| `@report({ source, ... })` | Declare the source and refresh policy |\n| `@groupBy(column?)` | Group by a source column |\n| `@minute()` through `@year()` | Bucket a source timestamp |\n| `@sum()`, `@avg()`, `@min()`, `@max()` | Aggregate a source column |\n| `@count()` | Count rows or distinct values |\n| `@aggregate()` | Define a custom supported aggregate shape |\n\nEvery non-system report field must be a grouping key, time bucket, or aggregate.\nMetadata is stored under the field's `_meta.__report` contract so scanner and\nruntime compilation stay aligned.\n\n## Refresh modes\n\n- `rebuild` replaces the materialized result.\n- `incremental` recomputes groups affected after a source watermark and removes\n groups that become empty.\n- TTL-backed collections refresh before reads when the report is stale.\n- `ReportScheduleRunner` and `registerReportRefreshInterceptor()` enqueue\n durable refresh jobs through [`smrt-jobs`](../jobs/README.md).\n\nIncremental sources need watermark and soft-delete columns, normally\n`updatedAt` and `deletedAt`. Tenant-scoped aggregate SQL explicitly filters\n`tenant_id`; normal collection interceptors cannot secure a raw aggregate.\n\n## Public entry points\n\nThe package exposes focused subpaths for consumers that want only the compiler,\ndecorators, refresh engine, scheduler, state models, or aggregate compatibility\nhelpers:\n\n```text\n@happyvertical/smrt-reports\n@happyvertical/smrt-reports/compiler\n@happyvertical/smrt-reports/decorators\n@happyvertical/smrt-reports/refresh\n@happyvertical/smrt-reports/scheduler\n@happyvertical/smrt-reports/state\n@happyvertical/smrt-reports/aggregate\n```\n\n## Report adapter\n\nThe package also exports a transport-neutral adapter for report surfaces:\n\n```ts\nimport {\n buildReportAdapterDescriptor,\n buildReportDrilldownQuery,\n queryReportMaterializedRows,\n reportMaterializedRowKey,\n splitReportFilterScopes,\n} from '@happyvertical/smrt-reports';\n\nconst descriptor = await buildReportAdapterDescriptor(MonthlyRevenue, {\n tenantScope: 'current',\n});\nconst reports = await MonthlyRevenueCollection.create({ db: 'app.db' });\nconst result = await queryReportMaterializedRows(\n MonthlyRevenue,\n {\n version: 1,\n requestId: 'monthly-revenue-first-page',\n mode: 'rows',\n projection: ['id', 'customer_id', 'revenue'],\n filter: {\n kind: 'all',\n filters: [\n {\n kind: 'condition',\n field: 'customer_id',\n operator: 'eq',\n value: 'customer-42',\n },\n {\n kind: 'condition',\n field: 'revenue',\n operator: 'gte',\n value: 1000,\n },\n ],\n },\n page: { kind: 'offset', offset: 0, limit: 25 },\n sort: [{ field: 'revenue', direction: 'desc' }],\n },\n {\n collection: reports,\n // Preserve the resource identity of the descriptor selected above.\n adapter: { tenantScope: 'current' },\n },\n);\nconst rowKey = reportMaterializedRowKey(result.rows[0]);\nconst drilldown = await buildReportDrilldownQuery(MonthlyRevenue, result.rows[0]);\n```\n\n`ReportAdapterDescriptor` is deterministic JSON with a stable resource id,\n`id` as the identity field, typed report columns, a canonical `DataQuerySchema`,\nand structural DataTable hints. The hints are deliberately not a `smrt-ui`\ndependency. Consumers may map the descriptor to their own presentation layer.\nThe adapter exposes only persisted report columns and the primary key: transient,\nsystem, and non-column fields are not surfaced. Sensitive/secret fields and\nfields with a `readPermission` are excluded without a principal, so the\ndescriptor fails closed.\n\nThe `dataTable.columns` entries carry neutral `headerPath`, `valueFormat`,\nalignment, role, and responsive hints. Grouping fields, time buckets, and\naggregate measures receive deterministic multi-level header ancestry; a\nconsumer can override any column by stable id with\n`buildReportAdapterDescriptor(..., { dataTable: { columns: { ... } } })`.\n`valueFormat` is an instruction for the rendering boundary only: rows returned\nby `queryReportMaterializedRows()` retain their raw JSON-safe values for\nsorting, exports, and agents. Use `dataTable.structuralRows` for a computed\nsummary, subtotal, aggregate, or footer. Each is marked `selection: 'excluded'`\nand `actions: 'excluded'`, and must be passed to the consumer table's structural\nrow surface rather than its selectable data rows.\n\n`queryReportMaterializedRows()` is the bounded read slice for already-materialized\nrows. It supports projection, offset/limit paging (default limit 50),\ndeterministic multi-sort (with `id` as the final tie-break), typed filters, and\ndatabase-backed dimension facets. A descriptor marks group/time fields as\n`filterScope: 'where'` and aggregate measures as `filterScope: 'having'`; use\n`splitReportFilterScopes()` when constructing a live source query. An AND may\ncombine the two scopes, but an OR or NOT cannot mix them, because moving either\nside across a source `WHERE`/`HAVING` boundary would change its meaning.\n\nMaterialized query results normalize hydrated values against their declared\ncolumn types. Objects in scalar columns become JSON text; JSON columns retain\nstructured values. Date-only and timezone-less timestamps use UTC, matching\nSQLite storage, and temporal facet buckets follow the same rule. Strings over\n4096 characters are shortened with `truncated` and a warning, except row IDs:\nidentities are never rewritten, and unsupported IDs fail validation. Unsafe bigint\nvalues fail instead of silently losing precision.\n\nThe materialized collection executes the normalized, allowlisted predicate as\nparameterized SQL and applies it identically to rows, totals, and facets. It\nnever accepts raw SQL, source field paths, tenant ids, or principal ids.\nThe descriptor's `queryExecution` contract declares three delivery choices:\n`visible` (the default) returns rows for an already-authorized surface,\n`silent` returns the same bounded result while making no visible-surface change,\nand `background` delegates a normalized, authority-free task to the application's\n`enqueueBackgroundQuery` host. A background result is only a queue handle, never\nmaterialized rows. The host retains the authenticated principal, tenant, report\ndefinition, field policy, database, and eventual job execution; none can be\nsupplied in a query request or background task.\nWhen a consumer selects a descriptor with `ReportAdapterOptions`, pass those\nsame options through the query's `adapter` option so returned and background\ntask resource ids stay aligned with the selected descriptor.\n`buildReportDrilldownQuery()` carries only the row's declared groups/buckets\nand a fixed inheritance contract for the current principal, tenant, report\ndefinition, and field policy; an authenticated source adapter must enforce that\ncontract before it reads source records. Time buckets remain declarative so the\nsource adapter keeps the report's database/timezone semantics. Every returned\nrow must have a non-empty string `id`; use that value, never a display or page\nindex, for row identity.\n\nWhen no collection is injected, reads resolve the registered report collection\nthrough `ObjectRegistry`, allowing normal s-m-r-t collection interceptors to enforce\ntenant filtering. `tenantScoped` and `tenantField` in the descriptor are true\nonly when the report is actually registered with tenant metadata; a scope string\nalone is not an authority boundary. An injected collection is application-owned\nand must provide the equivalent tenant boundary. Raw aggregate refreshes still\nneed explicit tenant predicates as described above.\n\nThe descriptor's `refresh` section declares mode, triggers, stale-read behavior,\nand a permissioned, audited `refresh` action with `preview` and `apply` phases.\nThe adapter itself stays read-only. Call `getReportLifecycle()` for a tenant-safe\nsnapshot of current, stale, refreshing, lock-skipped, or failed materialization\nstate; it redacts lock owners, raw errors, and tenant-fanout identifiers. Pass a\n`lifecycle` option to `queryReportMaterializedRows()` only when a consumer needs\nthat context; the result then distinguishes a current, stale, or read-triggered\nrefresh. `previewReportRefresh()` and `applyReportRefresh()` require an\napplication action host to authorize and audit the caller before a durable\nreport-refresh job is queued. Manual refresh hosts must also return a versioned,\nnon-secret `executionAuthority` binding. Each worker process registers the named\nhost with `registerReportRefreshExecutionAuthorityHost()`; the task checks the\npersisted tenant binding, resolves current membership and permission, and records\nan allowed or denied execution audit immediately before refresh. Every enqueue\npath also receives a `DurableJobPayloadSigner`, and every worker registers it\nwith `registerReportRefreshJobIntegritySigner()`. This authenticates the task\ntarget, trigger, tenant scope, authority reference, and payload across storage;\nthe signing key is never persisted. Scheduled and on-change maintenance jobs\nremain system-authority jobs and do not carry an initiating user, but their job\npayloads use the same integrity boundary. Only a registered `SmrtReportCollection` can\nsynchronously refresh a stale read, and only when its TTL policy is positive and\nnot manual.\n\n### Scheduled refresh lifecycle\n\nDeclare `refresh: { schedule: '*/5 * * * *', tenantFanout: 'discover' }`\non a tenant-scoped report to discover distinct non-null source tenants each\nscheduled run. Include scopes still present in the materialized table so a\nthe next rebuild removes stale groups for tenants losing their final source row.\nDiscovery preserves the configured refresh mode. Incremental reports still need\na `fullRebuildSchedule` to catch hard deletions that do not advance a source\nwatermark. Provision schedules with `ensureReportRefreshSchedules({ db,\nreports })`; no discovered scopes means no work, and discovery never refreshes\nthe global slice.\nThe worker uses the registered source/report tenant columns and its supplied\nmaintenance executor. Discovery does not grant database privileges or widen a\ntenant-bound or manual job. Keep `tenantFanout: true` with explicit `tenantIds`\nwhen the host owns a fixed tenant list.\n\nInitialize a `ReportScheduleRunner` and a jobs `TaskRunner` against the migrated\ndatabase, then call `const unbind = schedules.bindTaskRunner(tasks)` before\nstarting them. This connects terminal job events to schedule counters, freeing\nthe concurrency slot after success or failure. Repeated binding is idempotent;\ncall `unbind()` on shutdown. Listen for `runner:error` to detect failed counter\nupdates. Persisted failure messages use the jobs package redaction policy.\nDurable payload signing and execution-time authority checks remain\nrequired; completion accounting uses the signed schedule identity.\n\nRepeated schedule provisioning inserts missing identities and preserves existing\nhistory, next-run time, running count, and operator settings. A different cron,\nmode, or tenant scope identifies a different schedule; explicitly disable obsolete\nschedules when changing those settings.\n\n### Saved views and snapshot exports\n\n`normalizeReportSavedView()` and `restoreReportSavedView()` provide the\nserializable view boundary. A storage host owns the saved view's tenant and\nowner; on every restore it must pass the stored payload through the current\ndescriptor. That reapplies field, projection, sorting, grouping, and definition\npolicy, so a stale view cannot reveal a field that is no longer allowed.\n`migrateReportSavedView()` upgrades the original unversioned (or explicit v0)\nlayout to v1 before that current-policy validation; unsupported future versions\nfail clearly rather than being guessed.\n\nBuild an export from a completed materialized-row read with\n`createReportExportSnapshot()`, supplying an opaque binding from the\napplication's immutable materialization-snapshot host, then call\n`createReportExportRequest()`. The snapshot fixes the canonical query\nfingerprint, normalized projection and sort, exact row count, `asOf`,\n`refreshedAt`, stale state, and definition fingerprint. Every request also\ncontains a deterministic offset-page read plan; a renderer begins at offset zero\nand advances by its validated page size until the exact bounded row count is\nreached. Use `createReportExportPageRequest()` for each page; it preserves the\nfrozen query semantics while replacing only visible pagination.\n\nEvery request is bounded by rows, bytes, and deadline; exports over the\nforeground row limit become an authority-free background handoff. Use\n`previewReportExport()` and `applyReportExport()` with the same application\naction host for human and agent callers. The host authorizes and audits the\nfixed `reports.export` action; exports containing personal, sensitive, or secret\ncolumns require explicit confirmation.\n\nPreview, apply, and every worker call `validateReportExportExecution()`, which\nrequires the host to prove that the opaque binding still resolves to the exact\nimmutable materialization snapshot under its current principal, tenant, report\ndefinition, and field policy. If it cannot, the operation fails rather than\nrelabelling newer rows with an old `asOf`. Artifact metadata has no URL or\ndownload token; before serving it, call `validateReportExportArtifact()` to\nreject expiry, definition drift, and out-of-bounds progress, then apply the\nhost's current authorization and snapshot validation again.\n\n## Runtime reports\n\nA report can also be defined at runtime as data. `RuntimeReportSpec` is a closed,\nversioned structure (source id, dimensions, measures, filters, having, sort,\nchart hint) with no free-form query text. `compileRuntimeReportSpec()` checks it\nagainst a server-owned source allow-list, field policy (sensitive and\npermission-gated fields are not nameable), the caller's permissions, and tenant\nisolation, then targets the same aggregate plan as declared reports.\n`RuntimeReport` stores validated specs per tenant, and a stored spec is\nre-compiled for the running user on every run. Result rows are not stored. A row\ncan only be inserted, or have its spec changed, through `saveRuntimeReport()`\nwith the spec hash a human confirmed; the model layer refuses every other write. The\nchat package offers matching assistant tools that propose a spec, preview it,\nand save only after a human confirms. See\n[`agents/runtime-reports.md`](./agents/runtime-reports.md).\n\n## Browser components\n\n`@happyvertical/smrt-reports/svelte` exports saved-report and detail screens,\na confirmation card, a report/measure picker, and a shared-chart result view.\nThey accept a principal-scoped browser transport and show an unavailable state\nwhen no server is supplied. See the [browser integration contract](./agents/runtime-report-browser.md)\nfor route-host identity and capability props, confirmation wiring, recipe\nsurfaces, and playground previews.\n\n## Development\n\n```bash\npnpm --filter @happyvertical/smrt-reports test\npnpm --filter @happyvertical/smrt-reports typecheck\npnpm --filter @happyvertical/smrt-reports build\n```\n\nSee [`AGENTS.md`](./AGENTS.md) for refresh, schema, and tenancy invariants.\n"
|
|
1169
1281
|
}
|
|
1170
1282
|
],
|
|
1171
1283
|
"recipes": [
|
|
@@ -1214,6 +1326,86 @@
|
|
|
1214
1326
|
"Needs a server because its package does not build for a browser: node:crypto/events; @happyvertical/sql root: the sync query builders it needs (validateColumnName, buildWhere, buildAggregate, bucketExpr, tableExists) have no browser-safe SDK entry; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3625, #3627)."
|
|
1215
1327
|
]
|
|
1216
1328
|
}
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
"id": "reports.runtime",
|
|
1332
|
+
"className": "RuntimeReportsRecipe",
|
|
1333
|
+
"label": "Reports",
|
|
1334
|
+
"summary": "Ask for a report, approve it, and share its live totals with your team.",
|
|
1335
|
+
"synonyms": [],
|
|
1336
|
+
"models": [
|
|
1337
|
+
"@happyvertical/smrt-reports:RuntimeReport"
|
|
1338
|
+
],
|
|
1339
|
+
"nav": [
|
|
1340
|
+
{
|
|
1341
|
+
"label": "Reports",
|
|
1342
|
+
"model": "@happyvertical/smrt-reports:RuntimeReport",
|
|
1343
|
+
"icon": "fileText"
|
|
1344
|
+
}
|
|
1345
|
+
],
|
|
1346
|
+
"requires": [],
|
|
1347
|
+
"help": {
|
|
1348
|
+
"markdown": "# Reports\n\nAsk the assistant for a total, comparison, or trend. Review the proposed report\nand approve it to save it. Your saved reports are shared with your team; each\nperson sees results using their own access to the underlying information.\n\nReports run fresh when opened. You can archive a report to hide it from the\nactive list and restore it later. Creating and restoring reports needs report\nwriting access. Reports need a connected server and are unavailable in offline\npreviews. Scheduling, renaming, and exporting reports are not available yet.\n",
|
|
1349
|
+
"fieldRefs": []
|
|
1350
|
+
},
|
|
1351
|
+
"surfaces": [
|
|
1352
|
+
{
|
|
1353
|
+
"kind": "route",
|
|
1354
|
+
"path": "/reports",
|
|
1355
|
+
"export": "@happyvertical/smrt-reports/svelte#RuntimeReportsRoute",
|
|
1356
|
+
"label": "Reports"
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
"kind": "route",
|
|
1360
|
+
"path": "/reports/:id",
|
|
1361
|
+
"export": "@happyvertical/smrt-reports/svelte#RuntimeReportDetailRoute",
|
|
1362
|
+
"label": "Report detail"
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
"kind": "widget",
|
|
1366
|
+
"type": "runtime-report-chart",
|
|
1367
|
+
"export": "@happyvertical/smrt-reports/svelte#RuntimeReportChartWidget",
|
|
1368
|
+
"label": "Report chart",
|
|
1369
|
+
"description": "Chart a measure from a saved report.",
|
|
1370
|
+
"icon": "chart",
|
|
1371
|
+
"optionsEditor": "@happyvertical/smrt-reports/svelte#RuntimeReportWidgetOptions",
|
|
1372
|
+
"options": [
|
|
1373
|
+
{
|
|
1374
|
+
"key": "reportId",
|
|
1375
|
+
"type": "text",
|
|
1376
|
+
"label": "Report",
|
|
1377
|
+
"required": true
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
"key": "measure",
|
|
1381
|
+
"type": "identifier",
|
|
1382
|
+
"label": "Measure",
|
|
1383
|
+
"required": true
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
"key": "dimension",
|
|
1387
|
+
"type": "identifier",
|
|
1388
|
+
"label": "Dimension"
|
|
1389
|
+
}
|
|
1390
|
+
],
|
|
1391
|
+
"data": {
|
|
1392
|
+
"load": "@happyvertical/smrt-reports#loadRuntimeReportChart",
|
|
1393
|
+
"models": [
|
|
1394
|
+
"@happyvertical/smrt-reports:RuntimeReport"
|
|
1395
|
+
]
|
|
1396
|
+
},
|
|
1397
|
+
"defaultSpan": 2,
|
|
1398
|
+
"maxSpan": 4
|
|
1399
|
+
}
|
|
1400
|
+
],
|
|
1401
|
+
"runtime": "server",
|
|
1402
|
+
"demo": {
|
|
1403
|
+
"mode": "server",
|
|
1404
|
+
"reasons": [
|
|
1405
|
+
"The recipe declares runtime \"server\".",
|
|
1406
|
+
"Needs a server because its package does not build for a browser: node:crypto/events; @happyvertical/sql root: the sync query builders it needs (validateColumnName, buildWhere, buildAggregate, bucketExpr, tableExists) have no browser-safe SDK entry; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3625, #3627)."
|
|
1407
|
+
]
|
|
1408
|
+
}
|
|
1217
1409
|
}
|
|
1218
1410
|
],
|
|
1219
1411
|
"browser": {
|