@uptimizr/metrics 0.0.0 → 0.2.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 CHANGED
@@ -8,6 +8,8 @@
8
8
  The **semantic metric registry**: one machine-readable `MetricDefinition` per Uptimizr analytics
9
9
  metric. It is the single source of truth for _what can be asked_ of a collector — the endpoint, the
10
10
  filters, the shape and units of a row, the row caps, how to read the answer and what not to trust.
11
+ It also owns the Zod mirrors of the three **result envelopes** (`format=full | table | summary`,
12
+ ADR 0051 §2), so the collector, the tool catalog and the MCP server describe one set of shapes.
11
13
 
12
14
  It is **pure data**: `zod` and a type-only `@uptimizr/schema` import, nothing else. No `node:`
13
15
  built-in, no DOM, no database driver. Import it from a browser bundle, a worker or an `npx` CLI.
@@ -35,6 +37,34 @@ for (const metric of allMetrics()) {
35
37
  const metric: MetricDefinition | undefined = getMetric("perf_summary");
36
38
  ```
37
39
 
40
+ ```ts
41
+ import { resultEnvelopeSchema, structuredEnvelopeSchema, getMetric } from "@uptimizr/metrics";
42
+
43
+ const row = getMetric("top_meshes")!.row;
44
+ // Union of the three envelopes, for a caller that can express one (a route's
45
+ // 200 schema, a parser): rows[] | { meta, rows } | a summary digest.
46
+ resultEnvelopeSchema(row).parse(await response.json());
47
+ // The merged OBJECT form of the same union, for an MCP `outputSchema` — the
48
+ // SDK drops an output schema that is not an object, unions included.
49
+ structuredEnvelopeSchema(row);
50
+ ```
51
+
52
+ This package also owns the **vocabulary half** of validating a query-DSL document
53
+ (ADR 0051 §3). `@uptimizr/schema`'s `queryV1Schema` checks the shape; `validateQuery` answers
54
+ the registry's questions and returns them as data:
55
+
56
+ ```ts
57
+ import { validateQuery, nativeDimensions, genericDimensions } from "@uptimizr/metrics";
58
+
59
+ const { issues, metric, tier } = validateQuery(query); // [] issues means it can be run
60
+ issues[0]?.code; // "unsupported_filter" | "dimension_not_native" | "limit_too_large" | …
61
+ issues[0]?.accepted; // what *would* have worked, when that is a closed list
62
+ tier; // "delegated" (the metric's own builder) | "generic" (the shared group-by)
63
+
64
+ nativeDimensions(metric!); // the grain its rows carry — `metric.grainDimensions`
65
+ genericDimensions(metric!); // what it can *also* be grouped by, or [] if it cannot
66
+ ```
67
+
38
68
  ## Rules for agents
39
69
 
40
70
  - **Derive, never restate.** Tool catalogs, OpenAPI paths, capability lists and docs tables are
@@ -51,10 +81,109 @@ const metric: MetricDefinition | undefined = getMetric("perf_summary");
51
81
  calls. Rename nothing; add instead.
52
82
  - **Respect `limits`.** `maxRows` is the registry's promise that no answer is unbounded;
53
83
  `maxSummaryRows` is what a model should be shown.
84
+ - **Describe a result with the envelope schemas, never a hand-written copy.** `tableEnvelopeSchema`,
85
+ `summaryEnvelopeSchema`, `resultEnvelopeSchema` and `structuredEnvelopeSchema` are the one
86
+ definition; `@uptimizr/db/summary` re-exports them under its established names, and the summariser
87
+ that _builds_ an envelope still lives there.
54
88
  - **Read `caveats` before you conclude anything.** They record small-sample, sampling-rate and
55
89
  capture-gating conditions — a metric with no enabled source channel returns an honest empty
56
90
  result, not a zero.
57
91
  - **`null` is not `0`.** An aggregate over no samples is SQL `NULL` and means "no data".
92
+ - **`dimensions` is not the grain.** `MetricDefinition.dimensions` lists what a metric can be
93
+ _filtered, keyed or regrouped_ by; `grainDimensions` (read it through `nativeDimensions`) is what
94
+ its rows are actually keyed by. `top_meshes` declares `session` and returns one row per mesh.
95
+ - **`grainDimensions` is declared, not derived.** It used to be read back out of `row.shape`; since
96
+ #304 it is registry data, and `src/__tests__/registry.test.ts` keeps the old derivation as the
97
+ gate on the declaration. Add it to every new entry.
98
+ - **`genericGroupBy` is a claim about portability.** Declare it only where the measure is a
99
+ `count(*)`, a `count(DISTINCT session_id)`, or a `sum`/`avg`/`max` over a **promoted** column —
100
+ the shapes that render identically on all four engines at any grain. A spatial binning and a
101
+ percentile do not, and must not have one. Every measure column it names must also be a column of
102
+ the metric's `row`.
103
+ - **`sourceChannels` is load-bearing.** The collector's project context document
104
+ (`GET /api/v1/context`, ADR 0051 §5) reports a metric under `metrics.disabledByCapture` when
105
+ **every** channel it declares produced no events over the window, which is how an agent learns to
106
+ say "that channel is off" instead of reporting the zero as a finding. An empty `sourceChannels`
107
+ means a derived rollup and is never reported as disabled — so declare the channels a metric
108
+ really reads, no more and no fewer.
109
+
110
+ ## Derived metrics (ADR 0051 §4)
111
+
112
+ Most entries name a `build*` aggregation. Five do not, and are not store resources either: the
113
+ insight primitives `insight_baseline`, `insight_movers`, `insight_anomalies`,
114
+ `insight_significance` and `insight_scene_health` are computed in pure TypeScript _over other
115
+ metrics' data_ (`@uptimizr/db`'s `src/insights/`). They carry `derived: "insight"`, and there are
116
+ three kinds of entry rather than two:
117
+
118
+ | Predicate | Entry |
119
+ | ------------------- | -------------------------------------------------------------- |
120
+ | `isResourceMetric` | A store read — no builder, no querystring, no `format`. |
121
+ | `isDerivedMetric` | Computed in TypeScript; a real aggregate with an endpoint. |
122
+ | `isAggregateMetric` | Either a builder or a derivation — i.e. "takes a querystring". |
123
+
124
+ Prefer `isAggregateMetric` over `metric.builder != null` anywhere the question is "is this served as
125
+ an aggregate?", or a derived metric silently drops out of the envelope and OpenAPI surfaces.
126
+
127
+ Two things a derived entry carries that are worth reading before you use one. `insight_significance`
128
+ picks its statistical test from the **compared** metric's own entry — a headline column whose
129
+ `rateOf` names a denominator gets a two-proportion test, a bare count gets a Poisson rate test, and
130
+ everything else gets Welch's t — so `columns[...].rateOf` is load-bearing, not decoration.
131
+ `insight_scene_health` declares its per-factor **weights in its `caveats`** so they are visible in
132
+ `capabilities` and in the generated tool catalog; they are a judgement, published so it can be
133
+ argued with and overridden per request.
134
+
135
+ ## Panel specs (ADR 0051 §7)
136
+
137
+ The same split, one level up. `@uptimizr/schema`'s `panelSpecV1Schema` says a spec is well formed;
138
+ `validatePanelSpec(spec)` says whether it will draw anything. It runs `validateQuery` over the
139
+ spec's query (prefixing each issue's `path` with `query.`) and adds the two checks only the registry
140
+ can make — does the chart suit the metric's grain, and does the `encoding` name columns the metric
141
+ actually returns:
142
+
143
+ ```ts
144
+ import {
145
+ validatePanelSpec,
146
+ suggestChart,
147
+ defaultEncoding,
148
+ chartsForMetric,
149
+ } from "@uptimizr/metrics";
150
+
151
+ const { issues, metric, tier } = validatePanelSpec(spec); // [] issues means it can be pinned
152
+ issues[0]?.code; // the DSL's codes, plus "chart_grain_mismatch" | "unknown_encoding_column"
153
+ issues[0]?.accepted; // the charts that would have worked, or the metric's real result columns
154
+
155
+ suggestChart(metric, query); // what to pre-fill; always satisfies `chartSuitsMetric`
156
+ defaultEncoding(metric, chart); // the metric's axis/label on `x`, its measure on `y`
157
+ ```
158
+
159
+ `PANEL_CHART_RULES` is the compatibility table as data — the validator, `suggestChart` and the
160
+ published table all read it, so they cannot drift:
161
+
162
+ | Chart | Requires |
163
+ | --------------- | -------------------------------------------------------------------------------------------------------------- |
164
+ | `table` | nothing — any metric's rows can be listed |
165
+ | `stat` | a single-record result (a `project`-grain metric with no grain dimensions) |
166
+ | `bar` | a label column, a measure column, and a ranked or bucketed grain (`mesh`, `scene`, `session`, `row`, `bucket`) |
167
+ | `line` / `area` | an ordered axis column — in practice a `bucket`-grain metric |
168
+ | `heatmap2d` | a `bin` grain (the metric already bins its input into a grid) |
169
+ | `world3d` | a `voxel` grain (the metric already bins its input into world-space cells) |
170
+
171
+ `chartSuitsMetric(chart, metric)` is the predicate behind one row; `chartsForMetric(metric)` lists
172
+ every chart that fits. `axisColumn` / `labelColumn` / `measureColumn` read the column semantics the
173
+ rules depend on, and `resultColumns(metric, query)` is what an encoding is checked against — the
174
+ metric's declared `row` on the delegated tier, but the grouped dimensions plus the declared measures
175
+ on the generic tier, because a regrouped `top_meshes` has no `mesh` column.
176
+
177
+ **Rules for agents:**
178
+
179
+ - **A pinned panel is read much later than it is written**, which is why the chart is checked at all.
180
+ A `line` over `top_meshes` does not fail — it draws something a reader takes for a trend. Pin time
181
+ is the moment somebody is paying attention, so that is where the refusal belongs.
182
+ - **Widen the table with a `PanelChartRule`, never a special case.** A rule's `requires` string is
183
+ what the collector's `400` quotes; `src/__tests__/panelSpec.test.ts` pins the whole chart × metric
184
+ matrix and the rendered table.
185
+ - A rule may read only the **grain and the column semantics**, like everything else here. If a metric
186
+ charts badly, fix its `label` / `axis` / `measure` declarations.
58
187
 
59
188
  ## Where the SQL lives
60
189
 
package/README.md CHANGED
@@ -7,7 +7,9 @@
7
7
 
8
8
  Everything that used to restate the query surface by hand is **derived** from this package: the
9
9
  agent tool catalog in [`@uptimizr/agent-core`](../agent-core), the `uptimizr://capabilities`
10
- resource and tool output schemas in [`@uptimizr/mcp`](../mcp), the collector's
10
+ resource and tool output schemas in [`@uptimizr/mcp`](../mcp) — which describe a row **and** the
11
+ `format=full | table | summary` result envelopes this package also defines (ADR 0051 §2) — the
12
+ collector's
11
13
  `GET /api/v1/openapi.json` document, and the endpoint/tool tables in the docs. Coverage therefore
12
14
  cannot drift: CI fails when an aggregation has no registry entry, when a row schema does not match
13
15
  real query output, or when an endpoint's querystring keys diverge from its declared filters.
@@ -28,12 +30,12 @@ pnpm add @uptimizr/metrics
28
30
  ```ts
29
31
  import { allMetrics, getMetric, METRIC_IDS } from "@uptimizr/metrics";
30
32
 
31
- METRIC_IDS.length; // 71 metrics (69 aggregations + 2 store resources)
33
+ METRIC_IDS.length; // 78 metrics (70 aggregations + 5 insight derivations + 3 store resources)
32
34
 
33
35
  const metric = getMetric("top_meshes");
34
36
  metric?.title; // "Most-interacted meshes"
35
37
  metric?.endpoint?.path; // "/api/v1/meshes/top"
36
- metric?.filters; // ["since", "until", "bins", "limit", "session"]
38
+ metric?.filters; // ["since", "until", "bins", "limit", "session", "format"]
37
39
  metric?.row; // z.ZodObject — the shape of one row
38
40
  metric?.columns.count?.unit; // "count"
39
41
  metric?.limits.maxRows; // the hard cap no consumer may exceed
@@ -44,23 +46,23 @@ const byCategory = Object.groupBy(allMetrics(), (m) => m.category);
44
46
 
45
47
  ## What a `MetricDefinition` carries
46
48
 
47
- | Field | Meaning |
48
- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `id` | Stable snake_case id. Also the DSL metric name and the **agent tool name**. |
50
- | `title`, `description` | Agent-facing prose: what it measures and what one row is. |
51
- | `builder` | The `build*` aggregation in `@uptimizr/db` that computes it. Absent for the two **resource** entries (`session_meta`, `scene_representation`), which are store reads. |
52
- | `endpoint` | `{ method, path, pathParams }` — the canned collector route, when one exists. |
53
- | `grain` | What one row represents (`project`, `scene`, `session`, `mesh`, `bin`, `voxel`, `bucket`, `row`). |
54
- | `dimensions` | The `DimensionId`s the rows are keyed by. Closed vocabulary. |
55
- | `filters` | The `FilterId`s the endpoint accepts — exactly its Zod querystring keys. |
56
- | `row` | `z.ZodObject` for one row: the source for OpenAPI, tool output schemas and numeric coercion. |
57
- | `columns` | Per-column `{ description, unit, measure, label, rateOf }`. |
58
- | `limits` | `{ maxRows, maxSummaryRows }` — no consumer can ask for an unbounded payload. |
59
- | `interpretation` | How to read the result. |
60
- | `caveats` | Small-sample, capture-gating and sampling-rate warnings. |
61
- | `sourceChannels` | The `EventType` capture channels that feed it (ADR 0012). |
62
- | `related`, `comparable` | Metrics worth reading alongside; comparison semantics for deltas. |
63
- | `category` | Grouping used by the docs, the capabilities resource and the health score. |
49
+ | Field | Meaning |
50
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `id` | Stable snake_case id. Also the DSL metric name and the **agent tool name**. |
52
+ | `title`, `description` | Agent-facing prose: what it measures and what one row is. |
53
+ | `builder` | The `build*` aggregation in `@uptimizr/db` that computes it. Absent for the three **resource** entries (`session_meta`, `session_narrative`, `scene_representation`), which are store reads, and for the five `derived: "insight"` entries, which are computed in TypeScript. |
54
+ | `endpoint` | `{ method, path, pathParams }` — the canned collector route, when one exists. |
55
+ | `grain` | What one row represents (`project`, `scene`, `session`, `mesh`, `bin`, `voxel`, `bucket`, `row`). |
56
+ | `dimensions` | The `DimensionId`s it can be filtered, keyed or regrouped by — `grainDimensions` is what its rows are actually keyed by. Closed vocabulary. |
57
+ | `filters` | The `FilterId`s the endpoint accepts — exactly its Zod querystring keys. |
58
+ | `row` | `z.ZodObject` for one row: the source for OpenAPI, tool output schemas (wrapped in the result envelopes below) and numeric coercion. |
59
+ | `columns` | Per-column `{ description, unit, measure, label, rateOf }`. |
60
+ | `limits` | `{ maxRows, maxSummaryRows }` — no consumer can ask for an unbounded payload. |
61
+ | `interpretation` | How to read the result. |
62
+ | `caveats` | Small-sample, capture-gating and sampling-rate warnings. |
63
+ | `sourceChannels` | The `EventType` capture channels that feed it (ADR 0012). |
64
+ | `related`, `comparable` | Metrics worth reading alongside; comparison semantics for deltas. |
65
+ | `category` | Grouping used by the docs, the capabilities resource and the health score. |
64
66
 
65
67
  ## Exports
66
68
 
@@ -75,7 +77,27 @@ const byCategory = Object.groupBy(allMetrics(), (m) => m.category);
75
77
  **Helpers**
76
78
 
77
79
  - `allMetrics()`, `getMetric(id)`, `isMetricId(value)`
78
- - `metricForBuilder(builder)`, `isResourceMetric(metric)`
80
+ - `metricForBuilder(builder)`, and the entry-kind predicates `isResourceMetric(metric)`,
81
+ `isDerivedMetric(metric)`, `isAggregateMetric(metric)`
82
+ - `validateQuery(query)`, `nativeDimensions(metric)`, `genericDimensions(metric)`,
83
+ `queryTier(metric, query)` — the query-DSL vocabulary checks (ADR 0051 §3)
84
+
85
+ **Result envelopes** (ADR 0051 §2)
86
+
87
+ The Zod mirrors of `format=full | table | summary`. They live here, next to the row schemas they
88
+ wrap, because the collector (`@uptimizr/db`, which re-exports them from `@uptimizr/db/summary`
89
+ under its own names), the generated tool catalog and the MCP server all need them and none of
90
+ those may depend on the others. The summariser that _builds_ an envelope stays in
91
+ `@uptimizr/db/summary`.
92
+
93
+ - `resultFormatSchema` — the `full | table | summary` enum.
94
+ - `tableEnvelopeSchema(row)` — `{ meta, rows }`; `tableMetaSchema` — the `meta` block alone.
95
+ - `summaryEnvelopeSchema` — the four grain-driven digests, discriminated on `kind`.
96
+ - `resultEnvelopeSchema(row, full?)` — the union of the three, for a caller that can express one
97
+ (a route's 200 response schema, a parser). `full` defaults to an array of `row`.
98
+ - `structuredEnvelopeSchema(row)` — the same union merged into **one loose object**, every key
99
+ optional. An MCP `outputSchema` must be an object schema: the SDK drops anything else, a
100
+ top-level union included, and then advertises no schema at all.
79
101
 
80
102
  **Types**
81
103
 
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Zod mirrors of the three result envelopes (ADR 0051 §2, design sketch §B.1).
3
+ *
4
+ * `format=full | table | summary` is the shared querystring of every
5
+ * registry-served aggregate endpoint, and the same three shapes are what an
6
+ * agent gets back through MCP. They are described **here**, next to the row
7
+ * schemas they wrap, because three different packages need them and none of
8
+ * them may depend on the others:
9
+ *
10
+ * - `@uptimizr/db` attaches each route's 200 response schema from the registry,
11
+ * so the collector's Zod type provider serialises a formatted response
12
+ * *through* one of these — without them a `table` or `summary` response would
13
+ * be stripped to nothing on its way out. `@uptimizr/db/summary` re-exports
14
+ * every schema below under its established names, so that package's public
15
+ * API is unchanged.
16
+ * - `@uptimizr/agent-core` derives each generated tool's `outputSchema` from
17
+ * them, and `@uptimizr/mcp` validates `structuredContent` with them. Neither
18
+ * package may depend on `@uptimizr/db` (it carries a ~37 MB DuckDB binding;
19
+ * `dependencies.test.ts` in both packages fails if it reappears) — which is
20
+ * exactly why the *pure* schemas live in this dependency-free package.
21
+ *
22
+ * Only the schemas moved. The summariser that *builds* an envelope
23
+ * (`summarizeRows`, `tableResult`, `clusterCells`, …) is still
24
+ * `@uptimizr/db/summary`'s; relocating it is #337.
25
+ */
26
+ import { z } from "zod";
27
+ /**
28
+ * `format=full | table | summary`, the shared querystring value every
29
+ * registry-served aggregate endpoint accepts. Spelled out literally rather than
30
+ * built from a `RESULT_FORMATS` array so the inferred type is the string union
31
+ * itself and a route handler keeps its narrowing.
32
+ */
33
+ export declare const resultFormatSchema: z.ZodEnum<{
34
+ summary: "summary";
35
+ full: "full";
36
+ table: "table";
37
+ }>;
38
+ /** `format=summary`: one of the four grain-driven shapes. */
39
+ export declare const summaryEnvelopeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
40
+ kind: z.ZodLiteral<"ranked">;
41
+ top: z.ZodArray<z.ZodObject<{
42
+ label: z.ZodString;
43
+ value: z.ZodNullable<z.ZodNumber>;
44
+ share: z.ZodNullable<z.ZodNumber>;
45
+ shareInterval: z.ZodOptional<z.ZodObject<{
46
+ low: z.ZodNumber;
47
+ high: z.ZodNumber;
48
+ }, z.core.$strip>>;
49
+ drill: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
50
+ drillQuery: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
51
+ }, z.core.$strip>>;
52
+ rest: z.ZodObject<{
53
+ rows: z.ZodNumber;
54
+ value: z.ZodNullable<z.ZodNumber>;
55
+ share: z.ZodNullable<z.ZodNumber>;
56
+ }, z.core.$strip>;
57
+ metric: z.ZodEnum<{
58
+ [x: string]: string;
59
+ }>;
60
+ range: z.ZodObject<{
61
+ since: z.ZodNullable<z.ZodNumber>;
62
+ until: z.ZodNullable<z.ZodNumber>;
63
+ }, z.core.$strip>;
64
+ filters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
65
+ sampleSize: z.ZodObject<{
66
+ sessions: z.ZodNullable<z.ZodNumber>;
67
+ events: z.ZodNullable<z.ZodNumber>;
68
+ }, z.core.$strip>;
69
+ total: z.ZodNullable<z.ZodNumber>;
70
+ measure: z.ZodNullable<z.ZodObject<{
71
+ column: z.ZodString;
72
+ unit: z.ZodNullable<z.ZodString>;
73
+ additive: z.ZodBoolean;
74
+ }, z.core.$strip>>;
75
+ confidence: z.ZodOptional<z.ZodObject<{
76
+ kind: z.ZodLiteral<"wilson">;
77
+ level: z.ZodNumber;
78
+ note: z.ZodString;
79
+ }, z.core.$strip>>;
80
+ reading: z.ZodString;
81
+ caveats: z.ZodArray<z.ZodString>;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ kind: z.ZodLiteral<"series">;
84
+ series: z.ZodObject<{
85
+ axis: z.ZodString;
86
+ points: z.ZodNumber;
87
+ first: z.ZodNullable<z.ZodNumber>;
88
+ last: z.ZodNullable<z.ZodNumber>;
89
+ min: z.ZodNullable<z.ZodNumber>;
90
+ max: z.ZodNullable<z.ZodNumber>;
91
+ firstLabel: z.ZodNullable<z.ZodString>;
92
+ lastLabel: z.ZodNullable<z.ZodString>;
93
+ minLabel: z.ZodNullable<z.ZodString>;
94
+ maxLabel: z.ZodNullable<z.ZodString>;
95
+ trend: z.ZodEnum<{
96
+ up: "up";
97
+ down: "down";
98
+ flat: "flat";
99
+ }>;
100
+ slope: z.ZodNullable<z.ZodNumber>;
101
+ }, z.core.$strip>;
102
+ metric: z.ZodEnum<{
103
+ [x: string]: string;
104
+ }>;
105
+ range: z.ZodObject<{
106
+ since: z.ZodNullable<z.ZodNumber>;
107
+ until: z.ZodNullable<z.ZodNumber>;
108
+ }, z.core.$strip>;
109
+ filters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
110
+ sampleSize: z.ZodObject<{
111
+ sessions: z.ZodNullable<z.ZodNumber>;
112
+ events: z.ZodNullable<z.ZodNumber>;
113
+ }, z.core.$strip>;
114
+ total: z.ZodNullable<z.ZodNumber>;
115
+ measure: z.ZodNullable<z.ZodObject<{
116
+ column: z.ZodString;
117
+ unit: z.ZodNullable<z.ZodString>;
118
+ additive: z.ZodBoolean;
119
+ }, z.core.$strip>>;
120
+ confidence: z.ZodOptional<z.ZodObject<{
121
+ kind: z.ZodLiteral<"wilson">;
122
+ level: z.ZodNumber;
123
+ note: z.ZodString;
124
+ }, z.core.$strip>>;
125
+ reading: z.ZodString;
126
+ caveats: z.ZodArray<z.ZodString>;
127
+ }, z.core.$strip>, z.ZodObject<{
128
+ kind: z.ZodLiteral<"clusters">;
129
+ axes: z.ZodArray<z.ZodString>;
130
+ occupiedCells: z.ZodNumber;
131
+ densityThreshold: z.ZodNumber;
132
+ clusters: z.ZodArray<z.ZodObject<{
133
+ centroid: z.ZodArray<z.ZodNumber>;
134
+ extent: z.ZodObject<{
135
+ min: z.ZodArray<z.ZodNumber>;
136
+ max: z.ZodArray<z.ZodNumber>;
137
+ }, z.core.$strip>;
138
+ cells: z.ZodNumber;
139
+ weight: z.ZodNumber;
140
+ share: z.ZodNullable<z.ZodNumber>;
141
+ drill: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
142
+ region: z.ZodOptional<z.ZodNullable<z.ZodString>>;
143
+ regions: z.ZodOptional<z.ZodArray<z.ZodString>>;
144
+ nearestMesh: z.ZodOptional<z.ZodNullable<z.ZodString>>;
145
+ distance: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
146
+ }, z.core.$strip>>;
147
+ rest: z.ZodObject<{
148
+ clusters: z.ZodNumber;
149
+ cells: z.ZodNumber;
150
+ weight: z.ZodNullable<z.ZodNumber>;
151
+ share: z.ZodNullable<z.ZodNumber>;
152
+ }, z.core.$strip>;
153
+ metric: z.ZodEnum<{
154
+ [x: string]: string;
155
+ }>;
156
+ range: z.ZodObject<{
157
+ since: z.ZodNullable<z.ZodNumber>;
158
+ until: z.ZodNullable<z.ZodNumber>;
159
+ }, z.core.$strip>;
160
+ filters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
161
+ sampleSize: z.ZodObject<{
162
+ sessions: z.ZodNullable<z.ZodNumber>;
163
+ events: z.ZodNullable<z.ZodNumber>;
164
+ }, z.core.$strip>;
165
+ total: z.ZodNullable<z.ZodNumber>;
166
+ measure: z.ZodNullable<z.ZodObject<{
167
+ column: z.ZodString;
168
+ unit: z.ZodNullable<z.ZodString>;
169
+ additive: z.ZodBoolean;
170
+ }, z.core.$strip>>;
171
+ confidence: z.ZodOptional<z.ZodObject<{
172
+ kind: z.ZodLiteral<"wilson">;
173
+ level: z.ZodNumber;
174
+ note: z.ZodString;
175
+ }, z.core.$strip>>;
176
+ reading: z.ZodString;
177
+ caveats: z.ZodArray<z.ZodString>;
178
+ }, z.core.$strip>, z.ZodObject<{
179
+ kind: z.ZodLiteral<"record">;
180
+ record: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>;
181
+ rates: z.ZodRecord<z.ZodString, z.ZodObject<{
182
+ value: z.ZodNullable<z.ZodNumber>;
183
+ numerator: z.ZodString;
184
+ denominator: z.ZodString;
185
+ }, z.core.$strip>>;
186
+ metric: z.ZodEnum<{
187
+ [x: string]: string;
188
+ }>;
189
+ range: z.ZodObject<{
190
+ since: z.ZodNullable<z.ZodNumber>;
191
+ until: z.ZodNullable<z.ZodNumber>;
192
+ }, z.core.$strip>;
193
+ filters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
194
+ sampleSize: z.ZodObject<{
195
+ sessions: z.ZodNullable<z.ZodNumber>;
196
+ events: z.ZodNullable<z.ZodNumber>;
197
+ }, z.core.$strip>;
198
+ total: z.ZodNullable<z.ZodNumber>;
199
+ measure: z.ZodNullable<z.ZodObject<{
200
+ column: z.ZodString;
201
+ unit: z.ZodNullable<z.ZodString>;
202
+ additive: z.ZodBoolean;
203
+ }, z.core.$strip>>;
204
+ confidence: z.ZodOptional<z.ZodObject<{
205
+ kind: z.ZodLiteral<"wilson">;
206
+ level: z.ZodNumber;
207
+ note: z.ZodString;
208
+ }, z.core.$strip>>;
209
+ reading: z.ZodString;
210
+ caveats: z.ZodArray<z.ZodString>;
211
+ }, z.core.$strip>], "kind">;
212
+ /** The `meta` block `format=table` wraps a metric's rows in. */
213
+ export declare const tableMetaSchema: z.ZodObject<{
214
+ metric: z.ZodEnum<{
215
+ [x: string]: string;
216
+ }>;
217
+ range: z.ZodObject<{
218
+ since: z.ZodNullable<z.ZodNumber>;
219
+ until: z.ZodNullable<z.ZodNumber>;
220
+ }, z.core.$strip>;
221
+ filters: z.ZodRecord<z.ZodString, z.ZodUnknown>;
222
+ sampleSize: z.ZodObject<{
223
+ sessions: z.ZodNullable<z.ZodNumber>;
224
+ events: z.ZodNullable<z.ZodNumber>;
225
+ }, z.core.$strip>;
226
+ rows: z.ZodNumber;
227
+ truncated: z.ZodBoolean;
228
+ limits: z.ZodObject<{
229
+ maxRows: z.ZodNumber;
230
+ maxSummaryRows: z.ZodNumber;
231
+ }, z.core.$strip>;
232
+ }, z.core.$strip>;
233
+ /** `format=table`: the `meta` envelope around a metric's own row schema. */
234
+ export declare function tableEnvelopeSchema(row: z.ZodType): z.ZodObject;
235
+ /**
236
+ * The result a caller gets from a surface that honours `format`: the union of
237
+ * the three envelopes.
238
+ *
239
+ * Order matters. The `full` shape is first, so a default request is parsed by
240
+ * precisely the schema it was parsed by before this existed and its bytes
241
+ * cannot drift. The envelopes are unambiguous against it — `table` is an object
242
+ * with `meta` and `rows`, `summary` an object with `kind` and `reading`, and
243
+ * neither can satisfy a row array or a stats row's required numeric columns.
244
+ *
245
+ * `full` defaults to a plain array of `row`, which is what an agent-facing
246
+ * caller wants; the collector passes its own route-level 200 schema instead,
247
+ * because a single-record route (the spatial `stats` endpoints) answers with a
248
+ * bare object rather than an array.
249
+ */
250
+ export declare function resultEnvelopeSchema(row: z.ZodType, full?: z.ZodType): z.ZodType;
251
+ /**
252
+ * The **object** form of {@link resultEnvelopeSchema}, for a tool that must
253
+ * advertise its result as a single JSON Schema object — which is what MCP's
254
+ * `outputSchema` is: a `type: "object"` schema the SDK validates
255
+ * `structuredContent` against, on the server with Zod and again on the client
256
+ * with Ajv.
257
+ *
258
+ * A top-level `z.union` cannot be used there. The MCP TypeScript SDK normalises
259
+ * an output schema to an object schema and **silently drops** anything that is
260
+ * not one (`normalizeObjectSchema` returns `undefined` for a union), so
261
+ * `tools/list` would advertise no output schema at all and every call would then
262
+ * fail validation — worse than the bug this replaces. So the three envelopes are
263
+ * merged into one loose object whose every key is optional:
264
+ *
265
+ * - `rows` — `format=full` (wrapped as `{ rows }` by the MCP server, so a
266
+ * single-record read looks like every other) and the rows of `format=table`.
267
+ * Its element schema stays the metric's own, so a column that is out of
268
+ * contract is still reported by name.
269
+ * - `meta` — only `format=table` carries it.
270
+ * - `kind` and the summary fields — only `format=summary` carries them, and
271
+ * `kind` says which grain-specific fields (`top`, `series`, `clusters`,
272
+ * `record`) came with it.
273
+ *
274
+ * The object is deliberately *loose*: an envelope key a newer collector adds is
275
+ * passed through rather than dropped. Callers that can express a union — a
276
+ * collector route's response schema, a test — should use
277
+ * {@link resultEnvelopeSchema}, which discriminates strictly.
278
+ */
279
+ export declare function structuredEnvelopeSchema(row: z.ZodType, metric?: string): z.ZodObject;
280
+ //# sourceMappingURL=envelopes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelopes.d.ts","sourceRoot":"","sources":["../src/envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB;;;;EAAuC,CAAC;AAgKvE,6DAA6D;AAC7D,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAKhC,CAAC;AAEH,gEAAgE;AAChE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;iBAQ1B,CAAC;AAEH,4EAA4E;AAC5E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,SAAS,CAK/D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,GAAE,CAAC,CAAC,OAAsB,GAAG,CAAC,CAAC,OAAO,CAE9F;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,CAuDrF"}