@uptimizr/db 2.0.1 → 2.1.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.
Files changed (171) hide show
  1. package/AGENTS.md +228 -3
  2. package/README.md +13 -0
  3. package/dist/duckdb/migrations.d.ts.map +1 -1
  4. package/dist/duckdb/migrations.js +169 -0
  5. package/dist/duckdb/migrations.js.map +1 -1
  6. package/dist/duckdb/panelSpecs.d.ts +29 -0
  7. package/dist/duckdb/panelSpecs.d.ts.map +1 -0
  8. package/dist/duckdb/panelSpecs.js +104 -0
  9. package/dist/duckdb/panelSpecs.js.map +1 -0
  10. package/dist/duckdb/projectMetadata.d.ts +43 -0
  11. package/dist/duckdb/projectMetadata.d.ts.map +1 -0
  12. package/dist/duckdb/projectMetadata.js +222 -0
  13. package/dist/duckdb/projectMetadata.js.map +1 -0
  14. package/dist/duckdb/subscriptions.d.ts +45 -0
  15. package/dist/duckdb/subscriptions.d.ts.map +1 -0
  16. package/dist/duckdb/subscriptions.js +202 -0
  17. package/dist/duckdb/subscriptions.js.map +1 -0
  18. package/dist/index.d.ts +18 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +48 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/insights/anomalies.d.ts +213 -0
  23. package/dist/insights/anomalies.d.ts.map +1 -0
  24. package/dist/insights/anomalies.js +340 -0
  25. package/dist/insights/anomalies.js.map +1 -0
  26. package/dist/insights/baseline.d.ts +51 -0
  27. package/dist/insights/baseline.d.ts.map +1 -0
  28. package/dist/insights/baseline.js +48 -0
  29. package/dist/insights/baseline.js.map +1 -0
  30. package/dist/insights/buckets.d.ts +114 -0
  31. package/dist/insights/buckets.d.ts.map +1 -0
  32. package/dist/insights/buckets.js +220 -0
  33. package/dist/insights/buckets.js.map +1 -0
  34. package/dist/insights/changepoint.d.ts +130 -0
  35. package/dist/insights/changepoint.d.ts.map +1 -0
  36. package/dist/insights/changepoint.js +209 -0
  37. package/dist/insights/changepoint.js.map +1 -0
  38. package/dist/insights/evaluate.d.ts +63 -0
  39. package/dist/insights/evaluate.d.ts.map +1 -0
  40. package/dist/insights/evaluate.js +158 -0
  41. package/dist/insights/evaluate.js.map +1 -0
  42. package/dist/insights/health.d.ts +219 -0
  43. package/dist/insights/health.d.ts.map +1 -0
  44. package/dist/insights/health.js +349 -0
  45. package/dist/insights/health.js.map +1 -0
  46. package/dist/insights/index.d.ts +45 -0
  47. package/dist/insights/index.d.ts.map +1 -0
  48. package/dist/insights/index.js +40 -0
  49. package/dist/insights/index.js.map +1 -0
  50. package/dist/insights/measures.d.ts +222 -0
  51. package/dist/insights/measures.d.ts.map +1 -0
  52. package/dist/insights/measures.js +431 -0
  53. package/dist/insights/measures.js.map +1 -0
  54. package/dist/insights/movers.d.ts +123 -0
  55. package/dist/insights/movers.d.ts.map +1 -0
  56. package/dist/insights/movers.js +166 -0
  57. package/dist/insights/movers.js.map +1 -0
  58. package/dist/insights/significance.d.ts +221 -0
  59. package/dist/insights/significance.d.ts.map +1 -0
  60. package/dist/insights/significance.js +531 -0
  61. package/dist/insights/significance.js.map +1 -0
  62. package/dist/insights/stats.d.ts +104 -0
  63. package/dist/insights/stats.d.ts.map +1 -0
  64. package/dist/insights/stats.js +181 -0
  65. package/dist/insights/stats.js.map +1 -0
  66. package/dist/insights/windows.d.ts +95 -0
  67. package/dist/insights/windows.d.ts.map +1 -0
  68. package/dist/insights/windows.js +97 -0
  69. package/dist/insights/windows.js.map +1 -0
  70. package/dist/metadata.d.ts +178 -1
  71. package/dist/metadata.d.ts.map +1 -1
  72. package/dist/metadata.js +80 -0
  73. package/dist/metadata.js.map +1 -1
  74. package/dist/narrative/build.d.ts +78 -0
  75. package/dist/narrative/build.d.ts.map +1 -0
  76. package/dist/narrative/build.js +437 -0
  77. package/dist/narrative/build.js.map +1 -0
  78. package/dist/narrative/index.d.ts +14 -0
  79. package/dist/narrative/index.d.ts.map +1 -0
  80. package/dist/narrative/index.js +13 -0
  81. package/dist/narrative/index.js.map +1 -0
  82. package/dist/narrative/text.d.ts +25 -0
  83. package/dist/narrative/text.d.ts.map +1 -0
  84. package/dist/narrative/text.js +41 -0
  85. package/dist/narrative/text.js.map +1 -0
  86. package/dist/parity/cases.d.ts +3 -0
  87. package/dist/parity/cases.d.ts.map +1 -1
  88. package/dist/parity/cases.js +387 -7
  89. package/dist/parity/cases.js.map +1 -1
  90. package/dist/parity/compare.d.ts +19 -0
  91. package/dist/parity/compare.d.ts.map +1 -1
  92. package/dist/parity/compare.js +19 -0
  93. package/dist/parity/compare.js.map +1 -1
  94. package/dist/parity/fixtures.d.ts.map +1 -1
  95. package/dist/parity/fixtures.js +25 -0
  96. package/dist/parity/fixtures.js.map +1 -1
  97. package/dist/query/aggregations.d.ts +46 -1
  98. package/dist/query/aggregations.d.ts.map +1 -1
  99. package/dist/query/aggregations.js +86 -0
  100. package/dist/query/aggregations.js.map +1 -1
  101. package/dist/query/clickhouseDialect.d.ts.map +1 -1
  102. package/dist/query/clickhouseDialect.js +5 -1
  103. package/dist/query/clickhouseDialect.js.map +1 -1
  104. package/dist/query/customEventVocabulary.d.ts +46 -0
  105. package/dist/query/customEventVocabulary.d.ts.map +1 -0
  106. package/dist/query/customEventVocabulary.js +127 -0
  107. package/dist/query/customEventVocabulary.js.map +1 -0
  108. package/dist/query/dsl/builders.d.ts +38 -0
  109. package/dist/query/dsl/builders.d.ts.map +1 -0
  110. package/dist/query/dsl/builders.js +35 -0
  111. package/dist/query/dsl/builders.js.map +1 -0
  112. package/dist/query/dsl/compare.d.ts +167 -0
  113. package/dist/query/dsl/compare.d.ts.map +1 -0
  114. package/dist/query/dsl/compare.js +358 -0
  115. package/dist/query/dsl/compare.js.map +1 -0
  116. package/dist/query/dsl/compile.d.ts +89 -0
  117. package/dist/query/dsl/compile.d.ts.map +1 -0
  118. package/dist/query/dsl/compile.js +174 -0
  119. package/dist/query/dsl/compile.js.map +1 -0
  120. package/dist/query/dsl/explain.d.ts +115 -0
  121. package/dist/query/dsl/explain.d.ts.map +1 -0
  122. package/dist/query/dsl/explain.js +143 -0
  123. package/dist/query/dsl/explain.js.map +1 -0
  124. package/dist/query/dsl/generic.d.ts +127 -0
  125. package/dist/query/dsl/generic.d.ts.map +1 -0
  126. package/dist/query/dsl/generic.js +323 -0
  127. package/dist/query/dsl/generic.js.map +1 -0
  128. package/dist/query/dsl/index.d.ts +30 -0
  129. package/dist/query/dsl/index.d.ts.map +1 -0
  130. package/dist/query/dsl/index.js +23 -0
  131. package/dist/query/dsl/index.js.map +1 -0
  132. package/dist/query/dsl/order.d.ts +41 -0
  133. package/dist/query/dsl/order.d.ts.map +1 -0
  134. package/dist/query/dsl/order.js +55 -0
  135. package/dist/query/dsl/order.js.map +1 -0
  136. package/dist/query/dsl/significance.d.ts +130 -0
  137. package/dist/query/dsl/significance.d.ts.map +1 -0
  138. package/dist/query/dsl/significance.js +224 -0
  139. package/dist/query/dsl/significance.js.map +1 -0
  140. package/dist/query/index.d.ts +2 -0
  141. package/dist/query/index.d.ts.map +1 -1
  142. package/dist/query/index.js +8 -0
  143. package/dist/query/index.js.map +1 -1
  144. package/dist/query/summary/index.d.ts +4 -2
  145. package/dist/query/summary/index.d.ts.map +1 -1
  146. package/dist/query/summary/index.js +2 -1
  147. package/dist/query/summary/index.js.map +1 -1
  148. package/dist/query/summary/labels.d.ts +101 -0
  149. package/dist/query/summary/labels.d.ts.map +1 -0
  150. package/dist/query/summary/labels.js +261 -0
  151. package/dist/query/summary/labels.js.map +1 -0
  152. package/dist/query/summary/reading.d.ts.map +1 -1
  153. package/dist/query/summary/reading.js +21 -2
  154. package/dist/query/summary/reading.js.map +1 -1
  155. package/dist/query/summary/schema.d.ts +250 -20
  156. package/dist/query/summary/schema.d.ts.map +1 -1
  157. package/dist/query/summary/schema.js +135 -123
  158. package/dist/query/summary/schema.js.map +1 -1
  159. package/dist/query/summary/summarize.d.ts.map +1 -1
  160. package/dist/query/summary/summarize.js +91 -5
  161. package/dist/query/summary/summarize.js.map +1 -1
  162. package/dist/query/summary/types.d.ts +77 -2
  163. package/dist/query/summary/types.d.ts.map +1 -1
  164. package/dist/query/types.d.ts +47 -1
  165. package/dist/query/types.d.ts.map +1 -1
  166. package/dist/subscriptions.d.ts +208 -0
  167. package/dist/subscriptions.d.ts.map +1 -0
  168. package/dist/subscriptions.js +153 -0
  169. package/dist/subscriptions.js.map +1 -0
  170. package/llms.txt +9 -0
  171. package/package.json +8 -8
