@jeffjassky/telemetry 0.2.0 → 0.4.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/types/index.d.ts CHANGED
@@ -166,6 +166,351 @@ export declare function boundedMeta(): z.ZodType<Record<string, unknown> | undef
166
166
  /** Boot-time contract checks — throws on misconfiguration. createTelemetry runs it. */
167
167
  export declare function validateRegistry(registry: Registry): void;
168
168
 
169
+ // ── catalog (reports §3) ────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Everything a reader can ask this instance, inferred from the registry alone:
173
+ * typed dimensions with their value domains, the measures each event supports,
174
+ * and which rollup family answers a sum exactly. Pure and boot-time, like
175
+ * validateRegistry — createDashboard() and createTelemetryMcp() build one each
176
+ * and serve it beside the projection.
177
+ */
178
+ export interface Catalog {
179
+ events: Record<string, EventFacet>;
180
+ families: Record<string, FamilyFacet>;
181
+ /** name prefix before the first '.' → event names. An undotted name namespaces to itself. */
182
+ namespaces: Record<string, string[]>;
183
+ /** dims every record carries — filterable and groupable raw, whatever the source */
184
+ envelope: DimFacet[];
185
+ /** every subject type named by any spec's `subjects` or any rollup's `subjects` */
186
+ subjectTypes: string[];
187
+ }
188
+
189
+ export interface EventFacet {
190
+ kind: TelemetryKind;
191
+ origin: Origin | 'any';
192
+ subjects: string[];
193
+ description: string;
194
+ namespace: string;
195
+ /** one per declared attr, typed, followed by this kind's own envelope fields */
196
+ dims: DimFacet[];
197
+ /** 'count' first, then per metric key: sum:, avg:, p50:, p95:, p99: */
198
+ measures: MeasureFacet[];
199
+ /** rollup family names this event feeds (its `as`, or its own name) */
200
+ families: string[];
201
+ indexedAttrs: string[];
202
+ indexedMetrics: string[];
203
+ /** the EFFECTIVE retention — the spec's override, else RETENTION_DAYS[kind] */
204
+ retentionDays: number | null;
205
+ }
206
+
207
+ export interface FamilyFacet {
208
+ as: string;
209
+ /** the grain, in declared order (pinned per family by validateRegistry) */
210
+ by: DimSource[];
211
+ /** rollups.ts `label(src)` per dim — the `x=` prefix written into `dims` */
212
+ labels: string[];
213
+ bucket: 'hour' | 'day' | 'week' | 'month' | null;
214
+ /** a lifetime rollup has no bucket, and its `firstAt` IS the milestone */
215
+ lifetime: boolean;
216
+ /** the spec's `subjects` when `by` has a subject dim, else [] */
217
+ subjectTypes: string[];
218
+ sums: string[];
219
+ /** labels of `capture` sources */
220
+ capture: string[];
221
+ /** event names declaring this family, registry order */
222
+ feeders: string[];
223
+ retentionDays: number | null;
224
+ }
225
+
226
+ export interface DimFacet {
227
+ /**
228
+ * The DimSource form, so it passes straight through to a rollup `by`, to a
229
+ * groupBy, and to a filter term: 'attr:model' | 'field:client.platform' |
230
+ * 'subjectType' | 'actorType'.
231
+ */
232
+ key: string;
233
+ /** what rollups.ts writes before '=' — 'model', 'client.platform'; for the two pseudo-dims, the key */
234
+ label: string;
235
+ type: 'string' | 'enum' | 'number' | 'boolean' | 'date';
236
+ /** closed domain when known: z.enum / z.literal values, envelope enums */
237
+ values?: string[];
238
+ optional: boolean;
239
+ /** true when a real index exists — an `indexedAttrs` attr, or a base-indexed envelope field */
240
+ indexed: boolean;
241
+ }
242
+
243
+ export interface MeasureFacet {
244
+ /** 'count' | 'sum:cost_usd' | 'avg:cost_usd' | 'p95:duration_ms' … */
245
+ key: string;
246
+ metric?: string;
247
+ /** families whose `sum` carries this metric — exact answers. Only 'sum:' keys ever have one. */
248
+ exactVia: string[];
249
+ }
250
+
251
+ export interface DeriveCatalogOptions {
252
+ /** host additions to `client.platform`, exactly as CreateTelemetryConfig.platforms extends them */
253
+ platforms?: readonly string[];
254
+ }
255
+
256
+ /** the projection `/api/registry` and `describe_telemetry` have always returned */
257
+ export interface RegistryProjectionEntry {
258
+ kind: TelemetryKind;
259
+ origin: Origin | 'any';
260
+ subjects: string[];
261
+ description: string;
262
+ attrKeys: string[];
263
+ metricKeys: string[];
264
+ indexedAttrs: string[];
265
+ indexedMetrics: string[];
266
+ rollups: {
267
+ as: string;
268
+ by: DimSource[];
269
+ bucket: 'hour' | 'day' | 'week' | 'month' | null;
270
+ sum: string[];
271
+ subjects: string[];
272
+ }[];
273
+ }
274
+ export type RegistryProjection = Record<string, RegistryProjectionEntry>;
275
+
276
+ /** Pure. No Mongo, no I/O — derive once at boot and cache it on the instance. */
277
+ export declare function deriveCatalog(registry: Registry, opts?: DeriveCatalogOptions): Catalog;
278
+
279
+ /** the catalog narrowed back to the projection, so adding it costs the SPA nothing */
280
+ export declare function projectRegistry(catalog: Catalog): RegistryProjection;
281
+
282
+ // ── suggestions (reports §9) ────────────────────────────────────────────────
283
+
284
+ /**
285
+ * One registry edit the data is asking for. `message` is the sentence a human
286
+ * reads; `fix` is the line they paste. Nothing here writes anything — the host
287
+ * still edits the registry by hand, the package just stops making it guess.
288
+ */
289
+ export interface Suggestion {
290
+ kind: 'undeclared_attr' | 'missing_dim_default' | 'unregistered_event';
291
+ /** the registry entry to touch — an event name, or a rollup family name */
292
+ target: string;
293
+ /** attr key or dim label, when the suggestion is about one */
294
+ key?: string;
295
+ count: number;
296
+ message: string;
297
+ /** the registry change, as code */
298
+ fix: string;
299
+ }
300
+
301
+ export interface DeriveSuggestionsInput {
302
+ counters: TelemetryCounters;
303
+ catalog: Catalog;
304
+ /** the quarantine rows the caller already fetched — only `name` and `reason` are read */
305
+ quarantine?: readonly { name?: unknown; reason?: unknown; [k: string]: unknown }[];
306
+ }
307
+
308
+ /**
309
+ * Pure, like deriveCatalog: counters + catalog + quarantine in, registry lines
310
+ * out. Served on `GET /api/system` and by the `telemetry_health` MCP tool.
311
+ */
312
+ export declare function deriveSuggestions(input: DeriveSuggestionsInput): Suggestion[];
313
+
314
+ /** the returned list is capped here — a System page is a thing a human reads */
315
+ export declare const MAX_SUGGESTIONS: 50;
316
+
317
+ // ── reports (reports §4, §6) ────────────────────────────────────────────────
318
+
319
+ /**
320
+ * A Report is one shape: what a page renders, what a saved view stores, what a
321
+ * URL hash carries, and what `run_report` executes. `resolveReport` turns one
322
+ * into a Plan — the cheapest primitive that answers it exactly, a raw plan when
323
+ * nothing can, and a refusal with a reason when nothing at all can.
324
+ */
325
+ export type ReportSource =
326
+ | { event: string }
327
+ | { namespace: string }
328
+ | { kind: TelemetryKind }
329
+ | { family: string };
330
+
331
+ /** a shorthand from the UI's RANGES ('7d'), or an explicit half-open ISO pair */
332
+ export type ReportRange = string | { from: string; to: string };
333
+
334
+ export interface ReportFilter {
335
+ /** a DimFacet.key: 'attr:model' | 'field:env' | 'subjectType' | 'field:name' … */
336
+ dim: string;
337
+ op: 'eq' | 'in' | 'gte' | 'lte';
338
+ value: string | string[] | number;
339
+ }
340
+
341
+ export interface Report {
342
+ source: ReportSource;
343
+ range: ReportRange;
344
+ interval?: 'hour' | 'day' | 'week' | 'month';
345
+ /** a MeasureFacet.key. Default 'count'; also 'distinct:<subjectType>' and 'funnel' */
346
+ measure?: string;
347
+ /** DimFacet.key[], at most two */
348
+ groupBy?: string[];
349
+ filters?: ReportFilter[];
350
+ excludeActorTypes?: string[];
351
+ /** a rendering hint carried with the Report; no primitive takes it today */
352
+ sort?: 'value' | 'label' | 'time';
353
+ limit?: number;
354
+ /** same length, immediately before */
355
+ compare?: 'previous';
356
+
357
+ // ── funnel-only (`measure: 'funnel'`) ──
358
+ stages?: string[];
359
+ anchor?: string;
360
+ exits?: string[];
361
+ subjectType?: string;
362
+ }
363
+
364
+ /**
365
+ * The pre-Report `ViewSpec.query`, still parsed and lifted by normalizeQuery().
366
+ * Its `display` key is removed — the renderer decides from the Report itself
367
+ * (reports §8) — and a stored view still carrying one keeps parsing.
368
+ * @deprecated write a Report.
369
+ */
370
+ export interface LegacyQuery {
371
+ range?: string;
372
+ filters?: Record<string, unknown>;
373
+ groupBy?: string;
374
+ sort?: string;
375
+ }
376
+
377
+ export type PlanPrimitive =
378
+ | 'records' | 'series' | 'breakdown' | 'distribution'
379
+ | 'rollups' | 'distinctCount' | 'funnel';
380
+
381
+ /**
382
+ * How the executor folds a `rollups` plan: `rollups()` has no server-side
383
+ * groupBy, and the requested dims ARE the family's dims, so the grouping is a
384
+ * fold over the returned rows. `labels[i]` is the `dims` prefix rollups.ts
385
+ * writes for `groupBy[i]`; a subject dim labels to 'subject' and its stored
386
+ * value is the bare `type:id` ref.
387
+ */
388
+ export interface PlanShape {
389
+ groupBy: string[];
390
+ labels: string[];
391
+ measure: string;
392
+ interval?: 'hour' | 'day' | 'week' | 'month';
393
+ filters?: { dim: string; label: string; op: ReportFilter['op']; value: ReportFilter['value'] }[];
394
+ }
395
+
396
+ export interface Plan {
397
+ primitive: PlanPrimitive;
398
+ /** positional args AFTER scope — the executor is literally `q[primitive](scope, ...args)` */
399
+ args: unknown[];
400
+ exactness: 'exact' | 'raw' | 'scan';
401
+ /** the family that answers it, when one does */
402
+ via?: string;
403
+ /** human sentence — the UI badge and the MCP explanation */
404
+ why: string;
405
+ /** how to fold the rows a `rollups` plan returns */
406
+ shape?: PlanShape;
407
+ /** present when `compare: 'previous'` — same primitive, range shifted back by its own length */
408
+ previous?: { args: unknown[] };
409
+ }
410
+
411
+ export interface Unavailable {
412
+ unavailable: true;
413
+ why: string;
414
+ }
415
+
416
+ export interface ResolveOptions {
417
+ /** injected so a plan is deterministic — shorthand ranges end here */
418
+ now?: Date;
419
+ limits?: Partial<QueryLimits>;
420
+ }
421
+
422
+ /**
423
+ * Report → Plan, pure. No Mongo, no I/O, deterministic given `now` — pinned by
424
+ * unit tests like deriveCatalog and summarizeStages.
425
+ */
426
+ export declare function resolveReport(
427
+ report: Report,
428
+ catalog: Catalog,
429
+ opts?: ResolveOptions,
430
+ ): Plan | Unavailable;
431
+
432
+ /** lift a stored view's legacy query onto a Report. null when nothing names a source. */
433
+ export declare function normalizeQuery(query: Report | LegacyQuery | null | undefined): Report | null;
434
+
435
+ /**
436
+ * A Report is a URL, and these two are inverses: `parseReportQuery(reportToQuery(r))`
437
+ * deep-equals `r`. `source=event:<name>|namespace:<ns>|kind:<kind>|family:<as>`,
438
+ * `range=7d` or `from`+`to`, `groupBy` and `excludeActors` comma-separated, and
439
+ * `filter=<dim>:<op>:<value>` REPEATED — the dim may itself contain a colon, so
440
+ * the first `eq`/`in`/`gte`/`lte` token ends it. Unknown params are ignored; a
441
+ * malformed one throws with `status: 400` naming the param.
442
+ */
443
+ export declare function parseReportQuery(query: Record<string, unknown>): Report;
444
+ export declare function reportToQuery(report: Report): Record<string, string | string[]>;
445
+
446
+ // ── the executor ────────────────────────────────────────────────────────────
447
+
448
+ export interface ExecuteOptions {
449
+ /** injected so a plan is deterministic — shorthand ranges end here */
450
+ now?: Date;
451
+ limits?: Partial<QueryLimits>;
452
+ /** applied to a `records` plan's items before they leave (mcp.ts passes its redactor) */
453
+ redact?: (items: any[]) => any[];
454
+ }
455
+
456
+ export interface ReportResult {
457
+ /** the Report as executed — after a legacy lift, so the caller sees what ran */
458
+ report: Report;
459
+ plan: Plan;
460
+ /** the primitive's own result, EXCEPT a `rollups` plan, which arrives folded */
461
+ result: unknown;
462
+ /** present when `compare: 'previous'` — the same call, range shifted back */
463
+ previous?: unknown;
464
+ dataSource: 'raw' | 'rollups' | 'raw+rollups';
465
+ }
466
+
467
+ /** the fields the fold reads off a rollup doc */
468
+ export interface RollupDoc {
469
+ /** dimension values in the family's `by` order: 'region=eu', or a bare 'user:u_1' */
470
+ dims: string[];
471
+ bucketAt?: Date | string | null;
472
+ count?: number;
473
+ sums?: Record<string, number> | Map<string, number> | null;
474
+ }
475
+
476
+ /** what `breakdown()` returns, answered from the rollup store instead */
477
+ export interface FoldedRollups {
478
+ rows: Array<{ dims: (string | null)[]; at?: Date; value: number }>;
479
+ groups: number;
480
+ truncated: boolean;
481
+ dataSource: 'rollups';
482
+ }
483
+
484
+ /**
485
+ * Report → the answer: resolve, then `q[plan.primitive](scope, ...plan.args)`.
486
+ * An `Unavailable` throws with `status: 400` and the `why` as its message —
487
+ * a refusal is an answer to `resolveReport`, but not to someone asking for data.
488
+ */
489
+ export declare function executeReport(
490
+ q: Queries,
491
+ scope: string,
492
+ report: Report,
493
+ catalog: Catalog,
494
+ opts?: ExecuteOptions,
495
+ ): Promise<ReportResult>;
496
+
497
+ /**
498
+ * Fold a `rollups` plan's docs into the row shape `breakdown()` returns, per
499
+ * `Plan.shape` — the family's docs ARE the groups, so this is arithmetic rather
500
+ * than a second read, and a renderer never learns which store answered. Pure.
501
+ */
502
+ export declare function foldRollups(
503
+ rows: readonly RollupDoc[],
504
+ shape: PlanShape,
505
+ truncated?: boolean,
506
+ ): FoldedRollups;
507
+
508
+ /** '7d' → a half-open pair ending at `now`; an ISO pair validated. Throws `status: 400`. */
509
+ export declare function rangeOf(range: ReportRange, now?: Date): TimeRange;
510
+
511
+ /** the interval that keeps a range under ~120 buckets — util.js `intervalFor`, for pairs too */
512
+ export declare function intervalForRange(range: ReportRange, now?: Date): 'hour' | 'day' | 'week' | 'month';
513
+
169
514
  // ── typed emit ──────────────────────────────────────────────────────────────
