@phosphorco/bb-thread-metrics 0.0.0-stage → 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/dist/index.js ADDED
@@ -0,0 +1,529 @@
1
+ // src/recent.ts
2
+ import { z } from "zod";
3
+ var RECENT_LIMITS = Object.freeze({ rows: 1000, bytes: 4 * 1024 * 1024, rowBytes: 8 * 1024, inputBytes: 1024, providers: 32, windowMs: 3 * 24 * 60 * 60 * 1000 });
4
+ var id = z.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/);
5
+ var revision = z.string().min(1).max(256);
6
+ var time = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
7
+ var coverage = z.enum(["complete", "partial", "unknown"]);
8
+ var rank = z.number().finite().min(0).max(1).nullable();
9
+ var count = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
10
+ var windowShape = (value) => z.object({ m20: value, h24: value, d7: value, lifetime: value }).strip();
11
+ var reason = z.string().min(1).max(64);
12
+ var reasons = z.array(reason).max(16);
13
+ var recentDollarSchema = z.discriminatedUnion("kind", [
14
+ z.object({ kind: z.literal("estimate"), amount: z.number().finite().nonnegative() }).strip(),
15
+ z.object({ kind: z.literal("floor"), amount: z.number().finite().nonnegative() }).strip(),
16
+ z.object({ kind: z.literal("unavailable"), amount: z.null() }).strip()
17
+ ]);
18
+ var recentWindowsSchema = windowShape(recentDollarSchema);
19
+ var recentApproximationSchema = z.object({ approximate: z.boolean(), reasons: z.array(z.enum(["closest-model", "provider-default", "model-attribution", "bucket-boundary", "missing-price", "source-gap"])).max(6) }).strip().superRefine((v, c) => {
20
+ if (!v.approximate && v.reasons.length)
21
+ c.addIssue({ code: "custom", message: "Approximation reasons require approximate flag" });
22
+ });
23
+ var provider = z.object({ providerId: id, windows: recentWindowsSchema, approximation: recentApproximationSchema }).strip();
24
+ var recentScopeSchema = z.object({
25
+ memberCount: count,
26
+ coverage,
27
+ coverageReasons: reasons,
28
+ priceVersion: revision,
29
+ approximation: recentApproximationSchema,
30
+ windows: recentWindowsSchema,
31
+ scores: recentWindowsSchema,
32
+ providers: z.array(provider).max(RECENT_LIMITS.providers),
33
+ omittedProviders: count,
34
+ dollarRanks: z.object({ m20: rank, h24: rank, d7: rank }).strip(),
35
+ scoreRanks: z.object({ m20: rank, h24: rank, d7: rank }).strip()
36
+ }).strip().superRefine((v, c) => {
37
+ if (new Set(v.providers.map((x) => x.providerId)).size !== v.providers.length)
38
+ c.addIssue({ code: "custom", message: "Duplicate providers" });
39
+ if (v.omittedProviders && v.coverage === "complete")
40
+ c.addIssue({ code: "custom", message: "Omitted providers require partial coverage" });
41
+ });
42
+ var recentRowSchema = z.object({ threadId: id, revision, configRevision: revision, asOf: time, ownLastFoldAt: time.nullable(), familyLastFoldAt: time.nullable(), own: recentScopeSchema, family: recentScopeSchema }).strip();
43
+ var recentSnapshotInputSchema = z.object({ ifRevision: revision.optional() }).strict();
44
+ var thresholds = windowShape(z.number().finite().nonnegative());
45
+ var weights = z.record(id, z.number().finite().nonnegative()).refine((v) => Object.keys(v).length <= RECENT_LIMITS.providers, "Too many provider weights");
46
+ var configBase = { schemaVersion: z.literal(2), providerWeights: weights };
47
+ var scoringConfigV2Schema = z.discriminatedUnion("mode", [
48
+ z.object({ ...configBase, mode: z.literal("percentile"), gates: z.object({ m20: z.number().finite().min(0).max(1), h24: z.number().finite().min(0).max(1), d7: z.number().finite().min(0).max(1) }).strip() }).strip(),
49
+ z.object({ ...configBase, mode: z.literal("absolute"), thresholds }).strip()
50
+ ]);
51
+ var DEFAULT_SCORING_CONFIG_V2 = Object.freeze({ schemaVersion: 2, mode: "percentile", providerWeights: Object.freeze({}), gates: Object.freeze({ m20: 0.9, h24: 0.9, d7: 0.9 }) });
52
+ function weightedScoreV2(values, providerWeights) {
53
+ if (values.length > RECENT_LIMITS.providers)
54
+ throw new Error("Too many score providers");
55
+ weights.parse(providerWeights);
56
+ let sum = 0, unknown = false;
57
+ const seen = new Set;
58
+ for (const v of values) {
59
+ id.parse(v.providerId);
60
+ if (seen.has(v.providerId))
61
+ throw new Error("Duplicate score provider");
62
+ seen.add(v.providerId);
63
+ if (v.amount === null) {
64
+ unknown = true;
65
+ continue;
66
+ }
67
+ if (!Number.isFinite(v.amount) || v.amount < 0)
68
+ throw new Error("Invalid provider dollars");
69
+ const w = Object.hasOwn(providerWeights, v.providerId) ? providerWeights[v.providerId] : 1;
70
+ sum += w * v.amount;
71
+ if (!Number.isFinite(sum))
72
+ throw new Error("Weighted dollar overflow");
73
+ }
74
+ return unknown ? null : sum;
75
+ }
76
+ var population = z.object({ count, coverage }).strip();
77
+ var populations = z.object({ m20: population, h24: population, d7: population }).strip();
78
+ var scopedPopulations = z.object({ dollars: populations, scores: populations }).strip();
79
+ var recentSnapshotFullSchema = z.object({
80
+ version: z.literal(1),
81
+ revision,
82
+ unchanged: z.literal(false),
83
+ currency: z.literal("USD"),
84
+ estimated: z.literal(true),
85
+ cutoffLastFoldAt: time.nullable(),
86
+ asOf: time,
87
+ rankAsOf: time,
88
+ recentWindowMs: z.literal(RECENT_LIMITS.windowMs),
89
+ config: z.object({ revision, value: scoringConfigV2Schema }).strip(),
90
+ coverage,
91
+ coverageReasons: reasons,
92
+ materializationPending: count,
93
+ omittedRows: z.object({ count, exact: z.boolean() }).strip(),
94
+ population: z.object({ own: scopedPopulations, family: scopedPopulations }).strip(),
95
+ rows: z.array(recentRowSchema).max(RECENT_LIMITS.rows)
96
+ }).strip();
97
+ var recentSnapshotResultSchema = z.union([z.object({ version: z.literal(1), revision, unchanged: z.literal(true) }).strip(), recentSnapshotFullSchema]);
98
+ function validateRecentSnapshot(input, value) {
99
+ recentSnapshotInputSchema.parse(input);
100
+ const bytes = (v) => new TextEncoder().encode(JSON.stringify(v)).byteLength;
101
+ if (bytes(value) > RECENT_LIMITS.bytes)
102
+ throw new Error("Snapshot byte limit");
103
+ const raw = value;
104
+ if (raw?.rows !== undefined) {
105
+ if (!Array.isArray(raw.rows) || raw.rows.length > RECENT_LIMITS.rows)
106
+ throw new Error("Snapshot row limit");
107
+ for (const row of raw.rows)
108
+ if (bytes(row) > RECENT_LIMITS.rowBytes)
109
+ throw new Error("Snapshot row byte limit");
110
+ }
111
+ if (value && typeof value === "object" && value.unchanged === true) {
112
+ for (const field of Object.keys(recentSnapshotFullSchema.shape))
113
+ if (!["version", "revision", "unchanged"].includes(field) && Object.hasOwn(value, field))
114
+ throw new Error("Unchanged snapshot carries recognized full content");
115
+ }
116
+ const out = recentSnapshotResultSchema.parse(value);
117
+ if (out.unchanged) {
118
+ if (input.ifRevision !== out.revision)
119
+ throw new Error("Unmatched snapshot revision");
120
+ return out;
121
+ }
122
+ if (out.rankAsOf > out.asOf)
123
+ throw new Error("Future rank time");
124
+ if ((out.materializationPending || out.omittedRows.count || !out.omittedRows.exact) && out.coverage === "complete")
125
+ throw new Error("Pending/capped snapshot must disclose coverage");
126
+ let previousActivity = Infinity, previousId = "";
127
+ const ids = new Set;
128
+ for (const row of out.rows) {
129
+ if (ids.has(row.threadId))
130
+ throw new Error("Duplicate snapshot thread");
131
+ ids.add(row.threadId);
132
+ if (row.asOf > out.asOf)
133
+ throw new Error("Future row time");
134
+ if (row.configRevision !== out.config.revision)
135
+ throw new Error("Row score configuration mismatch");
136
+ if (row.ownLastFoldAt === null && row.familyLastFoldAt === null)
137
+ throw new Error("Missing fold activity timestamp");
138
+ const activity = Math.max(row.ownLastFoldAt ?? 0, row.familyLastFoldAt ?? 0);
139
+ if (activity > out.asOf || activity < Math.max(0, out.asOf - out.recentWindowMs))
140
+ throw new Error("Row outside recent activity window");
141
+ if (activity > previousActivity || activity === previousActivity && row.threadId < previousId)
142
+ throw new Error("Rows must be newest first with ID tie-break");
143
+ previousActivity = activity;
144
+ previousId = row.threadId;
145
+ for (const scope of ["own", "family"]) {
146
+ validateProjection(row[scope], out.config.value.providerWeights);
147
+ validateRanks(row[scope], out.population[scope], out.rankAsOf >= row.asOf);
148
+ }
149
+ }
150
+ if (out.omittedRows.count && out.rows.length && out.cutoffLastFoldAt !== previousActivity)
151
+ throw new Error("Missing eviction cutoff");
152
+ return out;
153
+ }
154
+ function validateProjection(projection, providerWeights) {
155
+ if (projection.coverageReasons.includes("unpriced") && !projection.approximation.reasons.includes("missing-price"))
156
+ throw new Error("Unpriced coverage requires missing-price provenance");
157
+ const observationGap = projection.coverage !== "complete" && !projection.coverageReasons.includes("unpriced") && projection.approximation.reasons.includes("source-gap") && projection.coverageReasons.some((r) => ["source-gap", "root-unattested", "history-unavailable", "missing-thread"].includes(r));
158
+ const unavailableProvenance = (approximation) => approximation.reasons.includes("missing-price") || observationGap && approximation.reasons.includes("source-gap");
159
+ for (const p of projection.providers) {
160
+ if (p.approximation.approximate && (!projection.approximation.approximate || p.approximation.reasons.some((reason2) => !projection.approximation.reasons.includes(reason2))))
161
+ throw new Error("Aggregate must retain provider approximation provenance");
162
+ if (Object.values(p.windows).some((v) => v.kind === "unavailable") && !unavailableProvenance(p.approximation))
163
+ throw new Error("Unavailable provider requires missing-price provenance");
164
+ }
165
+ if ([...Object.values(projection.windows), ...Object.values(projection.scores)].some((v) => v.kind === "unavailable") && !unavailableProvenance(projection.approximation))
166
+ throw new Error("Unavailable total requires missing-price provenance");
167
+ for (const window of ["m20", "h24", "d7", "lifetime"]) {
168
+ const known = projection.providers.filter((p) => p.windows[window].kind !== "unavailable");
169
+ const parts = known.map((p) => ({ providerId: p.providerId, amount: p.windows[window].amount }));
170
+ const dollars = parts.reduce((sum, p) => sum + p.amount, 0), score = weightedScoreV2(parts, providerWeights);
171
+ if (!Number.isFinite(dollars))
172
+ throw new Error("Provider dollar overflow");
173
+ const incomplete = projection.omittedProviders > 0 || known.length !== projection.providers.length;
174
+ const floor = incomplete || known.some((p) => p.windows[window].kind === "floor");
175
+ const equal = (a, b) => Math.abs(a - b) <= 8 * Number.EPSILON * Math.max(a, b);
176
+ for (const [total, subtotal, label] of [[projection.windows[window], dollars, "dollar"], [projection.scores[window], score, "score"]]) {
177
+ if (total.kind === "unavailable")
178
+ continue;
179
+ if (floor && total.kind === "estimate")
180
+ throw new Error("Partial/provider floor cannot become an exact estimate");
181
+ if (incomplete) {
182
+ if (total.amount < subtotal && !equal(total.amount, subtotal))
183
+ throw new Error(`Known provider ${label} subtotal exceeds total`);
184
+ } else if (!equal(total.amount, subtotal))
185
+ throw new Error(`Provider ${label} total mismatch`);
186
+ }
187
+ }
188
+ }
189
+ function validateRanks(projection, population2, contemporaneous) {
190
+ for (const [values, ranks, pops] of [[projection.windows, projection.dollarRanks, population2.dollars], [projection.scores, projection.scoreRanks, population2.scores]]) {
191
+ for (const w of ["m20", "h24", "d7"]) {
192
+ const r = ranks[w], n = pops[w].count;
193
+ if (r !== null && (n === 0 || n === 1 && r !== 0))
194
+ throw new Error("Rank contradicts empty/singleton population");
195
+ if (contemporaneous && r !== null && (values[w].amount === null || values[w].amount <= 0))
196
+ throw new Error("Rank requires supported positive contemporaneous amount");
197
+ }
198
+ }
199
+ }
200
+ function recentPercentileHeat(projection, gates = { m20: 0.9, h24: 0.9, d7: 0.9 }) {
201
+ for (const w of ["m20", "h24", "d7"])
202
+ if (!Number.isFinite(gates[w]) || gates[w] < 0 || gates[w] > 1)
203
+ throw new Error("Invalid gate");
204
+ for (const [w, label] of [["m20", "on-fire"], ["h24", "hot"], ["d7", "heavy"]]) {
205
+ const amount = projection.scores[w].amount, r = projection.scoreRanks[w];
206
+ if (amount !== null && amount > 0 && r !== null && r >= gates[w])
207
+ return label;
208
+ }
209
+ return null;
210
+ }
211
+
212
+ // src/index.ts
213
+ import { z as z2 } from "zod";
214
+ import { UsageVectorSchema } from "@phosphorco/bb-usage-algebra/vector";
215
+ var LIMITS = Object.freeze({ threadIds: 200, seriesPoints: 288, detailRows: 200, inputBytes: 65536, outputBytes: 2097152, describeBytes: 16384, candidates: 128, listedPlugins: 1e4, concurrency: 4, callMs: 2000, discoveryMs: 5000, familyEdges: 50000, familyMembers: 1e4, familyDepth: 256 });
216
+ var METRIC_BUCKET_MS = 300000;
217
+ var WINDOW_BUCKETS = { m20: 4, h24: 288, d7: 2016, lifetime: 2016 };
218
+ function retainedWindowStart(asOf, window) {
219
+ time2.parse(asOf);
220
+ windowSchema.parse(window);
221
+ return Math.max(0, Math.floor(asOf / METRIC_BUCKET_MS) * METRIC_BUCKET_MS - (WINDOW_BUCKETS[window] - 1) * METRIC_BUCKET_MS);
222
+ }
223
+ var METHODS = Object.freeze({ describe: "threadMetricsDescribe", read: "threadMetricsReadV1", detail: "threadMetricsDetailV1", recentSnapshot: "threadMetricsRecentSnapshotV1" });
224
+ var id2 = z2.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/);
225
+ var text = z2.string().min(1).max(256);
226
+ var time2 = z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
227
+ var money = z2.number().finite().nonnegative().nullable();
228
+ var scope = z2.enum(["own", "family"]);
229
+ var windowSchema = z2.enum(["m20", "h24", "d7", "lifetime"]);
230
+ var coverageSchema = z2.enum(["complete", "partial", "unknown"]);
231
+ var coverageReasonSchema = z2.enum(["source-gap", "root-unattested", "history-unavailable", "member-cap", "depth-cap", "edge-cap", "missing-thread", "unpriced", "retention", "row-cap", "pagination", "population-incomplete"]);
232
+ var reasons2 = z2.array(coverageReasonSchema).max(16);
233
+ var approximationSchema = z2.object({
234
+ approximate: z2.boolean(),
235
+ reasons: z2.array(z2.enum(["closest-model", "provider-default", "model-attribution", "bucket-boundary", "missing-price", "source-gap"])).max(6),
236
+ rates: z2.array(z2.object({ providerId: id2, modelId: text, rateId: text.nullable(), basis: z2.enum(["matched", "closest-model", "provider-default", "unpriced"]) }).superRefine((rate, ctx) => {
237
+ if (rate.basis === "unpriced" !== (rate.rateId === null))
238
+ ctx.addIssue({ code: "custom", path: ["rateId"], message: "Priced basis requires rateId; unpriced basis requires null" });
239
+ })).max(32).optional()
240
+ }).strip().superRefine((v, ctx) => {
241
+ if (!v.approximate && (v.reasons.length || v.rates?.some((r) => r.basis !== "matched")))
242
+ ctx.addIssue({ code: "custom", message: "Approximation provenance requires approximate flag" });
243
+ });
244
+ var rank2 = z2.number().finite().min(0).max(1).nullable();
245
+ var ranksSchema = z2.object({ m20: rank2, h24: rank2, d7: rank2 }).strip();
246
+ var population2 = z2.object({ asOf: time2, count: z2.number().int().nonnegative(), coverage: coverageSchema }).strip();
247
+ var rankPopulationSchema = z2.object({ m20: population2, h24: population2, d7: population2 }).strip();
248
+ var windowValueSchema = z2.object({ tokens: UsageVectorSchema, amount: money }).strip();
249
+ var windowsSchema = z2.object({ m20: windowValueSchema, h24: windowValueSchema, d7: windowValueSchema, lifetime: windowValueSchema }).strip();
250
+ var bucketSchema = z2.object({ start: time2, amount: money }).strip();
251
+ var metadata = {
252
+ threadId: id2,
253
+ scope,
254
+ memberCount: z2.number().int().nonnegative().max(LIMITS.familyMembers),
255
+ coverage: coverageSchema,
256
+ coverageReasons: reasons2,
257
+ excluded: z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
258
+ asOf: time2,
259
+ revision: text,
260
+ priceVersion: text,
261
+ currency: z2.literal("USD"),
262
+ estimated: z2.literal(true),
263
+ approximation: approximationSchema,
264
+ ranks: ranksSchema,
265
+ rankPopulation: rankPopulationSchema,
266
+ reported: z2.object({ amount: z2.number().finite().nonnegative(), currency: z2.string().min(1).max(16), asOf: time2 }).strip().optional()
267
+ };
268
+ var summaryRowSchema = z2.discriminatedUnion("changed", [
269
+ z2.object({ ...metadata, changed: z2.literal(true), windows: windowsSchema, buckets: z2.array(bucketSchema).max(LIMITS.seriesPoints).optional() }).strip(),
270
+ z2.object({ ...metadata, changed: z2.literal(false), windows: z2.never().optional(), buckets: z2.never().optional() }).strip()
271
+ ]);
272
+ var readInputSchema = z2.object({
273
+ threadIds: z2.array(id2).min(1).max(LIMITS.threadIds),
274
+ scope,
275
+ includeArchived: z2.boolean().optional(),
276
+ knownRevisions: z2.record(id2, text).optional(),
277
+ series: z2.object({ activeThreadId: id2, maxPoints: z2.number().int().min(1).max(LIMITS.seriesPoints) }).strict().optional()
278
+ }).strict().superRefine((v, ctx) => {
279
+ if (new Set(v.threadIds).size !== v.threadIds.length)
280
+ ctx.addIssue({ code: "custom", message: "Duplicate thread IDs" });
281
+ if (v.series && !v.threadIds.includes(v.series.activeThreadId))
282
+ ctx.addIssue({ code: "custom", message: "Series requires requested active thread" });
283
+ if (v.knownRevisions && Object.keys(v.knownRevisions).some((k) => !v.threadIds.includes(k)))
284
+ ctx.addIssue({ code: "custom", message: "Unrequested revision key" });
285
+ });
286
+ var readResultSchema = z2.object({ version: z2.literal(1), rows: z2.array(summaryRowSchema).max(LIMITS.threadIds) }).strip();
287
+ var detailInputSchema = z2.object({ threadId: id2, scope, includeArchived: z2.boolean().optional(), axis: z2.enum(["model", "time"]), window: windowSchema, cursor: z2.string().min(1).max(1024).optional(), limit: z2.number().int().min(1).max(LIMITS.detailRows).optional() }).strict();
288
+ var detailBase = { amount: money, tokens: UsageVectorSchema, approximation: approximationSchema };
289
+ var detailRowSchema = z2.discriminatedUnion("axis", [
290
+ z2.object({ ...detailBase, axis: z2.literal("model"), providerId: id2, modelId: text }).strip(),
291
+ z2.object({ ...detailBase, axis: z2.literal("time"), start: time2, end: time2 }).strip().refine((v) => v.end > v.start, { message: "Empty time interval" })
292
+ ]);
293
+ var detailResultSchema = z2.object({ version: z2.literal(1), threadId: id2, scope, axis: z2.enum(["model", "time"]), window: windowSchema, asOf: time2, revision: text, priceVersion: text, currency: z2.literal("USD"), estimated: z2.literal(true), coverage: coverageSchema, coverageReasons: reasons2, excluded: z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(), approximation: approximationSchema, total: windowValueSchema, rows: z2.array(detailRowSchema).max(LIMITS.detailRows), nextCursor: z2.string().min(1).max(1024).optional() }).strip();
294
+ var describeInputSchema = z2.object({}).strict();
295
+ var describeEnvelopeSchema = z2.object({ protocol: z2.literal("bb-thread-metrics"), versions: z2.array(z2.number().int().positive()).max(16) }).passthrough();
296
+ var capabilitiesSchema = z2.object({ summary: z2.literal(true), series: z2.boolean(), detail: z2.boolean(), recentSnapshotV1: z2.boolean().optional() }).strip();
297
+ var describeV1Schema = describeEnvelopeSchema.extend({ capabilities: capabilitiesSchema });
298
+ function parseBounded(schema, value, maxBytes) {
299
+ const json = JSON.stringify(value);
300
+ if (json === undefined || new TextEncoder().encode(json).byteLength > maxBytes) {
301
+ const error = new Error("Payload byte limit");
302
+ error.name = "MetricsCodecError";
303
+ throw error;
304
+ }
305
+ return schema.parse(value);
306
+ }
307
+ function negotiateVersion(value) {
308
+ const env = describeEnvelopeSchema.safeParse(value);
309
+ if (!env.success || !env.data.versions.includes(1))
310
+ return { kind: "incompatible" };
311
+ const v = describeV1Schema.safeParse(value);
312
+ return v.success ? { kind: "ready", version: 1, capabilities: v.data.capabilities } : { kind: "incompatible" };
313
+ }
314
+ function dollarPercentiles(population3) {
315
+ const seen = new Set;
316
+ for (const p of population3) {
317
+ if (seen.has(p.threadId))
318
+ throw new Error("Duplicate population thread");
319
+ seen.add(p.threadId);
320
+ if (p.amount !== null && (!Number.isFinite(p.amount) || p.amount < 0))
321
+ throw new Error("Invalid dollars");
322
+ }
323
+ const active = population3.filter((p) => p.amount !== null && p.amount > 0);
324
+ const sorted = active.map((p) => p.amount).sort((a, b) => a - b);
325
+ const lower = new Map;
326
+ sorted.forEach((x, i) => {
327
+ if (!lower.has(x))
328
+ lower.set(x, i);
329
+ });
330
+ return new Map(population3.map((p) => [p.threadId, p.amount === null || p.amount === 0 ? null : active.length < 2 ? 0 : lower.get(p.amount) / (active.length - 1)]));
331
+ }
332
+ function heat(ranks, gates = { m20: 0.9, h24: 0.9, d7: 0.9 }) {
333
+ for (const w of ["m20", "h24", "d7"])
334
+ if (!Number.isFinite(gates[w]) || gates[w] < 0 || gates[w] > 1)
335
+ throw new Error("Invalid gate");
336
+ return ranks.m20 !== null && ranks.m20 >= gates.m20 ? "on-fire" : ranks.h24 !== null && ranks.h24 >= gates.h24 ? "hot" : ranks.d7 !== null && ranks.d7 >= gates.d7 ? "heavy" : null;
337
+ }
338
+ function familyClosure(edges, roots, policy = {}) {
339
+ const cap = (n, fallback) => {
340
+ const v = n ?? fallback;
341
+ if (!Number.isInteger(v) || v < 1 || v > fallback)
342
+ throw new Error("Invalid family cap");
343
+ return v;
344
+ };
345
+ const maxMembers = cap(policy.maxMembers, LIMITS.familyMembers), maxDepth = cap(policy.maxDepth, LIMITS.familyDepth), maxEdges = cap(policy.maxEdges, LIMITS.familyEdges);
346
+ if (roots.length > LIMITS.threadIds)
347
+ throw new Error("Too many roots");
348
+ const why = new Set, nodes = new Map, children = new Map;
349
+ if (edges.length > maxEdges)
350
+ why.add("edge-cap");
351
+ for (const e of edges.slice(0, maxEdges)) {
352
+ if (nodes.has(e.threadId))
353
+ throw new Error("Duplicate edge thread");
354
+ nodes.set(e.threadId, e);
355
+ for (const p of [e.parentThreadId, e.lifecycleOwnerThreadId])
356
+ if (p) {
357
+ let c = children.get(p);
358
+ if (!c)
359
+ children.set(p, c = new Set);
360
+ c.add(e.threadId);
361
+ }
362
+ }
363
+ const queue = Array.from(new Set(roots), (threadId) => ({ threadId, depth: 0 })), seen = new Set, members = [];
364
+ for (let i = 0;i < queue.length; i++) {
365
+ const { threadId, depth } = queue[i];
366
+ if (seen.has(threadId))
367
+ continue;
368
+ seen.add(threadId);
369
+ const node = nodes.get(threadId);
370
+ if (!node) {
371
+ why.add("missing-thread");
372
+ continue;
373
+ }
374
+ if (node.deleted)
375
+ continue;
376
+ if (policy.includeArchived !== false || !node.archived) {
377
+ if (members.length >= maxMembers) {
378
+ why.add("member-cap");
379
+ break;
380
+ }
381
+ members.push(threadId);
382
+ }
383
+ const next = children.get(threadId);
384
+ if (next) {
385
+ if (depth >= maxDepth) {
386
+ if ([...next].some((c) => !seen.has(c)))
387
+ why.add("depth-cap");
388
+ } else
389
+ for (const c of next)
390
+ if (!seen.has(c))
391
+ queue.push({ threadId: c, depth: depth + 1 });
392
+ }
393
+ }
394
+ const source = coverageSchema.parse(policy.sourceCoverage ?? "complete");
395
+ if (source === "partial")
396
+ why.add("source-gap");
397
+ if (source === "unknown")
398
+ why.add("history-unavailable");
399
+ return { members, coverage: source === "unknown" ? "unknown" : why.size ? "partial" : "complete", coverageReasons: [...why] };
400
+ }
401
+ function validateReadResult(input, value) {
402
+ const out = parseBounded(readResultSchema, value, LIMITS.outputBytes), seen = new Set;
403
+ for (const row of out.rows) {
404
+ if (!input.threadIds.includes(row.threadId) || row.scope !== input.scope || seen.has(row.threadId))
405
+ throw new Error("Unrequested, duplicate or wrong-scope row");
406
+ seen.add(row.threadId);
407
+ if (!row.changed && input.knownRevisions?.[row.threadId] !== row.revision)
408
+ throw new Error("Unmatched unchanged revision");
409
+ if (row.changed && Object.values(row.windows).some((w) => w.amount === null) && !row.approximation.reasons.includes("missing-price"))
410
+ throw new Error("Unknown dollars require missing-price provenance");
411
+ if (row.changed && row.buckets) {
412
+ if (input.series?.activeThreadId !== row.threadId || row.buckets.length > input.series.maxPoints)
413
+ throw new Error("Unrequested series");
414
+ for (let i = 0;i < row.buckets.length; i++) {
415
+ const b = row.buckets[i];
416
+ if (b.amount === null && !row.approximation.reasons.includes("missing-price"))
417
+ throw new Error("Unknown bucket dollars require missing-price provenance");
418
+ if (b.start % METRIC_BUCKET_MS !== 0 || b.start > row.asOf || b.start < retainedWindowStart(row.asOf, "h24") || i > 0 && b.start <= row.buckets[i - 1].start)
419
+ throw new Error("Invalid series range/order");
420
+ }
421
+ }
422
+ }
423
+ return out;
424
+ }
425
+ function validateDetailResult(input, value) {
426
+ const out = parseBounded(detailResultSchema, value, LIMITS.outputBytes);
427
+ if (out.total.amount === null && !out.approximation.reasons.includes("missing-price"))
428
+ throw new Error("Unknown total dollars require missing-price provenance");
429
+ if (out.nextCursor && out.coverage === "complete")
430
+ throw new Error("Pagination must disclose incomplete coverage");
431
+ if (out.threadId !== input.threadId || out.scope !== input.scope || out.axis !== input.axis || out.window !== input.window || out.rows.length > (input.limit ?? LIMITS.detailRows))
432
+ throw new Error("Detail request mismatch");
433
+ let priorEnd;
434
+ const keys = new Set;
435
+ for (const row of out.rows) {
436
+ if (row.approximation.approximate && (!out.approximation.approximate || row.approximation.reasons.some((reason2) => !out.approximation.reasons.includes(reason2))))
437
+ throw new Error("Aggregate total must retain contributing row approximation provenance");
438
+ if (row.amount === null && !row.approximation.reasons.includes("missing-price"))
439
+ throw new Error("Unknown dollars require missing-price provenance");
440
+ if (row.axis !== input.axis)
441
+ throw new Error("Detail axis mismatch");
442
+ if (row.axis === "time") {
443
+ if (row.start < retainedWindowStart(out.asOf, input.window) || row.start >= out.asOf || row.end > out.asOf || row.start % METRIC_BUCKET_MS !== 0 || row.end !== out.asOf && row.end % METRIC_BUCKET_MS !== 0 || priorEnd !== undefined && row.start < priorEnd)
444
+ throw new Error("Invalid time detail range/order/overlap");
445
+ priorEnd = row.end;
446
+ }
447
+ const key = row.axis === "model" ? JSON.stringify([row.providerId, row.modelId]) : String(row.start);
448
+ if (keys.has(key))
449
+ throw new Error("Duplicate detail row");
450
+ keys.add(key);
451
+ }
452
+ if (out.coverage === "complete" && !input.cursor && !out.nextCursor) {
453
+ for (const key of ["inputUncached", "cacheRead", "cacheWrite", "output"]) {
454
+ const sum = out.rows.reduce((n, r) => n + r.tokens[key], 0);
455
+ if (!Number.isFinite(sum) || Math.abs(sum - out.total.tokens[key]) > 0.00000001 * Math.max(1, sum, out.total.tokens[key]))
456
+ throw new Error("Detail token total mismatch");
457
+ }
458
+ const hasUnknown = out.rows.some((r) => r.amount === null);
459
+ if (hasUnknown && out.total.amount !== null)
460
+ throw new Error("Unknown detail dollars must remain unknown");
461
+ if (!hasUnknown) {
462
+ const sum = out.rows.reduce((n, r) => n + (r.amount ?? 0), 0);
463
+ if (out.total.amount === null || !Number.isFinite(sum) || Math.abs(sum - out.total.amount) > 0.00000001 * Math.max(1, sum, out.total.amount))
464
+ throw new Error("Detail dollar total mismatch");
465
+ }
466
+ }
467
+ return out;
468
+ }
469
+ function classifyOwnerError(error) {
470
+ const e = error;
471
+ if (e?.status === 404 || e?.code === "unknown_method")
472
+ return "absent";
473
+ if (e?.status === 503)
474
+ return "disabled";
475
+ if (e?.name === "TimeoutError")
476
+ return "timeout";
477
+ if (e?.name === "AbortError")
478
+ return "aborted";
479
+ if (e?.name === "ZodError" || e?.name === "MetricsCodecError")
480
+ return "malformed";
481
+ return "failed";
482
+ }
483
+ export {
484
+ windowsSchema,
485
+ windowValueSchema,
486
+ windowSchema,
487
+ weightedScoreV2,
488
+ validateRecentSnapshot,
489
+ validateReadResult,
490
+ validateDetailResult,
491
+ summaryRowSchema,
492
+ scoringConfigV2Schema,
493
+ retainedWindowStart,
494
+ recentWindowsSchema,
495
+ recentSnapshotResultSchema,
496
+ recentSnapshotInputSchema,
497
+ recentSnapshotFullSchema,
498
+ recentScopeSchema,
499
+ recentRowSchema,
500
+ recentPercentileHeat,
501
+ recentDollarSchema,
502
+ recentApproximationSchema,
503
+ readResultSchema,
504
+ readInputSchema,
505
+ ranksSchema,
506
+ rankPopulationSchema,
507
+ parseBounded,
508
+ negotiateVersion,
509
+ heat,
510
+ familyClosure,
511
+ dollarPercentiles,
512
+ detailRowSchema,
513
+ detailResultSchema,
514
+ detailInputSchema,
515
+ describeV1Schema,
516
+ describeInputSchema,
517
+ describeEnvelopeSchema,
518
+ coverageSchema,
519
+ coverageReasonSchema,
520
+ classifyOwnerError,
521
+ capabilitiesSchema,
522
+ bucketSchema,
523
+ approximationSchema,
524
+ RECENT_LIMITS,
525
+ METRIC_BUCKET_MS,
526
+ METHODS,
527
+ LIMITS,
528
+ DEFAULT_SCORING_CONFIG_V2
529
+ };
@@ -0,0 +1,92 @@
1
+ // src/testing.ts
2
+ import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
3
+ import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot, METHODS, LIMITS, readInputSchema, detailInputSchema, describeEnvelopeSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult } from "./index.js";
4
+ async function runSupplierConformance(options) {
5
+ if (!options.readCases.length)
6
+ throw new Error("At least one real read case required");
7
+ const host = createFakePluginHost({ pluginId: options.pluginId }), checks = [];
8
+ const deadline = options.deadlineMs ?? LIMITS.callMs;
9
+ if (!Number.isInteger(deadline) || deadline < 1 || deadline > LIMITS.discoveryMs)
10
+ throw new Error("Invalid conformance deadline");
11
+ const call = (method, input) => host.harness.behavior.callRpc(method, input);
12
+ async function check(name, fn) {
13
+ let timer;
14
+ try {
15
+ await Promise.race([fn(), new Promise((_, reject) => {
16
+ timer = setTimeout(() => reject(new Error("Conformance deadline")), deadline);
17
+ })]);
18
+ checks.push({ name, passed: true });
19
+ } catch (e) {
20
+ checks.push({ name, passed: false, error: e instanceof Error ? e.message : String(e) });
21
+ } finally {
22
+ clearTimeout(timer);
23
+ }
24
+ }
25
+ function validationError(e) {
26
+ const v = e;
27
+ return v.name === "ZodError" && Array.isArray(v.issues) || v.code === "invalid_input" || v.body?.code === "invalid_input";
28
+ }
29
+ async function rejectsInput(method, input) {
30
+ try {
31
+ await call(method, input);
32
+ } catch (e) {
33
+ if (validationError(e))
34
+ return;
35
+ throw new Error("Expected structured invalid_input or ZodError");
36
+ }
37
+ throw new Error("Invalid input accepted");
38
+ }
39
+ try {
40
+ await options.register(host.bb);
41
+ let detail = false, recent = false;
42
+ await check("describe-negotiation", async () => {
43
+ const n = negotiateVersion(parseBounded(describeEnvelopeSchema, await call(METHODS.describe, {}), LIMITS.describeBytes));
44
+ if (n.kind !== "ready")
45
+ throw new Error("No compatible v1");
46
+ detail = n.capabilities.detail;
47
+ recent = Boolean(n.capabilities.recentSnapshotV1);
48
+ });
49
+ await check("strict-describe-input", () => rejectsInput(METHODS.describe, { unexpected: true }));
50
+ for (let i = 0;i < options.readCases.length; i++)
51
+ await check(`read-${i}`, async () => {
52
+ const input = parseBounded(readInputSchema, options.readCases[i], LIMITS.inputBytes);
53
+ validateReadResult(input, await call(METHODS.read, input));
54
+ });
55
+ await check("strict-read-input", () => rejectsInput(METHODS.read, { ...options.readCases[0], unexpected: true }));
56
+ await check("oversized-id-count", () => rejectsInput(METHODS.read, { scope: "family", threadIds: Array.from({ length: 201 }, (_, i) => `thr_${i}`) }));
57
+ await check("oversized-input-bytes", async () => {
58
+ const threadIds = Array.from({ length: 200 }, (_, i) => `thr_${i}_`.padEnd(200, "x"));
59
+ const oversized = { scope: "family", threadIds, knownRevisions: Object.fromEntries(threadIds.map((id) => [id, "r".repeat(256)])) };
60
+ readInputSchema.parse(oversized);
61
+ if (new TextEncoder().encode(JSON.stringify(oversized)).byteLength <= LIMITS.inputBytes)
62
+ throw new Error("Byte fixture is not oversized");
63
+ await rejectsInput(METHODS.read, oversized);
64
+ });
65
+ if (recent) {
66
+ if (!options.recentCases?.length)
67
+ checks.push({ name: "recent-cases-required", passed: false, error: "Advertised recent snapshot needs real cases" });
68
+ for (let i = 0;i < (options.recentCases?.length ?? 0); i++)
69
+ await check(`recent-${i}`, async () => {
70
+ const input = parseBounded(recentSnapshotInputSchema, options.recentCases[i], RECENT_LIMITS.inputBytes);
71
+ validateRecentSnapshot(input, await call(METHODS.recentSnapshot, input));
72
+ });
73
+ await check("strict-recent-input", () => rejectsInput(METHODS.recentSnapshot, { unexpected: true }));
74
+ }
75
+ if (detail) {
76
+ if (!options.detailCases?.length)
77
+ checks.push({ name: "detail-cases-required", passed: false, error: "Advertised detail needs real cases" });
78
+ for (let i = 0;i < (options.detailCases?.length ?? 0); i++)
79
+ await check(`detail-${i}`, async () => {
80
+ const input = parseBounded(detailInputSchema, options.detailCases[i], LIMITS.inputBytes);
81
+ validateDetailResult(input, await call(METHODS.detail, input));
82
+ });
83
+ await check("strict-detail-input", () => rejectsInput(METHODS.detail, { ...options.detailCases?.[0], unexpected: true }));
84
+ }
85
+ } finally {
86
+ await host.harness.lifecycle.dispose();
87
+ }
88
+ return { passed: checks.every((c) => c.passed), checks, proof: "source-conformance" };
89
+ }
90
+ export {
91
+ runSupplierConformance
92
+ };