package/dist/metadata.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * once and OSS carries no Postgres dependency.
8
8
  */
9
9
  import { createHash, randomBytes } from "node:crypto";
10
+ import { LIMITS } from "@uptimizr/schema";
10
11
  // --- API keys (pure crypto) ---
11
12
  /** SHA-256 hash of an API key. Only the hash is ever stored (never plaintext). */
12
13
  export function hashApiKey(plaintext) {
@@ -212,4 +213,83 @@ export function clampAuditTool(toolOrPath) {
212
213
  ? toolOrPath.slice(0, AUDIT_TOOL_MAX_LENGTH)
213
214
  : toolOrPath;
214
215
  }
216
+ /**
217
+ * Per-project row caps (ADR 0051 §5). Metadata is a curated set of notes, so
218
+ * every store refuses the write that would exceed its table's cap rather than
219
+ * silently growing. The numbers live in `@uptimizr/schema`'s `LIMITS` with the
220
+ * rest of the wire bounds; this is the storage-side view of them.
221
+ */
222
+ export const METADATA_LIMITS = {
223
+ annotations: LIMITS.maxProjectAnnotations,
224
+ glossary: LIMITS.maxProjectGlossaryEntries,
225
+ savedAnalyses: LIMITS.maxProjectSavedAnalyses,
226
+ panelSpecs: LIMITS.maxProjectPanelSpecs,
227
+ };
228
+ /**
229
+ * Thrown by a store when a write would take a project past the table's cap.
230
+ * The collector maps it to `409 Conflict` — the request was well-formed, the
231
+ * project is simply full, and the caller fixes it by deleting something.
232
+ */
233
+ export class MetadataLimitError extends Error {
234
+ table;
235
+ limit;
236
+ constructor(table, limit) {
237
+ super(`project has reached its limit of ${limit} ${table} rows`);
238
+ this.table = table;
239
+ this.limit = limit;
240
+ this.name = "MetadataLimitError";
241
+ }
242
+ }
243
+ /**
244
+ * Clamp a caller-supplied `limit` into `[1, max]`, defaulting when absent.
245
+ * Shared by every store so one engine cannot quietly return more rows than
246
+ * another for the same request.
247
+ */
248
+ export function clampMetadataLimit(limit, fallback = 100, max = 500) {
249
+ if (limit == null || !Number.isFinite(limit))
250
+ return fallback;
251
+ return Math.min(Math.max(Math.trunc(limit), 1), max);
252
+ }
253
+ /**
254
+ * Parse a stored `query` JSON column back into an object. A row written by this
255
+ * collector always parses; a hand-edited or truncated one degrades to `{}`
256
+ * rather than failing the whole listing.
257
+ */
258
+ export function parseSavedAnalysisQuery(json) {
259
+ try {
260
+ const parsed = JSON.parse(json);
261
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed)
262
+ ? parsed
263
+ : {};
264
+ }
265
+ catch {
266
+ return {};
267
+ }
268
+ }
269
+ /**
270
+ * Parse a stored `spec` JSON column back into a document.
271
+ *
272
+ * Returns `null` rather than a partial object when the text is not a JSON
273
+ * object at all: unlike a saved analysis' opaque `query` — which degrades to
274
+ * `{}` because it is stored but never interpreted — a panel spec is *rendered*,
275
+ * so a row that cannot be one is better skipped than drawn. The listing drops
276
+ * it and the rest of the grid is unaffected, which is the same posture
277
+ * `loadSpecPanels` takes in `@uptimizr/react`.
278
+ *
279
+ * The result is typed as the wire shape but **not** re-validated here: a store
280
+ * is not the boundary, and re-running Zod on every row of every listing would
281
+ * pay the validation cost on the read path for a document the write path
282
+ * already checked. The renderer validates what it is about to draw.
283
+ */
284
+ export function parsePanelSpec(json) {
285
+ try {
286
+ const parsed = JSON.parse(json);
287
+ return parsed != null && typeof parsed === "object" && !Array.isArray(parsed)
288
+ ? parsed
289
+ : null;
290
+ }
291
+ catch {
292
+ return null;
293
+ }
294
+ }
215
295
  //# sourceMappingURL=metadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `buildSessionNarrative` — the pure compaction behind