170
515
 
171
516
  export type AttrsOf<R extends Registry, N extends keyof R> =
@@ -275,8 +620,29 @@ export interface TelemetryCounters {
275
620
  deduped: number;
276
621
  /** `body` values clipped to the cap — the row survives, marked */
277
622
  truncated: number;
623
+ /**
624
+ * `rollupSkipped`, attributed: `${family}|${dimLabel}` → count. Which family
625
+ * dropped which dim, so the scalar becomes a `dimDefault` you can go and
626
+ * declare. The seven numbers above are unchanged — this is additive.
627
+ */
628
+ rollupSkippedBy: Record<string, number>;
629
+ /**
630
+ * attrs keys a record carried that its spec does not declare:
631
+ * `${name}|${key}` → count. Those records are REJECTED by the strict parse,
632
+ * not stripped; this groups what the quarantine lists one row at a time.
633
+ */
634
+ undeclaredAttrs: Record<string, number>;
278
635
  }
279
636
 
637
+ /**
638
+ * Distinct keys either attributed map holds before new ones fold into a single
639
+ * `(other)` bucket. Both are keyed on client-controlled data, so the bound is
640
+ * what stops a hostile client growing the process heap; the totals stay
641
+ * honest, only the attribution stops.
642
+ */
643
+ export declare const COUNTER_MAP_MAX: 1000;
644
+ export declare const COUNTER_OVERFLOW_KEY: '(other)|(other)';
645
+
280
646
  /** What emit() did. `Promise<void>` could not distinguish "written" from "queued". */
