@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
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The in-memory evaluator for a {@link BucketMeasure} (ADR 0051 §4).
3
+ *
4
+ * `buildMetricBuckets` renders a measure to SQL for the four persistent stores.
5
+ * The collector also ships an in-memory store — the one the playground and the
6
+ * end-to-end harness boot without a database — which has no SQL to run. Rather
7
+ * than let the insight endpoints go dark there (an empty series reads as "no
8
+ * data", which is a different and misleading claim), the same declarative
9
+ * measure is evaluated directly over events here.
10
+ *
11
+ * Pure, and deliberately the *same shape* as the SQL: one pass that groups by
12
+ * bucket, then one aggregate per bucket, using the same closed predicate
13
+ * vocabulary. `src/__tests__/insights.test.ts` runs both paths over the same
14
+ * rows so the two cannot drift.
15
+ */
16
+ import { resolveBucketMeasure, BUCKET_SECONDS, } from "./measures.js";
17
+ import { byBucketThenDimension } from "./buckets.js";
18
+ import { quantile } from "./stats.js";
19
+ /** The vector columns a geometry predicate can guard. */
20
+ const VECTORS = ["position", "direction", "hit_point", "screen"];
21
+ /**
22
+ * The split dimension's value on one event — the in-memory mirror of
23
+ * `BUCKET_SPLIT_COLUMNS` (#306). `''` is "unknown", the same reading the stores'
24
+ * `NOT NULL DEFAULT ''` columns give.
25
+ */
26
+ function splitValueOf(event, dimension) {
27
+ switch (dimension) {
28
+ case "scene":
29
+ return event.scene_id ?? "";
30
+ case "event_type":
31
+ return event.event_type ?? "";
32
+ case "mesh":
33
+ return event.mesh ?? "";
34
+ case "name":
35
+ return event.name ?? "";
36
+ case "source":
37
+ return event.source ?? "";
38
+ }
39
+ }
40
+ /** Whether an event satisfies one predicate from the closed vocabulary. */
41
+ function matches(event, predicate) {
42
+ switch (predicate.kind) {
43
+ case "eq":
44
+ return (event[predicate.column] ?? "") === predicate.value;
45
+ case "ne":
46
+ return (event[predicate.column] ?? "") !== predicate.value;
47
+ case "in":
48
+ return predicate.values.includes(event[predicate.column] ?? "");
49
+ case "geometry": {
50
+ const column = VECTORS.find((name) => name === predicate.column);
51
+ const vector = column == null ? undefined : event[column];
52
+ return Array.isArray(vector) && vector.length === predicate.arity;
53
+ }
54
+ }
55
+ }
56
+ /** The numeric value a measure's aggregate reads off one event. */
57
+ function valueOf(event, measure) {
58
+ if (measure.aggregate.kind === "count" || measure.aggregate.kind === "sessions")
59
+ return null;
60
+ const raw = event[measure.aggregate.column];
61
+ if (typeof raw !== "number" || !Number.isFinite(raw))
62
+ return null;
63
+ // `js_heap_bytes` of 0 means "not reported" — excluded from averages and
64
+ // percentiles, kept for a max. Mirrors `valueColumnExpr` in `buckets.ts`.
65
+ if (measure.aggregate.column === "js_heap_bytes" &&
66
+ raw === 0 &&
67
+ measure.aggregate.kind !== "max") {
68
+ return null;
69
+ }
70
+ return raw;
71
+ }
72
+ /**
73
+ * Evaluate a metric's bucket series over events held in memory.
74
+ *
75
+ * Returns the same rows `buildMetricBuckets` would: ascending by bucket, with
76
+ * buckets that matched nothing simply absent. Throws for a metric with no
77
+ * portable bucket form, exactly as the builder does.
78
+ */
79
+ export function evaluateBucketMeasure(events, opts) {
80
+ const measure = resolveBucketMeasure(opts.metric, opts.series);
81
+ if (measure == null) {
82
+ throw new Error(opts.series == null
83
+ ? `metric '${opts.metric}' has no portable bucket series`
84
+ : `metric '${opts.metric}' declares no '${opts.series}' series`);
85
+ }
86
+ const width = BUCKET_SECONDS[opts.bucket ?? "day"] * 1000;
87
+ const types = measure.eventTypes.length > 0 ? new Set(measure.eventTypes) : null;
88
+ // Keyed by bucket, or by `bucket|dimension` on a grouped read (#306) — the
89
+ // in-memory counterpart of adding the column to the SQL `GROUP BY`.
90
+ const buckets = new Map();
91
+ for (const event of events) {
92
+ if (!Number.isFinite(event.ts))
93
+ continue;
94
+ if (opts.since != null && event.ts < opts.since)
95
+ continue;
96
+ if (opts.until != null && event.ts >= opts.until)
97
+ continue;
98
+ if (opts.scene != null && opts.scene.length > 0 && event.scene_id !== opts.scene)
99
+ continue;
100
+ if (types != null && !types.has(event.event_type))
101
+ continue;
102
+ if ((measure.where ?? []).some((predicate) => !matches(event, predicate)))
103
+ continue;
104
+ const start = Math.floor(event.ts / width) * width;
105
+ const dimension = opts.groupBy == null ? null : splitValueOf(event, opts.groupBy);
106
+ const key = dimension == null ? String(start) : `${start}|${dimension}`;
107
+ let entry = buckets.get(key);
108
+ if (entry == null) {
109
+ entry = { start, dimension, values: [], count: 0, sessions: new Set() };
110
+ buckets.set(key, entry);
111
+ }
112
+ entry.count += 1;
113
+ entry.sessions.add(event.session_id);
114
+ const value = valueOf(event, measure);
115
+ if (value != null)
116
+ entry.values.push(value);
117
+ }
118
+ const rows = [];
119
+ for (const entry of buckets.values()) {
120
+ rows.push({
121
+ bucket: entry.start,
122
+ value: aggregate(entry, measure),
123
+ sample_size: measure.aggregate.kind === "sessions" ? entry.sessions.size : entry.count,
124
+ ...(entry.dimension == null ? {} : { dimension_value: entry.dimension }),
125
+ });
126
+ }
127
+ return rows.sort(byBucketThenDimension);
128
+ }
129
+ /** Apply a measure's aggregate to one bucket's accumulated values. */
130
+ function aggregate(entry, measure) {
131
+ switch (measure.aggregate.kind) {
132
+ case "count":
133
+ return entry.count;
134
+ case "sessions":
135
+ return entry.sessions.size;
136
+ case "sum": {
137
+ if (entry.values.length === 0)
138
+ return null;
139
+ let total = 0;
140
+ for (const value of entry.values)
141
+ total += value;
142
+ return total;
143
+ }
144
+ case "avg": {
145
+ if (entry.values.length === 0)
146
+ return null;
147
+ let total = 0;
148
+ for (const value of entry.values)
149
+ total += value;
150
+ return total / entry.values.length;
151
+ }
152
+ case "max":
153
+ return entry.values.length === 0 ? null : Math.max(...entry.values);
154
+ case "quantile":
155
+ return quantile(entry.values, measure.aggregate.q);
156
+ }
157
+ }
158
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.js","sourceRoot":"","sources":["../../src/insights/evaluate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * **`scene_health`** — "which scene is in trouble, and why?" (ADR 0051 §4, sketch §D).
3
+ *
4
+ * One number per scene, and — more importantly — the six numbers it was built
5
+ * from. A single score is only useful if it can be taken apart: every factor
6
+ * reports the **metric id** behind it, the **raw** value that metric produced,
7
+ * the **normalised** 0–100 it contributed and the **weight** it carried, so
8
+ * "the lobby is at 38" is always one step away from "because its 5th-percentile
9
+ * FPS is 19 against a project norm of 47".
10
+ *
11
+ * Pure TypeScript over the same bucket series as every other primitive.
12
+ *
13
+ * ## The score is relative, and says so
14
+ *
15
+ * There is no absolute scale on which 60 FPS is "healthy" — a marketing
16
+ * configurator and a six-player VR game do not share one — so every factor is
17
+ * normalised against **the project's own baseline over the previous equal
18
+ * window**, using the same robust centre-and-spread `movers` ranks with
19
+ * (median, and a MAD floored at 1% of the level). A factor exactly at the
20
+ * project norm scores **50**; {@link HEALTH_Z_SPAN} robust deviations better
21
+ * scores 100, the same distance worse scores 0.
22
+ *
23
+ * That makes the score a comparison, not a grade: a project where every scene
24
+ * is equally bad reads 50 across the board. It is the right shape for the
25
+ * question actually asked ("*which* scene should I look at this week"), and the
26
+ * registry entry's caveats say plainly that it is not a quality bar.
27
+ *
28
+ * ## Factors are declared, not inferred
29
+ *
30
+ * {@link HEALTH_FACTORS} is a fixed, ordered catalog. Each entry names the
31
+ * metric it reads, whether it is a level or a rate (and which catalog series
32
+ * supply the numerator and denominator), which direction is good, and its
33
+ * default weight. Nothing here is derived from a request, so the fan-out is a
34
+ * compile-time constant and a caller can only change the **weights** — never
35
+ * which series are read.
36
+ */
37
+ import type { MetricId } from "@uptimizr/metrics";
38
+ import type { MetricBucketRow } from "./buckets.js";
39
+ import { type BucketGrain, type BucketVariant } from "./measures.js";
40
+ import { type ResolvedWindow } from "./windows.js";
41
+ /** How one health factor is measured. */
42
+ export interface HealthFactorSpec {
43
+ /** Stable factor id — the key a `weights` override addresses it by. */
44
+ readonly id: string;
45
+ /** The registry metric this factor is a reading of. */
46
+ readonly metric: MetricId;
47
+ /**
48
+ * The series that supplies the factor's numerator: a named catalog variant,
49
+ * or `undefined` for the metric's own headline series.
50
+ */
51
+ readonly numerator?: BucketVariant;
52
+ /**
53
+ * The series that supplies the denominator, making this factor a **rate**.
54
+ * `undefined` for a level factor (an FPS percentile), which is read directly.
55
+ */
56
+ readonly denominator?: BucketVariant;
57
+ /** Whether a *higher* raw value is healthier. */
58
+ readonly direction: "up" | "down";
59
+ /** Default weight in the score. Overridable per request. */
60
+ readonly weight: number;
61
+ /** What one unit of the raw value is. */
62
+ readonly unit: string;
63
+ /** What the raw number actually measures — copied into the row. */
64
+ readonly note: string;
65
+ }
66
+ /**
67
+ * The six factors, in the order they are reported.
68
+ *
69
+ * The weights are a judgement, and they are declared here (and surfaced in the
70
+ * registry entry, so they appear in `capabilities` and in the generated tool
71
+ * catalog) precisely so that judgement is arguable rather than hidden: errors
72
+ * and raw smoothness dominate, frustration signals matter, and exploration is a
73
+ * tiebreak. A project that disagrees passes `weights`.
74
+ */
75
+ export declare const HEALTH_FACTORS: readonly HealthFactorSpec[];
76
+ /** Every factor id, for validating a `weights` override. */
77
+ export declare const HEALTH_FACTOR_IDS: readonly string[];
78
+ /** The declared default weights, as the registry entry publishes them. */
79
+ export declare const HEALTH_DEFAULT_WEIGHTS: Readonly<Record<string, number>>;
80
+ /**
81
+ * How many robust deviations from the project norm span half the scale.
82
+ *
83
+ * At the norm a factor scores 50; `HEALTH_Z_SPAN` deviations better scores 100
84
+ * and the same distance worse scores 0. Four is chosen so that the everyday
85
+ * range of a metric occupies the middle of the scale rather than saturating it
86
+ * — a scene one MAD off the norm reads 62 or 38, which is a nudge, and it takes
87
+ * a genuinely unusual reading to bottom out.
88
+ */
89
+ export declare const HEALTH_Z_SPAN = 4;
90
+ /** Decimals a health number is rounded to before it leaves the API. */
91
+ export declare const HEALTH_PRECISION = 6;
92
+ /**
93
+ * Sessions below which a scene's score is not worth reading.
94
+ *
95
+ * Not a filter: the row is returned with its `sampleSize` so a caller can see
96
+ * why, exactly as `movers` keeps its sub-`minSample` rows.
97
+ */
98
+ export declare const HEALTH_MIN_SESSIONS = 20;
99
+ /** How many scenes one request may score when the caller names none. */
100
+ export declare const HEALTH_DEFAULT_SCENES = 5;
101
+ /** The hard cap on scenes per request — the fan-out is linear in this. */
102
+ export declare const HEALTH_MAX_SCENES = 10;
103
+ /** Default window, in days. */
104
+ export declare const HEALTH_DEFAULT_WINDOW_DAYS = 7;
105
+ /**
106
+ * Resolve the window a health score is computed over, and the project baseline
107
+ * window immediately before it.
108
+ *
109
+ * **This is the one insight primitive whose window includes the bucket in
110
+ * progress**, and the difference is deliberate. `baseline` and `movers` floor
111
+ * `until` down to the last complete bucket because they report *counts*, and a
112
+ * third of a day of traffic compared against whole days reads as a collapse
113
+ * every morning. Every health factor is a **rate or a percentile** — errors per
114
+ * session, long frames per sampled window, the 5th-percentile FPS — and none of
115
+ * those is distorted by a partial bucket: half a day of data gives half the
116
+ * numerator *and* half the denominator.
117
+ *
118
+ * Flooring here would instead make the score answer about **yesterday**, which
119
+ * is the wrong answer to 'which scene is in trouble' and, on a dashboard whose
120
+ * range is the last hour, would show an empty tile all day.
121
+ *
122
+ * The baseline is the equal window immediately before — the same 'previous
123
+ * equal window' reference `movers` uses, so a health score and a movers list
124
+ * are read against the same past.
125
+ */
126
+ export declare function resolveHealthWindows(opts: {
127
+ since?: number;
128
+ until?: number;
129
+ windowDays?: number;
130
+ bucket: BucketGrain;
131
+ now: number;
132
+ }): {
133
+ range: ResolvedWindow;
134
+ baseline: ResolvedWindow;
135
+ };
136
+ /** One factor's contribution to a scene's score. */
137
+ export interface HealthFactorRow {
138
+ /** The factor id — the key a `weights` override addresses. */
139
+ id: string;
140
+ /** The registry metric behind the raw value. */
141
+ metric: string;
142
+ /** The value that metric produced over the window, in `unit`. */
143
+ raw: number | null;
144
+ /** The project baseline this was normalised against — the previous window's median. */
145
+ baseline: number | null;
146
+ /** 0–100 against that baseline; 50 is exactly the project norm. `null` when unavailable. */
147
+ score: number | null;
148
+ /** The weight this factor carried. */
149
+ weight: number;
150
+ unit: string;
151
+ /** What the raw number is, and why it is normalised the way it is. */
152
+ note: string;
153
+ }
154
+ /** What `scene_health` reports. One row per scene. */
155
+ export interface SceneHealthRow {
156
+ /** The scene scored; `''` when the request spanned every scene. */
157
+ scene: string;
158
+ /** Weighted mean of the available factors' scores, 0–100; `null` when none were. */
159
+ score: number | null;
160
+ factors: HealthFactorRow[];
161
+ /** Sessions behind the window — what decides whether the score means anything. */
162
+ sampleSize: number;
163
+ since: number;
164
+ until: number;
165
+ }
166
+ /** The two bucket series one factor reads, over one window. */
167
+ export interface HealthSeries {
168
+ numerator: readonly MetricBucketRow[];
169
+ /** Absent for a level factor. */
170
+ denominator?: readonly MetricBucketRow[];
171
+ }
172
+ /** Everything `computeSceneHealth` needs about one factor. */
173
+ export interface HealthFactorInput {
174
+ /** The scene's series over the scored window. */
175
+ current: HealthSeries;
176
+ /** The project's series over the preceding equal window — the baseline. */
177
+ baseline: HealthSeries;
178
+ }
179
+ /**
180
+ * Map a raw value onto 0–100 against a baseline centre and spread.
181
+ *
182
+ * Linear in the robust z and clamped, rather than a logistic or a percentile
183
+ * rank: a reader has to be able to do the arithmetic backwards from the row.
184
+ * `null` when the baseline carried nothing to compare against — an unscored
185
+ * factor, never a default of 50, because "we have no history for this" and
186
+ * "this is exactly normal" are different statements.
187
+ */
188
+ export declare function normaliseFactor(raw: number | null, baseline: readonly number[], direction: "up" | "down"): {
189
+ score: number | null;
190
+ centre: number | null;
191
+ };
192
+ /**
193
+ * Resolve the weights a request scores with.
194
+ *
195
+ * An override replaces a factor's weight; factors the override does not mention
196
+ * keep their declared weight, so `weights={"error_rate":0.6}` means "I care
197
+ * much more about errors" rather than "score errors only". Unknown ids are
198
+ * rejected at the route, not silently ignored.
199
+ */
200
+ export declare function resolveWeights(override?: Readonly<Record<string, number>> | null): Record<string, number>;
201
+ /**
202
+ * Score one scene.
203
+ *
204
+ * `inputs` is keyed by factor id; a factor with no entry (a project with no XR
205
+ * traffic, a scene with no perf samples) is reported with `score: null` and
206
+ * excluded from the weighted mean rather than dropped from the row — the reason
207
+ * a score is missing a factor has to be visible in the answer.
208
+ */
209
+ export declare function computeSceneHealth(opts: {
210
+ scene: string;
211
+ since: number;
212
+ until: number;
213
+ sampleSize: number;
214
+ inputs: Readonly<Record<string, HealthFactorInput | undefined>>;
215
+ weights?: Readonly<Record<string, number>> | null;
216
+ }): SceneHealthRow;
217
+ /** Least healthy first, then by scene id so the order is a function of the rows. */
218
+ export declare function rankSceneHealth(rows: readonly SceneHealthRow[]): SceneHealthRow[];
219
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/insights/health.ts"],"names":[],"mappings":"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"}