3
+ * `GET /api/v1/sessions/:id/narrative` (ADR 0051 §7, design sketch §G.2).
4
+ *
5
+ * It turns one session's raw event stream into an ordered, bounded account of
6
+ * what that session *did*: where it went, what held its attention, what it
7
+ * touched, what stuttered, what broke, and how it ended. Timestamps are relative
8
+ * to the session's first event, every line of prose is templated from the
9
+ * entry's own fields, and the whole thing is capped by `maxEntries`.
10
+ *
11
+ * **Pure.** It takes an `AnyEvent[]` and returns a plain object: no store, no
12
+ * request, no clock, no I/O. It lives in `@uptimizr/db` rather than in
13
+ * `@uptimizr/agent-core` because the collector is the only consumer and
14
+ * `agent-core` must stay browser-safe and driver-free; it is not in
15
+ * `@uptimizr/metrics` because that package is pure *data*.
16
+ *
17
+ * ## What it deliberately leaves out (ADR 0003)
18
+ *
19
+ * A narrative is derived from raw per-session data, so the route that serves it
20
+ * needs both `ENABLE_RAW_SESSION_RETENTION` and a `query:raw` key. The
21
+ * compaction is the second line of defence: it is an allow-list, not a redactor.
22
+ * It reads only the fields named below, so nothing else can leak even if a
23
+ * future event type carries it —
24
+ *
25
+ * - **never** `visitorId`, `url` or `pageMeta` (title, referrer, language, …);
26
+ * - **never** a position, hit point, ray, UV or screen coordinate;
27
+ * - from `device`/`graphics`, only the rendering `engine` and graphics `api` —
28
+ * no renderer string, no vendor, no OS, no browser, no memory or core count;
29
+ * - from `session_start.user`, nothing at all;
30
+ * - custom-event property **keys** (developer-chosen field names), never their
31
+ * values, unless the caller explicitly passes `includeCustomProps`;
32
+ * - `runtime_error.message` truncated, and never its `source` (a URL) or
33
+ * `stack`;
34
+ * - `graphics_diagnostic` by `category`/`severity` only — never its message.
35
+ *
36
+ * `src/__tests__/narrative.test.ts` asserts the exclusions against a synthetic
37
+ * session whose every event carries a visitor hash, a URL and page metadata.
38
+ */
39
+ import { type SessionNarrativeEntry, type SessionNarrativeTotals } from "@uptimizr/metrics";
40
+ import type { AnyEvent } from "@uptimizr/schema";
41
+ /** How to compact a session. Every field has a registry-declared default. */
42
+ export interface SessionNarrativeOptions {
43
+ /** Dwell floor in ms; a mesh below it is a glance, not attention. */
44
+ minDwellMs?: number;
45
+ /** A `frame_perf` sample below this FPS counts towards a dip. */
46
+ fpsThreshold?: number;
47
+ /** Hard cap on entries, closing `summary` entry included. */
48
+ maxEntries?: number;
49
+ /**
50
+ * Include custom-event property **values** as well as their keys. Off by
51
+ * default: a property value is app-supplied and may carry anything, so it is
52
+ * outside the privacy allow-list unless the operator opts in.
53
+ */
54
+ includeCustomProps?: boolean;
55
+ }
56
+ /** An ordered, bounded account of one session. */
57
+ export interface SessionNarrative {
58
+ /** The session the narrative describes. */
59
+ sessionId: string;
60
+ /** Wall-clock start of the session (epoch ms) — the origin `tMs` counts from. */
61
+ startedAt: number;
62
+ /** The entries, oldest first, ending with the `summary` entry. */
63
+ entries: SessionNarrativeEntry[];
64
+ /** Whether entries were dropped to honour `maxEntries`. */
65
+ truncated: boolean;
66
+ /** Session-wide totals, also carried on the closing `summary` entry. */
67
+ totals: SessionNarrativeTotals;
68
+ }
69
+ /**
70
+ * Compact one session's events into a narrative.
71
+ *
72
+ * `events` may arrive in any order (they are sorted here, stably, so events
73
+ * sharing a timestamp keep their stream order) and may be empty — an empty
74
+ * session yields an empty narrative with zeroed totals, which the collector
75
+ * turns into a 404.
76
+ */
77
+ export declare function buildSessionNarrative(events: readonly AnyEvent[], options?: SessionNarrativeOptions): SessionNarrative;
78
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/narrative/build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,6EAA6E;AAC7E,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;IACnB,wEAAwE;IACxE,MAAM,EAAE,sBAAsB,CAAC;CAChC;AA0ED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,QAAQ,EAAE,EAC3B,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAsYlB"}
@@ -0,0 +1,437 @@
1
+ /**
2
+ * `buildSessionNarrative` — the pure compaction behind
3
+ * `GET /api/v1/sessions/:id/narrative` (ADR 0051 §7, design sketch §G.2).
4
+ *
5
+ * It turns one session's raw event stream into an ordered, bounded account of
6
+ * what that session *did*: where it went, what held its attention, what it
7
+ * touched, what stuttered, what broke, and how it ended. Timestamps are relative
8
+ * to the session's first event, every line of prose is templated from the
9
+ * entry's own fields, and the whole thing is capped by `maxEntries`.
10
+ *
11
+ * **Pure.** It takes an `AnyEvent[]` and returns a plain object: no store, no
12
+ * request, no clock, no I/O. It lives in `@uptimizr/db` rather than in
13
+ * `@uptimizr/agent-core` because the collector is the only consumer and
14
+ * `agent-core` must stay browser-safe and driver-free; it is not in
15
+ * `@uptimizr/metrics` because that package is pure *data*.
16
+ *
17
+ * ## What it deliberately leaves out (ADR 0003)
18
+ *
19
+ * A narrative is derived from raw per-session data, so the route that serves it
20
+ * needs both `ENABLE_RAW_SESSION_RETENTION` and a `query:raw` key. The
21
+ * compaction is the second line of defence: it is an allow-list, not a redactor.
22
+ * It reads only the fields named below, so nothing else can leak even if a
23
+ * future event type carries it —
24
+ *
25
+ * - **never** `visitorId`, `url` or `pageMeta` (title, referrer, language, …);
26
+ * - **never** a position, hit point, ray, UV or screen coordinate;
27
+ * - from `device`/`graphics`, only the rendering `engine` and graphics `api` —
28
+ * no renderer string, no vendor, no OS, no browser, no memory or core count;
29
+ * - from `session_start.user`, nothing at all;
30
+ * - custom-event property **keys** (developer-chosen field names), never their
31
+ * values, unless the caller explicitly passes `includeCustomProps`;
32
+ * - `runtime_error.message` truncated, and never its `source` (a URL) or
33
+ * `stack`;
34
+ * - `graphics_diagnostic` by `category`/`severity` only — never its message.
35
+ *
36
+ * `src/__tests__/narrative.test.ts` asserts the exclusions against a synthetic
37
+ * session whose every event carries a visitor hash, a URL and page metadata.
38
+ */
39
+ import { NARRATIVE_LIMITS, } from "@uptimizr/metrics";
40
+ /** Input sources that mean "this was an immersive XR session" (ADR 0011). */
41
+ const XR_SOURCES = new Set(["xr-controller", "hand", "gaze", "transient"]);
42
+ /** Clamp `value` into `[min, max]`, falling back to `fallback` when absent. */
43
+ function clamp(value, fallback, min, max) {
44
+ if (value == null || !Number.isFinite(value))
45
+ return fallback;
46
+ return Math.min(max, Math.max(min, value));
47
+ }
48
+ /** Collapse whitespace and cut to `max` characters, marking the cut with `…`. */
49
+ function truncate(text, max = NARRATIVE_LIMITS.maxMessageLength) {
50
+ const flat = text.replace(/\s+/g, " ").trim();
51
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
52
+ }
53
+ /** `3400` → `"3.4s"`, `900` → `"900ms"` — durations a reader can skim. */
54
+ function humanMs(ms) {
55
+ if (ms < 1000)
56
+ return `${Math.round(ms)}ms`;
57
+ if (ms < 60_000)
58
+ return `${(ms / 1000).toFixed(1)}s`;
59
+ const minutes = Math.floor(ms / 60_000);
60
+ const seconds = Math.round((ms % 60_000) / 1000);
61
+ return `${minutes}m ${seconds}s`;
62
+ }
63
+ /** A number rendered for prose: one decimal place, no trailing `.0`. */
64
+ function humanNumber(value) {
65
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
66
+ }
67
+ /** `via mouse` / `` — the input source clause, when the event carried one. */
68
+ function sourceClause(source) {
69
+ return typeof source === "string" && source.length > 0 ? ` via ${source}` : "";
70
+ }
71
+ /** Drop the `undefined` keys so `refs` serialises as only what it really has. */
72
+ function refsOf(refs) {
73
+ const out = {};
74
+ if (refs.mesh)
75
+ out.mesh = refs.mesh;
76
+ if (refs.scene)
77
+ out.scene = refs.scene;
78
+ if (refs.name)
79
+ out.name = refs.name;
80
+ return out;
81
+ }
82
+ /**
83
+ * Compact one session's events into a narrative.
84
+ *
85
+ * `events` may arrive in any order (they are sorted here, stably, so events
86
+ * sharing a timestamp keep their stream order) and may be empty — an empty
87
+ * session yields an empty narrative with zeroed totals, which the collector
88
+ * turns into a 404.
89
+ */
90
+ export function buildSessionNarrative(events, options = {}) {
91
+ const minDwellMs = clamp(options.minDwellMs, NARRATIVE_LIMITS.defaultMinDwellMs, 0, NARRATIVE_LIMITS.maxMinDwellMs);
92
+ const fpsThreshold = clamp(options.fpsThreshold, NARRATIVE_LIMITS.defaultFpsThreshold, 1, NARRATIVE_LIMITS.maxFpsThreshold);
93
+ const maxEntries = Math.round(clamp(options.maxEntries, NARRATIVE_LIMITS.defaultMaxEntries, 1, NARRATIVE_LIMITS.maxMaxEntries));
94
+ const ordered = [...events]
95
+ .map((event, index) => ({ event, index }))
96
+ .sort((a, b) => a.event.ts - b.event.ts || a.index - b.index)
97
+ .map((entry) => entry.event);
98
+ const empty = {
99
+ events: 0,
100
+ durationMs: 0,
101
+ scenes: 0,
102
+ meshes: 0,
103
+ interactions: 0,
104
+ dips: 0,
105
+ errors: 0,
106
+ };
107
+ const first = ordered[0];
108
+ if (!first) {
109
+ return { sessionId: "", startedAt: 0, entries: [], truncated: false, totals: empty };
110
+ }
111
+ const t0 = first.ts;
112
+ const rel = (ts) => Math.max(0, Math.round(ts - t0));
113
+ const entries = [];
114
+ const push = (entry) => {
115
+ entries.push(entry);
116
+ };
117
+ const dwell = new Map();
118
+ const scenes = new Set();
119
+ const meshes = new Set();
120
+ let interactions = 0;
121
+ let dips = 0;
122
+ let errors = 0;
123
+ let dip = null;
124
+ let xrFirstTMs = null;
125
+ let xrLastTMs = null;
126
+ let xrSource = "";
127
+ let boundaryCount = 0;
128
+ let boundaryMs = 0;
129
+ let boundaryTMs = 0;
130
+ let lastTs = first.ts;
131
+ /** Fold a dwell contribution for `mesh` into the accumulator. */
132
+ const addDwell = (mesh, tMs, scene, visibleMs, hoverMs) => {
133
+ meshes.add(mesh);
134
+ const existing = dwell.get(mesh);
135
+ if (existing) {
136
+ existing.visibleMs += visibleMs;
137
+ existing.hoverMs += hoverMs;
138
+ existing.count += 1;
139
+ if (tMs < existing.tMs)
140
+ existing.tMs = tMs;
141
+ return;
142
+ }
143
+ dwell.set(mesh, { mesh, scene, tMs, visibleMs, hoverMs, count: 1 });
144
+ };
145
+ /** Close the open perf dip, emitting an entry when it is long enough. */
146
+ const closeDip = () => {
147
+ if (!dip)
148
+ return;
149
+ if (dip.samples >= NARRATIVE_LIMITS.dipMinSamples) {
150
+ const durationMs = Math.max(0, dip.endTMs - dip.tMs);
151
+ dips += 1;
152
+ push({
153
+ tMs: dip.tMs,
154
+ kind: "perf_dip",
155
+ summary: `Frame rate dipped to ${humanNumber(dip.minFps)} fps ` +
156
+ `(mean ${humanNumber(dip.totalFps / dip.samples)}) across ${dip.samples} samples` +
157
+ (durationMs > 0 ? ` over ${humanMs(durationMs)}.` : "."),
158
+ refs: refsOf({ scene: dip.scene }),
159
+ ...(durationMs > 0 ? { durationMs } : {}),
160
+ count: dip.samples,
161
+ });
162
+ }
163
+ dip = null;
164
+ };
165
+ for (const event of ordered) {
166
+ lastTs = event.ts;
167
+ const tMs = rel(event.ts);
168
+ const scene = typeof event.sceneId === "string" ? event.sceneId : undefined;
169
+ if (scene)
170
+ scenes.add(scene);
171
+ // Anything with an XR input source marks the immersive stretch of the
172
+ // session. There is no explicit "entered XR" event (ADR 0011 models XR as an
173
+ // input source, not a mode), so the span is derived from the first and last
174
+ // XR-sourced event.
175
+ const source = event.source;
176
+ if (typeof source === "string" && XR_SOURCES.has(source)) {
177
+ if (xrFirstTMs == null) {
178
+ xrFirstTMs = tMs;
179
+ xrSource = source;
180
+ }
181
+ xrLastTMs = tMs;
182
+ }
183
+ switch (event.type) {
184
+ case "session_start": {
185
+ const engine = event.device?.engine;
186
+ const api = event.graphics?.api;
187
+ const renderer = engine ?? api;
188
+ push({
189
+ tMs,
190
+ kind: "scene",
191
+ summary: (scene ? `Session started in scene "${scene}"` : "Session started") +
192
+ (renderer ? ` on ${renderer}.` : "."),
193
+ refs: refsOf({ scene }),
194
+ });
195
+ break;
196
+ }
197
+ case "scene_change": {
198
+ push({
199
+ tMs,
200
+ kind: "scene",
201
+ summary: scene ? `Moved to scene "${scene}".` : "Moved to another scene.",
202
+ refs: refsOf({ scene }),
203
+ });
204
+ break;
205
+ }
206
+ case "mesh_visibility": {
207
+ addDwell(event.mesh, tMs, scene, event.visibleMs, 0);
208
+ break;
209
+ }
210
+ case "hover_dwell": {
211
+ addDwell(event.mesh, tMs, scene, 0, event.dwellMs);
212
+ break;
213
+ }
214
+ case "mesh_interaction": {
215
+ meshes.add(event.mesh);
216
+ interactions += 1;
217
+ push({
218
+ tMs,
219
+ kind: "interaction",
220
+ summary: `${event.kind} on "${event.mesh}"${sourceClause(event.source)}.`,
221
+ refs: refsOf({ mesh: event.mesh, scene }),
222
+ });
223
+ break;
224
+ }
225
+ case "pointer_click": {
226
+ // Only a click that hit something is a story. A click into empty space
227
+ // carries nothing but coordinates, which a narrative never reports —
228
+ // `dead_clicks` is the aggregate metric for those.
229
+ if (!event.hitMesh)
230
+ break;
231
+ meshes.add(event.hitMesh);
232
+ interactions += 1;
233
+ push({
234
+ tMs,
235
+ kind: "interaction",
236
+ summary: `Clicked "${event.hitMesh}"${sourceClause(event.source)}.`,
237
+ refs: refsOf({ mesh: event.hitMesh, scene }),
238
+ });
239
+ break;
240
+ }
241
+ case "input_action": {
242
+ interactions += 1;
243
+ const state = event.pressed === false ? " released" : "";
244
+ push({
245
+ tMs,
246
+ kind: "interaction",
247
+ summary: `Input action "${event.action}"${event.code ? ` (${event.code})` : ""}` +
248
+ `${state}${sourceClause(event.source)}.`,
249
+ refs: refsOf({ name: event.action, scene }),
250
+ });
251
+ break;
252
+ }
253
+ case "custom": {
254
+ interactions += 1;
255
+ const props = event.props ?? {};
256
+ const keys = Object.keys(props).sort().slice(0, NARRATIVE_LIMITS.maxCustomPropKeys);
257
+ // Keys are developer-chosen field names and are always safe to name;
258
+ // values are app data and are opt-in only (see the module comment).
259
+ const detail = options.includeCustomProps
260
+ ? keys.map((key) => `${key}=${truncate(String(props[key]), 40)}`).join(", ")
261
+ : keys.join(", ");
262
+ push({
263
+ tMs,
264
+ kind: "interaction",
265
+ summary: `Custom event "${event.name}"${detail ? ` (${detail})` : ""}.`,
266
+ refs: refsOf({ name: event.name, scene }),
267
+ });
268
+ break;
269
+ }
270
+ case "frame_perf": {
271
+ if (event.fps < fpsThreshold) {
272
+ if (dip == null) {
273
+ dip = {
274
+ tMs,
275
+ endTMs: tMs,
276
+ samples: 1,
277
+ minFps: event.fps,
278
+ scene,
279
+ totalFps: event.fps,
280
+ };
281
+ }
282
+ else {
283
+ dip.endTMs = tMs;
284
+ dip.samples += 1;
285
+ dip.totalFps += event.fps;
286
+ if (event.fps < dip.minFps)
287
+ dip.minFps = event.fps;
288
+ }
289
+ }
290
+ else {
291
+ closeDip();
292
+ }
293
+ break;
294
+ }
295
+ case "runtime_error": {
296
+ errors += 1;
297
+ // The message only — never `source` (a URL) or `stack`.
298
+ push({
299
+ tMs,
300
+ kind: "error",
301
+ summary: `Runtime ${event.kind}: ${truncate(event.message)}`,
302
+ refs: refsOf({ scene }),
303
+ });
304
+ break;
305
+ }
306
+ case "graphics_diagnostic": {
307
+ if (event.severity === "error" || event.severity === "fatal")
308
+ errors += 1;
309
+ // Category and severity are closed enums; the engine's own message is
310
+ // not reported.
311
+ push({
312
+ tMs,
313
+ kind: "diagnostic",
314
+ summary: `Graphics diagnostic: ${event.category} (${event.severity}).`,
315
+ refs: refsOf({ scene }),
316
+ ...(event.count != null ? { count: event.count } : {}),
317
+ });
318
+ break;
319
+ }
320
+ case "capability_change": {
321
+ const from = event.from ? truncate(event.from, 64) : undefined;
322
+ const to = event.to ? truncate(event.to, 64) : undefined;
323
+ const transition = from && to ? `: ${from} → ${to}` : to ? `: → ${to}` : "";
324
+ const durationMs = event.durationMs != null ? Math.round(event.durationMs) : undefined;
325
+ push({
326
+ tMs,
327
+ kind: "capability",
328
+ summary: `Capability change (${event.kind})${transition}` +
329
+ (durationMs != null ? ` for ${humanMs(durationMs)}.` : "."),
330
+ refs: refsOf({ scene }),
331
+ ...(durationMs != null ? { durationMs } : {}),
332
+ });
333
+ break;
334
+ }
335
+ case "xr_boundary_proximity": {
336
+ // Folded into one summary entry rather than one per contact: a guardian
337
+ // approach fires repeatedly and would otherwise flood the narrative.
338
+ if (boundaryCount === 0)
339
+ boundaryTMs = tMs;
340
+ boundaryCount += 1;
341
+ boundaryMs += event.durationMs;
342
+ break;
343
+ }
344
+ case "session_end": {
345
+ closeDip();
346
+ const durationMs = event.durationMs != null ? Math.round(event.durationMs) : Math.max(0, tMs);
347
+ push({
348
+ tMs,
349
+ kind: "end",
350
+ summary: `Session ended${event.reason ? ` (${event.reason})` : ""} after ${humanMs(durationMs)}.`,
351
+ refs: refsOf({ scene }),
352
+ durationMs,
353
+ });
354
+ break;
355
+ }
356
+ default:
357
+ break;
358
+ }
359
+ }
360
+ closeDip();
361
+ // --- deferred, session-wide entries -------------------------------------
362
+ for (const accumulator of dwell.values()) {
363
+ const totalMs = Math.round(accumulator.visibleMs + accumulator.hoverMs);
364
+ if (totalMs < minDwellMs)
365
+ continue;
366
+ const hover = accumulator.hoverMs > 0 ? ` (${humanMs(Math.round(accumulator.hoverMs))} hovered)` : "";
367
+ push({
368
+ tMs: accumulator.tMs,
369
+ kind: "dwell",
370
+ summary: `Dwelled on "${accumulator.mesh}" for ${humanMs(totalMs)}${hover}.`,
371
+ refs: refsOf({ mesh: accumulator.mesh, scene: accumulator.scene }),
372
+ durationMs: totalMs,
373
+ count: accumulator.count,
374
+ });
375
+ }
376
+ if (xrFirstTMs != null) {
377
+ push({
378
+ tMs: xrFirstTMs,
379
+ kind: "xr",
380
+ summary: `Entered immersive XR input (${xrSource}).`,
381
+ refs: {},
382
+ });
383
+ if (xrLastTMs != null && xrLastTMs > xrFirstTMs) {
384
+ push({
385
+ tMs: xrLastTMs,
386
+ kind: "xr",
387
+ summary: `Last immersive XR input, ${humanMs(xrLastTMs - xrFirstTMs)} after the first.`,
388
+ refs: {},
389
+ durationMs: xrLastTMs - xrFirstTMs,
390
+ });
391
+ }
392
+ }
393
+ if (boundaryCount > 0) {
394
+ push({
395
+ tMs: boundaryTMs,
396
+ kind: "xr",
397
+ summary: `Approached the play-area boundary ${boundaryCount} time(s) for ${humanMs(Math.round(boundaryMs))} in total.`,
398
+ refs: {},
399
+ durationMs: Math.round(boundaryMs),
400
+ count: boundaryCount,
401
+ });
402
+ }
403
+ // --- order, bound, summarise --------------------------------------------
404
+ const sorted = entries
405
+ .map((entry, index) => ({ entry, index }))
406
+ .sort((a, b) => a.entry.tMs - b.entry.tMs || a.index - b.index)
407
+ .map((item) => item.entry);
408
+ // The closing `summary` entry always survives, so the bound the caller asked
409
+ // for is the bound on the whole answer — never `maxEntries + 1`.
410
+ const room = Math.max(0, maxEntries - 1);
411
+ const truncated = sorted.length > room;
412
+ const kept = truncated ? sorted.slice(0, room) : sorted;
413
+ const totals = {
414
+ events: ordered.length,
415
+ durationMs: Math.max(0, Math.round(lastTs - t0)),
416
+ scenes: scenes.size,
417
+ meshes: meshes.size,
418
+ interactions,
419
+ dips,
420
+ errors,
421
+ };
422
+ kept.push({
423
+ tMs: totals.durationMs,
424
+ kind: "summary",
425
+ summary: `${totals.events} events over ${humanMs(totals.durationMs)}: ${totals.scenes} scene(s), ` +
426
+ `${totals.meshes} mesh(es), ${totals.interactions} interaction(s), ${totals.dips} perf dip(s), ` +
427
+ `${totals.errors} error(s).` +
428
+ (truncated
429
+ ? ` ${sorted.length - room} later entr(ies) were dropped to honour maxEntries.`
430
+ : ""),
431
+ refs: {},
432
+ totals,
433
+ truncated,
434
+ });
435
+ return { sessionId: first.sessionId, startedAt: t0, entries: kept, truncated, totals };
436
+ }
437
+ //# sourceMappingURL=build.js.map