281
647
  export interface EmitResult {
282
648
  /** the record _id — usable for correlation even when the row was not stored */
@@ -470,12 +836,33 @@ export declare function createIngest(opts: CreateIngestOptions): import('express
470
836
 
471
837
  // ── dashboard (dashboards §2–§8) ────────────────────────────────────────────
472
838
 
839
+ /**
840
+ * Two kinds of cap, and the word "limit" hides the difference. An OUTPUT cap
841
+ * bounds what the response CONTAINS — its `$limit` sits after the `$group`/sort
842
+ * or rides an indexed cursor, so the work behind it is bounded by the range and
843
+ * the indexes, not by the number. A SCAN cap bounds what the primitive READS,
844
+ * so an answer past it is an undercount — which is why all three report
845
+ * `truncated`.
846
+ */
473
847
  export interface QueryLimits {
848
+ // ── output caps ──
474
849
  records: number;
475
850
  series: number;
476
851
  rollups: number;
477
852
  trace: number;
478
853
  journey: number;
854
+ /**
855
+ * Distinct GROUPS one breakdown() returns — the top N by measure, read as
856
+ * cap+1 so truncation is observed. Never a bound on rows scanned.
857
+ */
858
+ breakdown: number;
859
+ /**
860
+ * Distinct VALUES one values() lookup returns — the top N by count, read as
861
+ * cap+1 so truncation is observed. Never a bound on rows scanned.
862
+ */
863
+ values: number;
864
+
865
+ // ── scan caps ──
479
866
  /** raw docs distribution will scan before it reports an undercount */
480
867
  distribution: number;
481
868
  /** rollup docs distinctCount will scan before it reports an undercount */
@@ -492,7 +879,8 @@ export interface TimeRange {
492
879
 
493
880
  export interface RecordFilter {
494
881
  kind?: string;
495
- name?: string;
882
+ /** one event name, or a SET of them as an `$in` — a namespace or a family is several */
883
+ name?: string | string[];
496
884
  severity?: string;
497
885
  env?: string;
498
886
  service?: string;
@@ -630,6 +1018,28 @@ export interface Queries {
630
1018
  Promise<{ items: any[]; nextCursor: string | null; dataSource: 'raw' }>;
631
1019
  series(scope: string, range: TimeRange, filter: RecordFilter, opts?: { measure?: string; interval?: 'hour' | 'day' | 'week' | 'month' }):
632
1020
  Promise<{ buckets: Array<{ at: Date; value: number }>; dataSource: 'raw' }>;
1021
+ /**
1022
+ * Top groups of a measure by one or two dimensions. `groupBy` takes
1023
+ * `attr:<key>`, `field:<path>` (an allowlist of envelope paths), `subjectType`
1024
+ * or `actorType`; 0 or 3+ dims, an unlisted path, or a bad interval throw with
1025
+ * `status: 400`. Rows carry `at` only when an `interval` is given.
1026
+ *
1027
+ * `limit` caps the GROUPS returned, never the rows scanned — the scan is
1028
+ * bounded by the range and the indexes exactly as `series` is, and truncation
1029
+ * keeps the TOP groups by measure. A record missing the dim groups under
1030
+ * `null` rather than being dropped. Aggregates across tenants under `'*'`.
1031
+ *
1032
+ * Two truncation flags, because they cut different axes: `truncated` means
1033
+ * groups were dropped, `bucketsTruncated` that the per-interval pass hit its
1034
+ * own ceiling (`limits.series` buckets per returned group) and some group
1035
+ * shown is missing periods. `bucketsTruncated` is always false with no
1036
+ * `interval`.
1037
+ *
1038
+ * `sum:durationMs` / `avg:durationMs` read the ENVELOPE field, not
1039
+ * `metrics.durationMs` — a span's duration is not a declared metric.
1040
+ */
1041
+ breakdown(scope: string, range: TimeRange, filter: RecordFilter, opts: { groupBy: string[]; measure?: string; interval?: 'hour' | 'day' | 'week' | 'month'; limit?: number }):
1042
+ Promise<{ rows: Array<{ dims: (string | null)[]; at?: Date; value: number }>; groups: number; truncated: boolean; bucketsTruncated: boolean; dataSource: 'raw' }>;
633
1043
  /**
634
1044
  * The sample is complete — nothing is sampled away between the match and the
635
1045
  * math — but `$percentile` is `method: 'approximate'` and the scan stops at
@@ -678,17 +1088,75 @@ export declare function createQueries(ctx: {
678
1088
  cacheSize?: number;
679
1089
  }): Queries;
680
1090
 
1091
+ // ── values (reports §5) ─────────────────────────────────────────────────────
1092
+
1093
+ /**
1094
+ * The observed domain of one dimension — the lookup a report builder makes
1095
+ * before it names a value. NOT a tenth primitive: it reads the catalog, which
1096
+ * the primitives deliberately do not, and it answers from whichever of four
1097
+ * sources is cheapest.
1098
+ */
1099
+ export interface ValuesParams {
1100
+ /**
1101
+ * A `DimFacet.key` — `attr:model`, `field:client.platform`, `subjectType`,
1102
+ * `actorType`. The literal `'subject'` also works and is the only way to ask
1103
+ * a family for its subject refs.
1104
+ */
1105
+ dim: string;
1106
+ /** the Report's source events: decides the raw step, narrows the other two */
1107
+ names?: string[];
1108
+ /** required by the raw step; ignored by the others */
1109
+ range?: TimeRange;
1110
+ /** values cap, default `limits.values` (200), clamped to it */
1111
+ limit?: number;
1112
+ }
1113
+
1114
+ export interface ValuesResult {
1115
+ /** catalog order for a declared enum, else by count desc then value asc */
1116
+ values: string[];
1117
+ /** parallel to `values` when the source can count — absent for 'catalog' */
1118
+ counts?: number[];
1119
+ /**
1120
+ * Which of the four answered, in preference order: `catalog` (a declared
1121
+ * enum — no read at all), `rollups` (one indexed `$group` over a family keyed
1122
+ * by the dim), `raw` (an indexed attr or envelope dim over the range), or
1123
+ * `none`. `none` is an ANSWER, never an error: the caller offers free-text
1124
+ * equality with a scan badge.
1125
+ */
1126
+ source: 'catalog' | 'rollups' | 'raw' | 'none';
1127
+ /** the family read, when `source === 'rollups'` */
1128
+ via?: string;
1129
+ /** more values existed than the cap; the ones kept are the top by count */
1130
+ truncated: boolean;
1131
+ dataSource: 'catalog' | 'rollups' | 'raw' | 'none';
1132
+ }
1133
+
1134
+ export interface ValuesCtx {
1135
+ catalog: Catalog;
1136
+ TelemetryModel: Model<any>;
1137
+ RollupModel: Model<any>;
1138
+ limits?: Partial<QueryLimits>;
1139
+ onSlowQuery?: (info: { op: string; ms: number; params: unknown }) => void;
1140
+ slowMs?: number;
1141
+ cacheTtlMs?: number;
1142
+ cacheSize?: number;
1143
+ }
1144
+
1145
+ export type Values = (scope: string, params: ValuesParams) => Promise<ValuesResult>;
1146
+
1147
+ /** memoized like `series`; never throws on the `none` path */
1148
+ export declare function createValues(ctx: ValuesCtx): Values;
1149
+
681
1150
  export interface ViewSpec {
682
1151
  name: string;
683
1152
  icon?: string;
684
- page: 'errors' | 'traces' | 'events' | 'journeys' | 'usage' | 'overview' | 'system';
685
- query: {
686
- range?: string;
687
- filters?: Record<string, unknown>;
688
- groupBy?: string;
689
- sort?: string;
690
- display?: 'table' | 'series' | 'breakdown' | 'stream';
691
- };
1153
+ page: 'errors' | 'traces' | 'events' | 'journeys' | 'usage' | 'overview' | 'system' | 'explore';
1154
+ /**
1155
+ * A Report — or the pre-Report shape, which every stored view still carries
1156
+ * and `normalizeQuery()` lifts. `spec` is a Mixed document, so nothing has to
1157
+ * migrate: a query with no `source` is read as legacy.
1158
+ */
1159
+ query: Report | LegacyQuery;
692
1160
  }
693
1161
 
694
1162
  export interface ResolvedView extends ViewSpec {
@@ -698,8 +1166,13 @@ export interface ResolvedView extends ViewSpec {
698
1166
  shared?: boolean;
699
1167
  }
700
1168
 
701
- /** derived views — generated from the registry, zero config */
702
- export declare function deriveViews(registry: Registry): ResolvedView[];
1169
+ /**
1170
+ * Derived views — generated from the registry, zero config. Five shapes, every
1171
+ * one a Report: per event, per rollup family, per namespace, per usage event
1172
+ * that meters money, and one funnel per subject type. Pass the boot-time
1173
+ * catalog to skip re-deriving one.
1174
+ */
1175
+ export declare function deriveViews(registry: Registry, catalog?: Catalog): ResolvedView[];
703
1176
 
704
1177
  export interface Viewer {
705
1178
  /**