modelrig 0.5.0 → 0.7.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 (159) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +54 -4
  3. package/dist/adapters/anthropic.d.ts.map +1 -1
  4. package/dist/adapters/anthropic.js +36 -7
  5. package/dist/adapters/attempt-meta-map.d.ts +63 -0
  6. package/dist/adapters/attempt-meta-map.d.ts.map +1 -0
  7. package/dist/adapters/attempt-meta-map.js +154 -0
  8. package/dist/adapters/deepseek.d.ts.map +1 -1
  9. package/dist/adapters/deepseek.js +25 -2
  10. package/dist/adapters/gemini.d.ts.map +1 -1
  11. package/dist/adapters/gemini.js +29 -2
  12. package/dist/adapters/grok.d.ts.map +1 -1
  13. package/dist/adapters/grok.js +25 -2
  14. package/dist/adapters/openai-compat.d.ts.map +1 -1
  15. package/dist/adapters/openai-compat.js +25 -2
  16. package/dist/adapters/openai-stream.d.ts +2 -0
  17. package/dist/adapters/openai-stream.d.ts.map +1 -1
  18. package/dist/adapters/openai-stream.js +23 -3
  19. package/dist/adapters/openai.d.ts.map +1 -1
  20. package/dist/adapters/openai.js +36 -6
  21. package/dist/artifacts/artifact.d.ts.map +1 -1
  22. package/dist/artifacts/artifact.js +24 -5
  23. package/dist/artifacts/classification.d.ts +27 -0
  24. package/dist/artifacts/classification.d.ts.map +1 -0
  25. package/dist/artifacts/classification.js +38 -0
  26. package/dist/artifacts/index.d.ts +3 -1
  27. package/dist/artifacts/index.d.ts.map +1 -1
  28. package/dist/artifacts/index.js +10 -1
  29. package/dist/artifacts/run-context.d.ts +25 -3
  30. package/dist/artifacts/run-context.d.ts.map +1 -1
  31. package/dist/artifacts/run-context.js +61 -0
  32. package/dist/artifacts/scrub.d.ts +26 -0
  33. package/dist/artifacts/scrub.d.ts.map +1 -1
  34. package/dist/artifacts/scrub.js +53 -0
  35. package/dist/artifacts/step.d.ts +9 -0
  36. package/dist/artifacts/step.d.ts.map +1 -1
  37. package/dist/artifacts/step.js +9 -0
  38. package/dist/bakeoff/engine.d.ts +46 -4
  39. package/dist/bakeoff/engine.d.ts.map +1 -1
  40. package/dist/bakeoff/engine.js +269 -19
  41. package/dist/bakeoff/eval-cases.d.ts +5 -0
  42. package/dist/bakeoff/eval-cases.d.ts.map +1 -1
  43. package/dist/bakeoff/eval-cases.js +3 -0
  44. package/dist/bakeoff/judge.d.ts +39 -0
  45. package/dist/bakeoff/judge.d.ts.map +1 -0
  46. package/dist/bakeoff/judge.js +102 -0
  47. package/dist/bakeoff/quality.d.ts +44 -0
  48. package/dist/bakeoff/quality.d.ts.map +1 -0
  49. package/dist/bakeoff/quality.js +126 -0
  50. package/dist/bakeoff/reference.d.ts +34 -0
  51. package/dist/bakeoff/reference.d.ts.map +1 -0
  52. package/dist/bakeoff/reference.js +68 -0
  53. package/dist/bakeoff/replay.d.ts +25 -0
  54. package/dist/bakeoff/replay.d.ts.map +1 -1
  55. package/dist/bakeoff/replay.js +48 -1
  56. package/dist/bakeoff/report.d.ts +8 -1
  57. package/dist/bakeoff/report.d.ts.map +1 -1
  58. package/dist/bakeoff/report.js +83 -12
  59. package/dist/bakeoff/samples.d.ts +35 -2
  60. package/dist/bakeoff/samples.d.ts.map +1 -1
  61. package/dist/bakeoff/samples.js +67 -6
  62. package/dist/bakeoff/stats.d.ts +17 -1
  63. package/dist/bakeoff/stats.d.ts.map +1 -1
  64. package/dist/bakeoff/stats.js +67 -0
  65. package/dist/bakeoff/types.d.ts +76 -5
  66. package/dist/bakeoff/types.d.ts.map +1 -1
  67. package/dist/bakeoff/types.js +4 -3
  68. package/dist/cli-query.d.ts +14 -0
  69. package/dist/cli-query.d.ts.map +1 -0
  70. package/dist/cli-query.js +59 -0
  71. package/dist/cli.d.ts +2 -1
  72. package/dist/cli.d.ts.map +1 -1
  73. package/dist/cli.js +26 -23
  74. package/dist/config.d.ts +6 -0
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +14 -0
  77. package/dist/detect/detector.d.ts +82 -0
  78. package/dist/detect/detector.d.ts.map +1 -0
  79. package/dist/detect/detector.js +23 -0
  80. package/dist/index.d.ts +19 -14
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +38 -11
  83. package/dist/query/analytics.d.ts +84 -0
  84. package/dist/query/analytics.d.ts.map +1 -0
  85. package/dist/query/analytics.js +280 -0
  86. package/dist/query/columns.d.ts +59 -0
  87. package/dist/query/columns.d.ts.map +1 -0
  88. package/dist/query/columns.js +89 -0
  89. package/dist/query/grammar.d.ts +74 -7
  90. package/dist/query/grammar.d.ts.map +1 -1
  91. package/dist/query/grammar.js +179 -71
  92. package/dist/query/saved.d.ts +26 -0
  93. package/dist/query/saved.d.ts.map +1 -0
  94. package/dist/query/saved.js +51 -0
  95. package/dist/routes/loader.js +15 -10
  96. package/dist/routes/objective-loader.d.ts +35 -0
  97. package/dist/routes/objective-loader.d.ts.map +1 -0
  98. package/dist/routes/objective-loader.js +286 -0
  99. package/dist/run/accept.d.ts +43 -0
  100. package/dist/run/accept.d.ts.map +1 -0
  101. package/dist/run/accept.js +65 -0
  102. package/dist/run/cost.d.ts +13 -0
  103. package/dist/run/cost.d.ts.map +1 -1
  104. package/dist/run/cost.js +27 -0
  105. package/dist/run/dispatch.d.ts +24 -1
  106. package/dist/run/dispatch.d.ts.map +1 -1
  107. package/dist/run/dispatch.js +19 -0
  108. package/dist/run/ladder-key.d.ts +4 -0
  109. package/dist/run/ladder-key.d.ts.map +1 -1
  110. package/dist/run/ladder-key.js +3 -0
  111. package/dist/run/quality-gate.d.ts +39 -0
  112. package/dist/run/quality-gate.d.ts.map +1 -0
  113. package/dist/run/quality-gate.js +70 -0
  114. package/dist/run/raw.d.ts.map +1 -1
  115. package/dist/run/raw.js +22 -3
  116. package/dist/run/record-attempt.d.ts.map +1 -1
  117. package/dist/run/record-attempt.js +22 -1
  118. package/dist/run/routing-docs.d.ts.map +1 -1
  119. package/dist/run/routing-docs.js +11 -4
  120. package/dist/run/run.d.ts.map +1 -1
  121. package/dist/run/run.js +46 -39
  122. package/dist/settings/index.d.ts +1 -1
  123. package/dist/settings/index.d.ts.map +1 -1
  124. package/dist/settings/index.js +2 -1
  125. package/dist/settings/task-settings.d.ts +10 -0
  126. package/dist/settings/task-settings.d.ts.map +1 -1
  127. package/dist/settings/task-settings.js +17 -0
  128. package/dist/telemetry/attempt-meta.d.ts +178 -0
  129. package/dist/telemetry/attempt-meta.d.ts.map +1 -0
  130. package/dist/telemetry/attempt-meta.js +319 -0
  131. package/dist/telemetry/exporter.d.ts.map +1 -1
  132. package/dist/telemetry/exporter.js +6 -0
  133. package/dist/telemetry/ingest-protocol.d.ts +11 -0
  134. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  135. package/dist/telemetry/ingest-protocol.js +3 -0
  136. package/dist/telemetry/otel-map.d.ts +10 -0
  137. package/dist/telemetry/otel-map.d.ts.map +1 -1
  138. package/dist/telemetry/otel-map.js +64 -0
  139. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  140. package/dist/telemetry/store-migrations.js +15 -0
  141. package/dist/telemetry/store.d.ts.map +1 -1
  142. package/dist/telemetry/store.js +25 -7
  143. package/dist/types/failures-teaching.d.ts.map +1 -1
  144. package/dist/types/failures-teaching.js +20 -0
  145. package/dist/types/failures.d.ts +1 -1
  146. package/dist/types/failures.d.ts.map +1 -1
  147. package/dist/types/failures.js +1 -0
  148. package/dist/types/provider.d.ts +9 -0
  149. package/dist/types/provider.d.ts.map +1 -1
  150. package/dist/types/quality.d.ts +41 -0
  151. package/dist/types/quality.d.ts.map +1 -0
  152. package/dist/types/quality.js +12 -0
  153. package/dist/types/route.d.ts +89 -0
  154. package/dist/types/route.d.ts.map +1 -1
  155. package/dist/types/telemetry.d.ts +13 -0
  156. package/dist/types/telemetry.d.ts.map +1 -1
  157. package/dist/validate-cli.d.ts.map +1 -1
  158. package/dist/validate-cli.js +10 -0
  159. package/package.json +1 -1
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ /**
3
+ * The analytics evaluator — WI-1b (spec §5, plan §3). Turns a parsed
4
+ * {@link QueryAst} into (a) a PostgREST read plan over `inferences`, and (b) a
5
+ * pure JS fold that groups + aggregates the RLS-scoped rows the server reads.
6
+ *
7
+ * ── WHY JS AGGREGATION, NOT SQL ──────────────────────────────────────────
8
+ * The control plane is read through the identity-forwarding PostgREST reader
9
+ * (T12, cache-stats precedent) — there is no raw-SQL executor and never will be
10
+ * (a hand-filter is the XOS smell the reader deletes). So the server reads raw
11
+ * RLS-scoped rows (a bounded scan) and folds them here. The fold's semantics
12
+ * mirror the SQLite compiler's (grammar.ts) column-for-column, so the local and
13
+ * hosted `/query` surfaces answer the same question the same way.
14
+ *
15
+ * ── NEVER CONTENT ────────────────────────────────────────────────────────
16
+ * Every selectable column is an enum/number/boolean/hash/identifier/timestamp
17
+ * (columns.ts). {@link selectableColumnsAreMetaSafe} proves it; the property
18
+ * test pins it. A caller cannot name a column that would carry a prompt or a
19
+ * completion, so the query surface cannot return content by construction.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.ANALYTICS_SCAN_CAP = void 0;
23
+ exports.describeAnalyticsSchema = describeAnalyticsSchema;
24
+ exports.selectableColumnsAreMetaSafe = selectableColumnsAreMetaSafe;
25
+ exports.planAnalyticsRead = planAnalyticsRead;
26
+ exports.evaluateAnalyticsQuery = evaluateAnalyticsQuery;
27
+ const columns_1 = require("./columns");
28
+ const attempt_meta_1 = require("../telemetry/attempt-meta");
29
+ /** The bounded scan the analytics reader issues — the newest rows for the org,
30
+ * folded in JS. A summary surface, not an export; matches the cache-stats
31
+ * ROLLUP ceiling. When a scan fills this cap, the result is flagged `truncated`. */
32
+ exports.ANALYTICS_SCAN_CAP = 10000;
33
+ /** The fixed row projection the analytics reader selects: the plain columns the
34
+ * grammar exposes plus the two jsonb envelopes (`tags`, `meta`) it folds. The
35
+ * whole jsonb objects travel so any reserved-dim / meta.* column resolves in
36
+ * JS without a per-column PostgREST select. */
37
+ const READ_PROJECTION = "route,provider,model,ts,failure_class,served_variant," +
38
+ "cost_estimate_usd,tokens_in,tokens_out,tokens_cached,latency_ms,ttfb_ms,tags,meta";
39
+ function fieldOf(def) {
40
+ return { name: def.name, type: def.classification, classification: def.classification, since: def.since };
41
+ }
42
+ /**
43
+ * The self-describing schema for `/v1/analytics/meta` (spec §5) — every
44
+ * dimension and metric the org can query, DERIVED from the grammar registry
45
+ * (columns.ts), so there is no hand-maintained list to drift. Count(*) is always
46
+ * available and needs no metric column.
47
+ */
48
+ function describeAnalyticsSchema() {
49
+ return {
50
+ dimensions: columns_1.DIMENSION_COLUMNS.map(fieldOf),
51
+ metrics: columns_1.METRIC_COLUMNS.map(fieldOf),
52
+ reserved_tags: [...columns_1.RESERVED_DIMENSION_NAMES],
53
+ };
54
+ }
55
+ /** The classifications that are, by definition, not free text. If a selectable
56
+ * column ever carried any other classification, the never-content guarantee
57
+ * would be broken — so the property test asserts this set covers every column. */
58
+ const META_SAFE = new Set([
59
+ "enum",
60
+ "number",
61
+ "boolean",
62
+ "hash",
63
+ "identifier",
64
+ "timestamp",
65
+ ]);
66
+ /**
67
+ * Prove that EVERY selectable column (dimension + metric) is metadata-safe — an
68
+ * enum/number/boolean/hash/identifier/timestamp, never content (spec §6.1). The
69
+ * `/v1/analytics/query` never-returns-content promise rests on this being total.
70
+ */
71
+ function selectableColumnsAreMetaSafe() {
72
+ return [...columns_1.DIMENSION_COLUMNS, ...columns_1.METRIC_COLUMNS].every((c) => META_SAFE.has(c.classification));
73
+ }
74
+ // ── The PostgREST read plan ──────────────────────────────────────────────────
75
+ /** PostgREST operator for a grammar operator. Only these six exist. */
76
+ const PGREST_OP = {
77
+ "=": "eq",
78
+ "!=": "neq",
79
+ ">": "gt",
80
+ ">=": "gte",
81
+ "<": "lt",
82
+ "<=": "lte",
83
+ };
84
+ /**
85
+ * The bounded read the analytics reader issues: the fixed projection, newest
86
+ * first, capped at {@link ANALYTICS_SCAN_CAP}. A `ts` bound in the query is
87
+ * pushed down to PostgREST (it narrows the scan cheaply and correctly — `ts` is
88
+ * a real timestamptz column); every other predicate is applied in the JS fold,
89
+ * where the grammar's semantics are authoritative regardless of jsonb typing.
90
+ */
91
+ function planAnalyticsRead(ast) {
92
+ const parts = [`select=${READ_PROJECTION}`, "order=ts.desc", `limit=${exports.ANALYTICS_SCAN_CAP}`];
93
+ for (const cond of ast.where) {
94
+ if (cond.column.source === "plain" && cond.column.key === "ts" && typeof cond.value === "string") {
95
+ const op = PGREST_OP[cond.op];
96
+ if (op !== undefined)
97
+ parts.push(`ts=${op}.${encodeURIComponent(cond.value)}`);
98
+ }
99
+ }
100
+ return { query: parts.join("&") };
101
+ }
102
+ // ── The JS fold (grouping + aggregation), mirroring the SQLite semantics ─────
103
+ /** Extract a column's raw value from a PostgREST row. */
104
+ function extract(row, ref) {
105
+ if (ref.source === "plain")
106
+ return row[ref.key];
107
+ const bag = ref.source === "tag" ? row.tags : row.meta;
108
+ if (ref.source === "tag")
109
+ return tagValue(bag, ref.key);
110
+ if (bag === null || bag === undefined || typeof bag !== "object")
111
+ return undefined;
112
+ // A meta key may be a dotted path (`cost.in`); a tag key is a single segment.
113
+ let cur = bag;
114
+ for (const seg of ref.key.split(".")) {
115
+ if (cur === null || cur === undefined || typeof cur !== "object")
116
+ return undefined;
117
+ cur = cur[seg];
118
+ }
119
+ return cur;
120
+ }
121
+ /** A tag value, or null when it is a string that fails the metadata-safe rule
122
+ * (tags are unconstrained at ingest; a free-text tag must not surface through
123
+ * a surface that promises "never content"). */
124
+ function tagValue(bag, key) {
125
+ if (bag === null || bag === undefined || typeof bag !== "object")
126
+ return undefined;
127
+ const v = bag[key];
128
+ if (typeof v === "string" && !(0, attempt_meta_1.isMetaSafeString)(v))
129
+ return null;
130
+ return v;
131
+ }
132
+ /** A finite number, else null (drops null/NaN/non-numeric for aggregation). */
133
+ function toNum(v) {
134
+ if (typeof v === "number")
135
+ return Number.isFinite(v) ? v : null;
136
+ if (typeof v === "string" && v !== "" && Number.isFinite(Number(v)))
137
+ return Number(v);
138
+ return null;
139
+ }
140
+ /** Compare under one operator over already-resolved numeric operands. */
141
+ function cmpNum(l, op, r) {
142
+ switch (op) {
143
+ case "=": return l === r;
144
+ case "!=": return l !== r;
145
+ case ">": return l > r;
146
+ case ">=": return l >= r;
147
+ case "<": return l < r;
148
+ case "<=": return l <= r;
149
+ }
150
+ return false;
151
+ }
152
+ /**
153
+ * Compare a row value against a grammar literal under one operator, matching the
154
+ * SQLite compiler. `timestamp` columns are compared by parsed epoch ms so a
155
+ * PostgREST `+00:00` serialization and a `.000Z` bound agree at the boundary
156
+ * (the JS re-filter must never drop a row the ts pushdown correctly included);
157
+ * otherwise numeric when both sides are numbers, else a string compare (correct
158
+ * for enums/identifiers/hashes).
159
+ */
160
+ function matches(left, op, right, classification) {
161
+ if (classification === "timestamp") {
162
+ const l = Date.parse(String(left));
163
+ const r = typeof right === "number" ? right : Date.parse(String(right));
164
+ if (Number.isFinite(l) && Number.isFinite(r))
165
+ return cmpNum(l, op, r);
166
+ // fall through to string compare when either side is unparseable
167
+ }
168
+ const ln = toNum(left);
169
+ const rn = typeof right === "number" ? right : toNum(right);
170
+ if (ln !== null && rn !== null)
171
+ return cmpNum(ln, op, rn);
172
+ if (op === "=" || op === "!=") {
173
+ const eq = stringOf(left) === String(right);
174
+ return op === "=" ? eq : !eq;
175
+ }
176
+ const ls = stringOf(left);
177
+ const rs = String(right);
178
+ switch (op) {
179
+ case ">": return ls > rs;
180
+ case ">=": return ls >= rs;
181
+ case "<": return ls < rs;
182
+ case "<=": return ls <= rs;
183
+ }
184
+ return false;
185
+ }
186
+ /** A bake-off replay/shadow row — `tags.replay` is set by the replay path. */
187
+ function isReplayRow(row) {
188
+ const tags = row.tags;
189
+ return tags !== null && tags !== undefined && typeof tags === "object" && tags.replay !== undefined;
190
+ }
191
+ /** A row value as a comparable string; undefined/null never equal a literal. */
192
+ function stringOf(v) {
193
+ if (v === null || v === undefined)
194
+ return "<null>";
195
+ if (typeof v === "boolean")
196
+ return v ? "true" : "false";
197
+ return String(v);
198
+ }
199
+ function passesWhere(row, ast) {
200
+ return ast.where.every((cond) => matches(extract(row, cond.column), cond.op, cond.value, cond.column.classification));
201
+ }
202
+ function projectValue(row, ref) {
203
+ const v = extract(row, ref);
204
+ return v === undefined ? null : v;
205
+ }
206
+ function fold(fn, rows, ref) {
207
+ const nums = rows.map((r) => toNum(extract(r, ref))).filter((n) => n !== null);
208
+ if (fn === "sum" || fn === "avg") {
209
+ if (nums.length === 0)
210
+ return null;
211
+ const sum = nums.reduce((a, b) => a + b, 0);
212
+ return fn === "sum" ? sum : sum / nums.length;
213
+ }
214
+ // min/max: numeric when the column is a number, else ordered by string
215
+ // (correct for ISO timestamps and snapshot ids — mirrors SQLite MIN/MAX).
216
+ if (ref.numeric) {
217
+ if (nums.length === 0)
218
+ return null;
219
+ return fn === "min" ? Math.min(...nums) : Math.max(...nums);
220
+ }
221
+ const strs = rows
222
+ .map((r) => extract(r, ref))
223
+ .filter((v) => v !== null && v !== undefined)
224
+ .map((v) => String(v));
225
+ if (strs.length === 0)
226
+ return null;
227
+ return strs.reduce((a, b) => (fn === "min" ? (b < a ? b : a) : b > a ? b : a));
228
+ }
229
+ function projectGroup(items, rows) {
230
+ const out = {};
231
+ const rep = rows[0];
232
+ for (const item of items) {
233
+ if (item.kind === "count")
234
+ out.count = rows.length;
235
+ else if (item.kind === "aggregate")
236
+ out[item.alias] = fold(item.fn, rows, item.metric);
237
+ else
238
+ out[item.column.alias] = rep !== undefined ? projectValue(rep, item.column) : null;
239
+ }
240
+ return out;
241
+ }
242
+ const hasAggregate = (ast) => ast.select.some((s) => s.kind === "count" || s.kind === "aggregate");
243
+ /**
244
+ * Evaluate a parsed query over the rows the analytics reader returned. Filters,
245
+ * groups, aggregates, and projects exactly as the SQLite compiler would, then
246
+ * applies the grammar LIMIT. `truncated` is true when the scan filled the cap
247
+ * (the answer may be partial) OR the grouped result exceeded the limit.
248
+ */
249
+ function evaluateAnalyticsQuery(ast, rows, opts = {}) {
250
+ // Live rows only (odometer rule, same as every console surface): bake-off
251
+ // replay rows run through the production route with `tags.replay` set and
252
+ // must never move a route's numbers — the grammar cannot express IS NULL, so
253
+ // the exclusion is unconditional here and in the SQLite compiler alike.
254
+ const kept = rows.filter((r) => !isReplayRow(r) && passesWhere(r, ast));
255
+ let grouped;
256
+ if (ast.groupBy.length > 0) {
257
+ const groups = new Map();
258
+ for (const row of kept) {
259
+ // Join on U+0001 so ("ab","") and ("a","b") never collide into one group —
260
+ // a control byte cannot appear in a metadata-safe (enum/id/hash) value.
261
+ const key = ast.groupBy.map((g) => stringOf(extract(row, g))).join("\u0001");
262
+ const bucket = groups.get(key);
263
+ if (bucket === undefined)
264
+ groups.set(key, [row]);
265
+ else
266
+ bucket.push(row);
267
+ }
268
+ grouped = [...groups.values()].map((g) => projectGroup(ast.select, g));
269
+ }
270
+ else if (hasAggregate(ast)) {
271
+ // Aggregates with no GROUP BY collapse to a single row (SQL semantics).
272
+ grouped = [projectGroup(ast.select, kept)];
273
+ }
274
+ else {
275
+ // A plain projection — one output row per input row.
276
+ grouped = kept.map((r) => projectGroup(ast.select, [r]));
277
+ }
278
+ const truncated = (opts.scannedAtCap ?? false) || grouped.length > ast.limit;
279
+ return { rows: grouped.slice(0, ast.limit), truncated, limit: ast.limit };
280
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The analytics column/metric registry — WI-1b (spec §5, plan §3).
3
+ *
4
+ * ── ONE SOURCE OF TRUTH ──────────────────────────────────────────────────
5
+ * Every dimension and metric the telemetry grammar exposes is declared HERE,
6
+ * with its storage source, classification, and "since" version. The SQLite
7
+ * grammar renderer (grammar.ts), the PostgREST analytics planner
8
+ * (analytics.ts), and the self-describing `/v1/analytics/meta` endpoint all
9
+ * read this table — so the grammar, the query surface, and its documentation
10
+ * can never drift (spec §5: "self-description derived from the grammar").
11
+ *
12
+ * ── THE METADATA-SAFE INVARIANT ──────────────────────────────────────────
13
+ * NO column here is free text. Every `classification` is one of enum | number
14
+ * | boolean | hash | identifier | timestamp — the classification rule from
15
+ * spec §2. A reserved dimension (`subject`, …) is an OPAQUE identifier: a
16
+ * customer-side id, hashed by the caller if it looks like PII. This is what
17
+ * lets `/v1/analytics/query` promise "never returns content" — there is no
18
+ * selectable column that could carry a prompt or a completion.
19
+ */
20
+ /** The metadata-safe classifications (spec §2). A column may only be one of
21
+ * these — there is deliberately no "text"/"string-freeform" bucket, which is
22
+ * the invariant the `/query` never-content guarantee rests on. */
23
+ export type Classification = "enum" | "number" | "boolean" | "hash" | "identifier" | "timestamp";
24
+ /** Where a column's value lives on the row. `plain` = a first-class column;
25
+ * `tag` = a key in the `tags` jsonb (SQLite: `tags_json`); `meta` = a path in
26
+ * the `meta` jsonb (SQLite: `meta_json`) — the WI-1a attempt envelope. */
27
+ export type ColumnSource = "plain" | "tag" | "meta";
28
+ export interface ColumnDef {
29
+ /** The grammar identifier a caller types (e.g. `route`, `subject`,
30
+ * `meta.finish_reason`, `meta.cost.in`). */
31
+ readonly name: string;
32
+ readonly source: ColumnSource;
33
+ /** The leaf key or path within the source. For `meta.cost.in` this is
34
+ * `cost.in`; for `subject` (tag) it is `subject`; for `route` it is `route`. */
35
+ readonly key: string;
36
+ readonly classification: Classification;
37
+ /** Whether the column is a NUMBER usable as an aggregate metric
38
+ * (`sum(...)`/`avg(...)`/`min(...)`/`max(...)`). */
39
+ readonly numeric: boolean;
40
+ /** The product version the column became queryable in. */
41
+ readonly since: string;
42
+ }
43
+ /** Plain numeric metrics (Phase 4 WS5) — the original aggregate targets. These
44
+ * are first-class columns on `inferences`; the grammar names them without a
45
+ * `meta.` prefix. `key` is the storage column (SQLite + Postgres share it). */
46
+ export declare const PLAIN_METRICS: readonly ColumnDef[];
47
+ /** Every column the grammar can GROUP BY / FILTER by (dimensions). Order is
48
+ * stable so `/meta` renders deterministically. */
49
+ export declare const DIMENSION_COLUMNS: readonly ColumnDef[];
50
+ /** Every column usable as an aggregate metric (`numeric`), keyed by grammar
51
+ * name — plain metrics plus the numeric envelope fields. */
52
+ export declare const METRIC_COLUMNS: readonly ColumnDef[];
53
+ /** Grammar name → dimension def (group/filter). */
54
+ export declare const DIMENSIONS_BY_NAME: ReadonlyMap<string, ColumnDef>;
55
+ /** Grammar name → metric def (aggregate target). */
56
+ export declare const METRICS_BY_NAME: ReadonlyMap<string, ColumnDef>;
57
+ /** The reserved tag keys, in declaration order (spec §4). */
58
+ export declare const RESERVED_DIMENSION_NAMES: readonly string[];
59
+ //# sourceMappingURL=columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../../src/query/columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;kEAEkE;AAClE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,WAAW,CAAC;AAEjG;;0EAE0E;AAC1E,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB;gDAC4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;oFACgF;IAChF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC;wDACoD;IACpD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AA8CD;;+EAE+E;AAC/E,eAAO,MAAM,aAAa,EAAE,SAAS,SAAS,EAO7C,CAAC;AAEF;kDACkD;AAClD,eAAO,MAAM,iBAAiB,EAAE,SAAS,SAAS,EAAkD,CAAC;AAErG;4DAC4D;AAC5D,eAAO,MAAM,cAAc,EAAE,SAAS,SAAS,EAG9C,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAE7D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAE1D,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAqC,CAAC"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * The analytics column/metric registry — WI-1b (spec §5, plan §3).
4
+ *
5
+ * ── ONE SOURCE OF TRUTH ──────────────────────────────────────────────────
6
+ * Every dimension and metric the telemetry grammar exposes is declared HERE,
7
+ * with its storage source, classification, and "since" version. The SQLite
8
+ * grammar renderer (grammar.ts), the PostgREST analytics planner
9
+ * (analytics.ts), and the self-describing `/v1/analytics/meta` endpoint all
10
+ * read this table — so the grammar, the query surface, and its documentation
11
+ * can never drift (spec §5: "self-description derived from the grammar").
12
+ *
13
+ * ── THE METADATA-SAFE INVARIANT ──────────────────────────────────────────
14
+ * NO column here is free text. Every `classification` is one of enum | number
15
+ * | boolean | hash | identifier | timestamp — the classification rule from
16
+ * spec §2. A reserved dimension (`subject`, …) is an OPAQUE identifier: a
17
+ * customer-side id, hashed by the caller if it looks like PII. This is what
18
+ * lets `/v1/analytics/query` promise "never returns content" — there is no
19
+ * selectable column that could carry a prompt or a completion.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.RESERVED_DIMENSION_NAMES = exports.METRICS_BY_NAME = exports.DIMENSIONS_BY_NAME = exports.METRIC_COLUMNS = exports.DIMENSION_COLUMNS = exports.PLAIN_METRICS = void 0;
23
+ // ── Plain columns (Phase 4 WS5 — the original grammar) ───────────────────────
24
+ const PLAIN = [
25
+ { name: "route", source: "plain", key: "route", classification: "identifier", numeric: false, since: "0.1.0" },
26
+ { name: "provider", source: "plain", key: "provider", classification: "enum", numeric: false, since: "0.1.0" },
27
+ { name: "model", source: "plain", key: "model", classification: "identifier", numeric: false, since: "0.1.0" },
28
+ { name: "ts", source: "plain", key: "ts", classification: "timestamp", numeric: false, since: "0.1.0" },
29
+ { name: "failure_class", source: "plain", key: "failure_class", classification: "enum", numeric: false, since: "0.1.0" },
30
+ { name: "served_variant", source: "plain", key: "served_variant", classification: "identifier", numeric: false, since: "0.1.0" },
31
+ ];
32
+ // ── Reserved dimensions (spec §4) — first-class group/filter columns that
33
+ // remain `tags` keys in storage (GIN-indexed; no DDL). An opaque customer-side
34
+ // id ⇒ identifier classification. ──────────────────────────────────────────
35
+ const RESERVED_DIMS = [
36
+ { name: "subject", source: "tag", key: "subject", classification: "identifier", numeric: false, since: "0.6.0" },
37
+ { name: "feature", source: "tag", key: "feature", classification: "identifier", numeric: false, since: "0.6.0" },
38
+ { name: "user", source: "tag", key: "user", classification: "identifier", numeric: false, since: "0.6.0" },
39
+ { name: "session", source: "tag", key: "session", classification: "identifier", numeric: false, since: "0.6.0" },
40
+ { name: "cost_center", source: "tag", key: "cost_center", classification: "identifier", numeric: false, since: "0.6.0" },
41
+ ];
42
+ // ── Attempt-envelope scalars (spec §2) — `meta.<field>` columns. Every one is
43
+ // an enum / number / boolean / hash / identifier — never content. ────────────
44
+ const META_SCALARS = [
45
+ { name: "meta.finish_reason", source: "meta", key: "finish_reason", classification: "enum", numeric: false, since: "0.6.0" },
46
+ { name: "meta.refusal", source: "meta", key: "refusal", classification: "boolean", numeric: false, since: "0.6.0" },
47
+ { name: "meta.streaming", source: "meta", key: "streaming", classification: "boolean", numeric: false, since: "0.6.0" },
48
+ { name: "meta.model_snapshot", source: "meta", key: "model_snapshot", classification: "identifier", numeric: false, since: "0.6.0" },
49
+ { name: "meta.context_bucket", source: "meta", key: "context_bucket", classification: "enum", numeric: false, since: "0.6.0" },
50
+ { name: "meta.error_code", source: "meta", key: "error_code", classification: "enum", numeric: false, since: "0.6.0" },
51
+ { name: "meta.region", source: "meta", key: "region", classification: "identifier", numeric: false, since: "0.6.0" },
52
+ { name: "meta.input_hash", source: "meta", key: "input_hash", classification: "hash", numeric: false, since: "0.6.0" },
53
+ { name: "meta.output_hash", source: "meta", key: "output_hash", classification: "hash", numeric: false, since: "0.6.0" },
54
+ // Numeric envelope fields — group/filter dimensions AND aggregate metrics.
55
+ { name: "meta.http_status", source: "meta", key: "http_status", classification: "number", numeric: true, since: "0.6.0" },
56
+ { name: "meta.reasoning_tokens", source: "meta", key: "reasoning_tokens", classification: "number", numeric: true, since: "0.6.0" },
57
+ { name: "meta.response_chars", source: "meta", key: "response_chars", classification: "number", numeric: true, since: "0.6.0" },
58
+ { name: "meta.cost.in", source: "meta", key: "cost.in", classification: "number", numeric: true, since: "0.6.0" },
59
+ { name: "meta.cost.out", source: "meta", key: "cost.out", classification: "number", numeric: true, since: "0.6.0" },
60
+ { name: "meta.cost.cache_read", source: "meta", key: "cost.cache_read", classification: "number", numeric: true, since: "0.6.0" },
61
+ { name: "meta.cost.cache_write", source: "meta", key: "cost.cache_write", classification: "number", numeric: true, since: "0.6.0" },
62
+ { name: "meta.cost.other", source: "meta", key: "cost.other", classification: "number", numeric: true, since: "0.6.0" },
63
+ ];
64
+ /** Plain numeric metrics (Phase 4 WS5) — the original aggregate targets. These
65
+ * are first-class columns on `inferences`; the grammar names them without a
66
+ * `meta.` prefix. `key` is the storage column (SQLite + Postgres share it). */
67
+ exports.PLAIN_METRICS = [
68
+ { name: "cost", source: "plain", key: "cost_estimate_usd", classification: "number", numeric: true, since: "0.1.0" },
69
+ { name: "tokens_in", source: "plain", key: "tokens_in", classification: "number", numeric: true, since: "0.1.0" },
70
+ { name: "tokens_out", source: "plain", key: "tokens_out", classification: "number", numeric: true, since: "0.1.0" },
71
+ { name: "tokens_cached", source: "plain", key: "tokens_cached", classification: "number", numeric: true, since: "0.1.0" },
72
+ { name: "latency_ms", source: "plain", key: "latency_ms", classification: "number", numeric: true, since: "0.1.0" },
73
+ { name: "ttfb_ms", source: "plain", key: "ttfb_ms", classification: "number", numeric: true, since: "0.1.0" },
74
+ ];
75
+ /** Every column the grammar can GROUP BY / FILTER by (dimensions). Order is
76
+ * stable so `/meta` renders deterministically. */
77
+ exports.DIMENSION_COLUMNS = [...PLAIN, ...RESERVED_DIMS, ...META_SCALARS];
78
+ /** Every column usable as an aggregate metric (`numeric`), keyed by grammar
79
+ * name — plain metrics plus the numeric envelope fields. */
80
+ exports.METRIC_COLUMNS = [
81
+ ...exports.PLAIN_METRICS,
82
+ ...META_SCALARS.filter((c) => c.numeric),
83
+ ];
84
+ /** Grammar name → dimension def (group/filter). */
85
+ exports.DIMENSIONS_BY_NAME = new Map(exports.DIMENSION_COLUMNS.map((c) => [c.name, c]));
86
+ /** Grammar name → metric def (aggregate target). */
87
+ exports.METRICS_BY_NAME = new Map(exports.METRIC_COLUMNS.map((c) => [c.name, c]));
88
+ /** The reserved tag keys, in declaration order (spec §4). */
89
+ exports.RESERVED_DIMENSION_NAMES = RESERVED_DIMS.map((c) => c.name);
@@ -1,21 +1,42 @@
1
1
  /**
2
- * The telemetry query grammar — Phase 4 WS5 ("your exhaust is your data").
2
+ * The telemetry query grammar — Phase 4 WS5 ("your exhaust is your data"),
3
+ * extended for WI-1b (the attempt-envelope analytics surface).
4
+ *
3
5
  * A hand-rolled recursive-descent parser over a DELIBERATELY tiny SQL-ish
4
6
  * language:
5
7
  *
6
8
  * SELECT item (, item)* FROM inferences
7
9
  * [WHERE cond (AND cond)*] [GROUP BY col (, col)*] [LIMIT n]
8
10
  * item := col | count(*) | sum(m)|avg(m)|min(m)|max(m)
9
- * col := route|provider|model|ts|failure_class|served_variant|tag.<name>
11
+ * col := route|provider|model|ts|failure_class|served_variant
12
+ * | subject|feature|user|session|cost_center (reserved dims)
13
+ * | meta.<field> (attempt envelope)
14
+ * | tag.<name>
10
15
  * m := cost|tokens_in|tokens_out|tokens_cached|latency_ms|ttfb_ms
16
+ * | meta.http_status|meta.reasoning_tokens|meta.response_chars
17
+ * | meta.cost.in|meta.cost.out|meta.cost.cache_read|meta.cost.cache_write|meta.cost.other
11
18
  * cond := col op literal op := = != > < >= <=
12
19
  *
13
- * Security model = the allowlist: unknown tokens throw, string literals
14
- * only ever travel as bind parameters, the output is a single read-only
15
- * SELECT. There is no raw-SQL path — by construction, not by filtering.
16
- * Row scoping: a token's scope is ANDed into WHERE as one more bind
17
- * parameter; the caller cannot opt out of it.
20
+ * ── TWO EXECUTION TARGETS, ONE PARSER (S1: extend, don't twin) ────────────
21
+ * `parseQuery` produces a {@link QueryAst}. `compileQuery` renders that AST to a
22
+ * single read-only SQLite SELECT (the local `/query` surface, `runTelemetryQuery`).
23
+ * The server analytics surface (`/v1/analytics/query`) evaluates the SAME AST
24
+ * over PostgREST-read rows in JS (query/analytics.ts) — so the grammar, its
25
+ * SQLite semantics, and its multi-tenant semantics can never disagree.
26
+ *
27
+ * Security model = the allowlist (columns.ts): unknown tokens throw, string
28
+ * literals only ever travel as bind parameters, the SQLite output is a single
29
+ * read-only SELECT. There is no raw-SQL path — by construction, not by filtering.
30
+ * The metadata-safe invariant is structural: every selectable column is an
31
+ * enum/number/boolean/hash/identifier/timestamp (columns.ts), never content.
18
32
  */
33
+ import type { Classification, ColumnSource } from "./columns";
34
+ export { describeAnalyticsSchema, evaluateAnalyticsQuery, planAnalyticsRead, ANALYTICS_SCAN_CAP, selectableColumnsAreMetaSafe, } from "./analytics";
35
+ export type { AnalyticsSchema, AnalyticsSchemaField, AnalyticsResult, AnalyticsReadPlan, AnalyticsRow } from "./analytics";
36
+ export { SAVED_QUERIES, savedQueryById, sinceIsoFor } from "./saved";
37
+ export type { SavedQuery } from "./saved";
38
+ export { DIMENSION_COLUMNS, METRIC_COLUMNS, RESERVED_DIMENSION_NAMES } from "./columns";
39
+ export type { ColumnDef, Classification, ColumnSource } from "./columns";
19
40
  export declare class QueryGrammarError extends Error {
20
41
  constructor(message: string);
21
42
  }
@@ -31,5 +52,51 @@ export interface CompiledQuery {
31
52
  readonly sql: string;
32
53
  readonly params: ReadonlyArray<string | number>;
33
54
  }
55
+ export type AggFn = "sum" | "avg" | "min" | "max";
56
+ /** A resolved reference to one column, carrying its storage + classification so
57
+ * the JS evaluator (analytics.ts) needs no second lookup. */
58
+ export interface AstColumnRef {
59
+ readonly name: string;
60
+ readonly source: ColumnSource;
61
+ readonly key: string;
62
+ readonly classification: Classification;
63
+ readonly numeric: boolean;
64
+ readonly alias: string;
65
+ }
66
+ export type AstSelectItem = {
67
+ readonly kind: "column";
68
+ readonly column: AstColumnRef;
69
+ } | {
70
+ readonly kind: "count";
71
+ readonly alias: "count";
72
+ } | {
73
+ readonly kind: "aggregate";
74
+ readonly fn: AggFn;
75
+ readonly metric: AstColumnRef;
76
+ readonly alias: string;
77
+ };
78
+ export interface AstCondition {
79
+ readonly column: AstColumnRef;
80
+ readonly op: string;
81
+ readonly value: string | number;
82
+ }
83
+ export interface QueryAst {
84
+ readonly select: readonly AstSelectItem[];
85
+ readonly where: readonly AstCondition[];
86
+ readonly groupBy: readonly AstColumnRef[];
87
+ readonly limit: number;
88
+ }
89
+ /**
90
+ * Parse a query string into a {@link QueryAst}. Pure and total: every path
91
+ * either returns a validated AST or throws {@link QueryGrammarError}. This is
92
+ * the single parser — both the SQLite compiler (below) and the PostgREST
93
+ * evaluator (analytics.ts) consume its output.
94
+ */
95
+ export declare function parseQuery(input: string): QueryAst;
96
+ /**
97
+ * Compile a query string to a single read-only SQLite SELECT (the local
98
+ * telemetry store). Row scoping is ANDed into WHERE unconditionally — the caller
99
+ * cannot opt out of it.
100
+ */
34
101
  export declare function compileQuery(input: string, scope: QueryScope): CompiledQuery;
35
102
  //# sourceMappingURL=grammar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/query/grammar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,4CAA4C;AAC5C,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;CACjD;AAwHD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,aAAa,CAkJ5E"}
1
+ {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/query/grammar.ts"],"names":[],"mappings":"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"}