@phosphorco/bb-thread-metrics 0.1.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/CONTRACT.md +83 -1
- package/README.md +28 -1
- package/dist/bb.js +12 -1
- package/dist/index.js +279 -54
- package/dist/testing.js +13 -2
- package/dist/types/bb.d.ts +439 -15
- package/dist/types/index.d.ts +38 -34
- package/dist/types/recent.d.ts +1661 -0
- package/dist/types/testing.d.ts +2 -1
- package/package.json +2 -2
package/CONTRACT.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Thread metrics wire v1 (@phosphorco/bb-thread-metrics 0.
|
|
1
|
+
# Thread metrics wire v1 (@phosphorco/bb-thread-metrics 0.2.0 candidate)
|
|
2
2
|
|
|
3
3
|
| Supplier owns | Package owns | Host owns |
|
|
4
4
|
| --- | --- | --- |
|
|
@@ -316,3 +316,85 @@ obsolete outcomes. These post-await fences are required even when the SDK claims
|
|
|
316
316
|
cancellation support. Keep last good data during failure according to the host's
|
|
317
317
|
staleness policy; a late response cannot revive a disposed/replaced demand.
|
|
318
318
|
No package-global lifecycle or client cancellation machinery is introduced.
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
## Additive recent snapshot capability (package0.2)
|
|
322
|
+
|
|
323
|
+
The new optional boolean `recentSnapshotV1` capability requires the exact fixed
|
|
324
|
+
`threadMetricsRecentSnapshotV1` handler; registration rejects either mismatch.
|
|
325
|
+
Input is strict `{ifRevision?:string}` within1024 canonical UTF-8 JSON bytes.
|
|
326
|
+
Full replies have wire version1, `unchanged:false`, opaque revision, immutable
|
|
327
|
+
publication time, rank time,72h recent window, a published config revision/value,
|
|
328
|
+
explicit coverage/materialization-pending/cap state and rows. Unchanged replies
|
|
329
|
+
have version1, `unchanged:true`, revision and no recognized full-only payload.
|
|
330
|
+
Recognized rows/config/times/population/coverage/cap fields are rejected BEFORE
|
|
331
|
+
stripping; unrelated future additive fields remain allowed within the byte cap. The revision
|
|
332
|
+
must exactly match requested ifRevision. A new supplier instance must not reuse a
|
|
333
|
+
revision for different content. Reads do not advance revision or run fold,
|
|
334
|
+
closure, rank or materializer work; suppliers serve a held published object.
|
|
335
|
+
Adapters validate shape/bytes but cannot prove supplier implementation purity.
|
|
336
|
+
|
|
337
|
+
Each row has independent own/family last-fold timestamps, asOf and content
|
|
338
|
+
revision; at least one activity timestamp must be in the envelope's72h window.
|
|
339
|
+
The row score-config revision must match the published envelope config. Own and
|
|
340
|
+
family each carry memberCount/coverage/priceVersion, compact approximation
|
|
341
|
+
provenance, four raw dollar windows and
|
|
342
|
+
provider-attributed dollar windows, omitted-provider count, four weighted-score
|
|
343
|
+
windows, and separately named dollar/score percentile ranks. `estimate`/`floor`
|
|
344
|
+
values are finite nonnegative amounts; `unavailable` has null amount. Explicit
|
|
345
|
+
floor0 is a supplier attestation; unsupported root zeros must be unavailable,
|
|
346
|
+
never inferred from a partial population or a missing row. Family providers must
|
|
347
|
+
represent deduplicated actual members, not sums of other family rows.
|
|
348
|
+
|
|
349
|
+
Full canonical JSON is capped at4MiB including additive unknown fields,1000 rows,
|
|
350
|
+
8KiB per raw row before stripping, and32 provider entries per scope. Omitted
|
|
351
|
+
providers/rows, uncertain omission counts (`exact:false`, even lower-bound0),
|
|
352
|
+
and pending materialization cannot claim complete coverage. These
|
|
353
|
+
are post-transport decoded bounds, not raw-network/preparse allocation limits.
|
|
354
|
+
Providers select most recent rows within both row/byte caps, publish cutoff and
|
|
355
|
+
honest omission state; row omission alone proves neither absence nor zero. The
|
|
356
|
+
codec always checks finite known provider subtotals and weighted arithmetic,
|
|
357
|
+
including when other providers are unavailable or omitted. Available totals must
|
|
358
|
+
cover every known subtotal within eight relative machine epsilons (no absolute
|
|
359
|
+
small-dollar epsilon); incomplete provider sets or contributing floors
|
|
360
|
+
require a floor rather than an estimate. Unknown components may yield unavailable
|
|
361
|
+
totals, never invented zero. Complete provider sets reconcile exact totals.
|
|
362
|
+
|
|
363
|
+
Each scope AND provider has `approximation:{approximate,reasons}` independently
|
|
364
|
+
of observation coverage. Compact bounded reasons retain `closest-model`,
|
|
365
|
+
`provider-default`, `model-attribution`, `bucket-boundary`, `missing-price`, and
|
|
366
|
+
`source-gap`; aggregate provenance must include contributing provider reasons.
|
|
367
|
+
Unavailable prices require `missing-price`. Unavailable observations are different:
|
|
368
|
+
a partial/unknown projection with source-gap approximation and source-gap,
|
|
369
|
+
root-unattested, history-unavailable or missing-thread coverage may retain
|
|
370
|
+
unavailable values without claiming a price failure. Any explicit unpriced
|
|
371
|
+
coverage still requires missing-price, and provider missing-price reasons must
|
|
372
|
+
remain in aggregate approximation. Suppliers must not label an actual pricing
|
|
373
|
+
gap as an observation gap. This is the lossless reason mapping
|
|
374
|
+
from readV1; per-model rate associations remain in on-demand read/detail and are
|
|
375
|
+
not copied into listing rows. `priceVersion` still identifies the price table.
|
|
376
|
+
Real materializer conformance remains necessary before release.
|
|
377
|
+
|
|
378
|
+
Scoring-config schemaVersion2 is versioned separately from normalization semantics.
|
|
379
|
+
Default mode is percentile with gates .9 m20/h24/d7; omitted provider weights mean1.
|
|
380
|
+
Absolute mode supplies thresholds for all four windows. `weightedScoreV2` sums
|
|
381
|
+
provider dollars times weights, returns unknown when a component is unavailable,
|
|
382
|
+
and rejects invalid/duplicate provider values and arithmetic overflow. Weighting
|
|
383
|
+
never reprices the raw-dollar fields. UI, settings CAS and scoring policy remain
|
|
384
|
+
ledger/surface-owned; this package contains no settings storage or sliders.
|
|
385
|
+
|
|
386
|
+
Ranks cannot be non-null for an empty population; a positive singleton ranks0.
|
|
387
|
+
When rankAsOf is at or after the row value epoch, zero or unavailable amounts require null rank.
|
|
388
|
+
Older rank epochs may deliberately remain while newer amounts change; consumers
|
|
389
|
+
must gate heat on supported positive CURRENT amounts as well as ranks.
|
|
390
|
+
`recentPercentileHeat` applies that current weighted-score gate; legacy
|
|
391
|
+
`heat(ranks)` alone is insufficient for recent rows. This codec does not rebuild
|
|
392
|
+
or prove actual population percentiles.
|
|
393
|
+
|
|
394
|
+
Supplier ranks recompute at most once/minute outside reads. A publication binds
|
|
395
|
+
configuration and scored content atomically; pending changes retain explicit
|
|
396
|
+
coverage. Consumers hold the object by SDK-owner identity and use keyed row
|
|
397
|
+
selectors. They may conditionally poll at30s while interested/visible/connected,
|
|
398
|
+
coalescing existing hints without a per-thread request. Covered listing rows need
|
|
399
|
+
no readV1; explicit inspection outside the window may use the existing bounded
|
|
400
|
+
read/detail lane. The contract package itself never polls or retries.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @phosphorco/bb-thread-metrics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Version0.2.0 candidate (not released): adds a bounded recent snapshot and scoring-configv2. Existing wirev1 summaries, family closure, heat ranks and optional
|
|
4
4
|
on-demand breakdowns for independent BB plugins. See [CONTRACT.md](CONTRACT.md)
|
|
5
5
|
for ownership, wire semantics and proof limits.
|
|
6
6
|
|
|
@@ -29,3 +29,30 @@ checks emitted declaration compatibility with server and browser SDKs. The
|
|
|
29
29
|
`/testing` entry's `runSupplierConformance` accepts the adopter's real registration
|
|
30
30
|
factory and representative read/detail cases. It returns explicit source
|
|
31
31
|
conformance checks, never live host acceptance.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Recent snapshot (0.2 candidate)
|
|
35
|
+
|
|
36
|
+
Owners may advertise `capabilities.recentSnapshotV1:true` with a matching
|
|
37
|
+
`recentSnapshot(input)` handler. `createThreadMetricsClient(...).recentSnapshot({ifRevision?})`
|
|
38
|
+
returns the immutable precomputed object or an unchanged revision. Absence of this
|
|
39
|
+
optional capability leaves the0.1 read/detail contract intact. Old consumers may
|
|
40
|
+
ignore it. The adapter does not add refresh timers, caches or materialization.
|
|
41
|
+
|
|
42
|
+
The full response holds own/family raw estimated-dollar windows and provider
|
|
43
|
+
contributions, separately named weighted scores, raw dollar ranks and score ranks,
|
|
44
|
+
activity/materialization/rank times, coverage/cap/pending flags and the published
|
|
45
|
+
scoring configuration. Zero is explicit supplier evidence; omission/unavailable is
|
|
46
|
+
not zero. Config weights affect heat scores, never retained dollars. Defaults are
|
|
47
|
+
weight1 and current p90 percentile gates. Raw/scored populations remain distinct.
|
|
48
|
+
|
|
49
|
+
Bounds:72h recent activity,1000 rows,4MiB canonical JSON and8KiB/row,32 provider
|
|
50
|
+
entries per projection/config. Bounds include unknown additive fields before
|
|
51
|
+
schema stripping; the SDK has already parsed transport JSON. Providers enforce
|
|
52
|
+
materializer work/storage and publication bounds independently. Clients must not
|
|
53
|
+
turn row omission into an eager per-ID read storm. The planned coordinator uses
|
|
54
|
+
one held snapshot/mount+30s conditional cadence; the package schedules nothing.
|
|
55
|
+
|
|
56
|
+
Current source tests are package/adapter proof only. Registry release, ledger
|
|
57
|
+
materialization, live byte/time measurement, all consumer adoptions and native
|
|
58
|
+
host/freshness proof remain separate gates.
|
package/dist/bb.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/bb.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { LIMITS, METHODS, describeInputSchema, describeEnvelopeSchema, capabilitiesSchema, readInputSchema, detailInputSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult, classifyOwnerError } from "./index.js";
|
|
3
|
+
import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot, LIMITS, METHODS, describeInputSchema, describeEnvelopeSchema, capabilitiesSchema, readInputSchema, detailInputSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult, classifyOwnerError } from "./index.js";
|
|
4
4
|
function sdkSchema(schema, maxBytes) {
|
|
5
5
|
return { "~standard": { version: 1, vendor: "bb-thread-metrics", jsonSchema: { input: () => z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }), output: () => z.toJSONSchema(schema, { io: "output", unrepresentable: "any" }) }, validate(value) {
|
|
6
6
|
try {
|
|
@@ -12,6 +12,8 @@ function sdkSchema(schema, maxBytes) {
|
|
|
12
12
|
}
|
|
13
13
|
function registerThreadMetricsOwner(bb, port) {
|
|
14
14
|
const capabilities = capabilitiesSchema.parse(port.capabilities);
|
|
15
|
+
if (Boolean(capabilities.recentSnapshotV1) !== Boolean(port.recentSnapshot))
|
|
16
|
+
throw new Error("Recent snapshot capability/handler mismatch");
|
|
15
17
|
if (capabilities.detail !== Boolean(port.detail))
|
|
16
18
|
throw new Error("Detail capability/handler mismatch");
|
|
17
19
|
const describe = () => ({ protocol: "bb-thread-metrics", versions: [1], capabilities });
|
|
@@ -32,6 +34,11 @@ function registerThreadMetricsOwner(bb, port) {
|
|
|
32
34
|
}
|
|
33
35
|
};
|
|
34
36
|
bb.rpc.register(contract, handlers, { experimental_discoverable: true });
|
|
37
|
+
if (port.recentSnapshot)
|
|
38
|
+
bb.rpc.register({ threadMetricsRecentSnapshotV1: { input: sdkSchema(recentSnapshotInputSchema, RECENT_LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsRecentSnapshotV1: async (raw) => {
|
|
39
|
+
const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
|
|
40
|
+
return validateRecentSnapshot(input, await port.recentSnapshot(input));
|
|
41
|
+
} }, { experimental_discoverable: true });
|
|
35
42
|
if (port.detail)
|
|
36
43
|
bb.rpc.register({ threadMetricsDetailV1: { input: sdkSchema(detailInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsDetailV1: async (raw) => {
|
|
37
44
|
const input = parseBounded(detailInputSchema, raw, LIMITS.inputBytes);
|
|
@@ -50,6 +57,10 @@ function createThreadMetricsClient(sdk, pluginId) {
|
|
|
50
57
|
const raw = await call(METHODS.describe, {}, signal);
|
|
51
58
|
return negotiateVersion(parseBounded(describeEnvelopeSchema, raw, LIMITS.describeBytes));
|
|
52
59
|
},
|
|
60
|
+
async recentSnapshot(raw = {}, signal) {
|
|
61
|
+
const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
|
|
62
|
+
return validateRecentSnapshot(input, await call(METHODS.recentSnapshot, input, signal));
|
|
63
|
+
},
|
|
53
64
|
async read(raw, signal) {
|
|
54
65
|
const input = parseBounded(readInputSchema, raw, LIMITS.inputBytes);
|
|
55
66
|
return validateReadResult(input, await call(METHODS.read, input, signal));
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,239 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/recent.ts
|
|
2
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";
|
|
3
214
|
import { UsageVectorSchema } from "@phosphorco/bb-usage-algebra/vector";
|
|
4
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 });
|
|
5
216
|
var METRIC_BUCKET_MS = 300000;
|
|
6
217
|
var WINDOW_BUCKETS = { m20: 4, h24: 288, d7: 2016, lifetime: 2016 };
|
|
7
218
|
function retainedWindowStart(asOf, window) {
|
|
8
|
-
|
|
219
|
+
time2.parse(asOf);
|
|
9
220
|
windowSchema.parse(window);
|
|
10
221
|
return Math.max(0, Math.floor(asOf / METRIC_BUCKET_MS) * METRIC_BUCKET_MS - (WINDOW_BUCKETS[window] - 1) * METRIC_BUCKET_MS);
|
|
11
222
|
}
|
|
12
|
-
var METHODS = Object.freeze({ describe: "threadMetricsDescribe", read: "threadMetricsReadV1", detail: "threadMetricsDetailV1" });
|
|
13
|
-
var
|
|
14
|
-
var text =
|
|
15
|
-
var
|
|
16
|
-
var money =
|
|
17
|
-
var scope =
|
|
18
|
-
var windowSchema =
|
|
19
|
-
var coverageSchema =
|
|
20
|
-
var coverageReasonSchema =
|
|
21
|
-
var
|
|
22
|
-
var approximationSchema =
|
|
23
|
-
approximate:
|
|
24
|
-
reasons:
|
|
25
|
-
rates:
|
|
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) => {
|
|
26
237
|
if (rate.basis === "unpriced" !== (rate.rateId === null))
|
|
27
238
|
ctx.addIssue({ code: "custom", path: ["rateId"], message: "Priced basis requires rateId; unpriced basis requires null" });
|
|
28
239
|
})).max(32).optional()
|
|
@@ -30,40 +241,40 @@ var approximationSchema = z.object({
|
|
|
30
241
|
if (!v.approximate && (v.reasons.length || v.rates?.some((r) => r.basis !== "matched")))
|
|
31
242
|
ctx.addIssue({ code: "custom", message: "Approximation provenance requires approximate flag" });
|
|
32
243
|
});
|
|
33
|
-
var
|
|
34
|
-
var ranksSchema =
|
|
35
|
-
var
|
|
36
|
-
var rankPopulationSchema =
|
|
37
|
-
var windowValueSchema =
|
|
38
|
-
var windowsSchema =
|
|
39
|
-
var bucketSchema =
|
|
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();
|
|
40
251
|
var metadata = {
|
|
41
|
-
threadId:
|
|
252
|
+
threadId: id2,
|
|
42
253
|
scope,
|
|
43
|
-
memberCount:
|
|
254
|
+
memberCount: z2.number().int().nonnegative().max(LIMITS.familyMembers),
|
|
44
255
|
coverage: coverageSchema,
|
|
45
|
-
coverageReasons:
|
|
46
|
-
excluded:
|
|
47
|
-
asOf:
|
|
256
|
+
coverageReasons: reasons2,
|
|
257
|
+
excluded: z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
|
|
258
|
+
asOf: time2,
|
|
48
259
|
revision: text,
|
|
49
260
|
priceVersion: text,
|
|
50
|
-
currency:
|
|
51
|
-
estimated:
|
|
261
|
+
currency: z2.literal("USD"),
|
|
262
|
+
estimated: z2.literal(true),
|
|
52
263
|
approximation: approximationSchema,
|
|
53
264
|
ranks: ranksSchema,
|
|
54
265
|
rankPopulation: rankPopulationSchema,
|
|
55
|
-
reported:
|
|
266
|
+
reported: z2.object({ amount: z2.number().finite().nonnegative(), currency: z2.string().min(1).max(16), asOf: time2 }).strip().optional()
|
|
56
267
|
};
|
|
57
|
-
var summaryRowSchema =
|
|
58
|
-
|
|
59
|
-
|
|
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()
|
|
60
271
|
]);
|
|
61
|
-
var readInputSchema =
|
|
62
|
-
threadIds:
|
|
272
|
+
var readInputSchema = z2.object({
|
|
273
|
+
threadIds: z2.array(id2).min(1).max(LIMITS.threadIds),
|
|
63
274
|
scope,
|
|
64
|
-
includeArchived:
|
|
65
|
-
knownRevisions:
|
|
66
|
-
series:
|
|
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()
|
|
67
278
|
}).strict().superRefine((v, ctx) => {
|
|
68
279
|
if (new Set(v.threadIds).size !== v.threadIds.length)
|
|
69
280
|
ctx.addIssue({ code: "custom", message: "Duplicate thread IDs" });
|
|
@@ -72,17 +283,17 @@ var readInputSchema = z.object({
|
|
|
72
283
|
if (v.knownRevisions && Object.keys(v.knownRevisions).some((k) => !v.threadIds.includes(k)))
|
|
73
284
|
ctx.addIssue({ code: "custom", message: "Unrequested revision key" });
|
|
74
285
|
});
|
|
75
|
-
var readResultSchema =
|
|
76
|
-
var detailInputSchema =
|
|
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();
|
|
77
288
|
var detailBase = { amount: money, tokens: UsageVectorSchema, approximation: approximationSchema };
|
|
78
|
-
var detailRowSchema =
|
|
79
|
-
|
|
80
|
-
|
|
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" })
|
|
81
292
|
]);
|
|
82
|
-
var detailResultSchema =
|
|
83
|
-
var describeInputSchema =
|
|
84
|
-
var describeEnvelopeSchema =
|
|
85
|
-
var capabilitiesSchema =
|
|
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();
|
|
86
297
|
var describeV1Schema = describeEnvelopeSchema.extend({ capabilities: capabilitiesSchema });
|
|
87
298
|
function parseBounded(schema, value, maxBytes) {
|
|
88
299
|
const json = JSON.stringify(value);
|
|
@@ -100,23 +311,23 @@ function negotiateVersion(value) {
|
|
|
100
311
|
const v = describeV1Schema.safeParse(value);
|
|
101
312
|
return v.success ? { kind: "ready", version: 1, capabilities: v.data.capabilities } : { kind: "incompatible" };
|
|
102
313
|
}
|
|
103
|
-
function dollarPercentiles(
|
|
314
|
+
function dollarPercentiles(population3) {
|
|
104
315
|
const seen = new Set;
|
|
105
|
-
for (const p of
|
|
316
|
+
for (const p of population3) {
|
|
106
317
|
if (seen.has(p.threadId))
|
|
107
318
|
throw new Error("Duplicate population thread");
|
|
108
319
|
seen.add(p.threadId);
|
|
109
320
|
if (p.amount !== null && (!Number.isFinite(p.amount) || p.amount < 0))
|
|
110
321
|
throw new Error("Invalid dollars");
|
|
111
322
|
}
|
|
112
|
-
const active =
|
|
323
|
+
const active = population3.filter((p) => p.amount !== null && p.amount > 0);
|
|
113
324
|
const sorted = active.map((p) => p.amount).sort((a, b) => a - b);
|
|
114
325
|
const lower = new Map;
|
|
115
326
|
sorted.forEach((x, i) => {
|
|
116
327
|
if (!lower.has(x))
|
|
117
328
|
lower.set(x, i);
|
|
118
329
|
});
|
|
119
|
-
return new Map(
|
|
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)]));
|
|
120
331
|
}
|
|
121
332
|
function heat(ranks, gates = { m20: 0.9, h24: 0.9, d7: 0.9 }) {
|
|
122
333
|
for (const w of ["m20", "h24", "d7"])
|
|
@@ -222,7 +433,7 @@ function validateDetailResult(input, value) {
|
|
|
222
433
|
let priorEnd;
|
|
223
434
|
const keys = new Set;
|
|
224
435
|
for (const row of out.rows) {
|
|
225
|
-
if (row.approximation.approximate && (!out.approximation.approximate || row.approximation.reasons.some((
|
|
436
|
+
if (row.approximation.approximate && (!out.approximation.approximate || row.approximation.reasons.some((reason2) => !out.approximation.reasons.includes(reason2))))
|
|
226
437
|
throw new Error("Aggregate total must retain contributing row approximation provenance");
|
|
227
438
|
if (row.amount === null && !row.approximation.reasons.includes("missing-price"))
|
|
228
439
|
throw new Error("Unknown dollars require missing-price provenance");
|
|
@@ -273,10 +484,22 @@ export {
|
|
|
273
484
|
windowsSchema,
|
|
274
485
|
windowValueSchema,
|
|
275
486
|
windowSchema,
|
|
487
|
+
weightedScoreV2,
|
|
488
|
+
validateRecentSnapshot,
|
|
276
489
|
validateReadResult,
|
|
277
490
|
validateDetailResult,
|
|
278
491
|
summaryRowSchema,
|
|
492
|
+
scoringConfigV2Schema,
|
|
279
493
|
retainedWindowStart,
|
|
494
|
+
recentWindowsSchema,
|
|
495
|
+
recentSnapshotResultSchema,
|
|
496
|
+
recentSnapshotInputSchema,
|
|
497
|
+
recentSnapshotFullSchema,
|
|
498
|
+
recentScopeSchema,
|
|
499
|
+
recentRowSchema,
|
|
500
|
+
recentPercentileHeat,
|
|
501
|
+
recentDollarSchema,
|
|
502
|
+
recentApproximationSchema,
|
|
280
503
|
readResultSchema,
|
|
281
504
|
readInputSchema,
|
|
282
505
|
ranksSchema,
|
|
@@ -298,7 +521,9 @@ export {
|
|
|
298
521
|
capabilitiesSchema,
|
|
299
522
|
bucketSchema,
|
|
300
523
|
approximationSchema,
|
|
524
|
+
RECENT_LIMITS,
|
|
301
525
|
METRIC_BUCKET_MS,
|
|
302
526
|
METHODS,
|
|
303
|
-
LIMITS
|
|
527
|
+
LIMITS,
|
|
528
|
+
DEFAULT_SCORING_CONFIG_V2
|
|
304
529
|
};
|
package/dist/testing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/testing.ts
|
|
2
2
|
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
|
|
3
|
-
import { METHODS, LIMITS, readInputSchema, detailInputSchema, describeEnvelopeSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult } from "./index.js";
|
|
3
|
+
import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot, METHODS, LIMITS, readInputSchema, detailInputSchema, describeEnvelopeSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult } from "./index.js";
|
|
4
4
|
async function runSupplierConformance(options) {
|
|
5
5
|
if (!options.readCases.length)
|
|
6
6
|
throw new Error("At least one real read case required");
|
|
@@ -38,12 +38,13 @@ async function runSupplierConformance(options) {
|
|
|
38
38
|
}
|
|
39
39
|
try {
|
|
40
40
|
await options.register(host.bb);
|
|
41
|
-
let detail = false;
|
|
41
|
+
let detail = false, recent = false;
|
|
42
42
|
await check("describe-negotiation", async () => {
|
|
43
43
|
const n = negotiateVersion(parseBounded(describeEnvelopeSchema, await call(METHODS.describe, {}), LIMITS.describeBytes));
|
|
44
44
|
if (n.kind !== "ready")
|
|
45
45
|
throw new Error("No compatible v1");
|
|
46
46
|
detail = n.capabilities.detail;
|
|
47
|
+
recent = Boolean(n.capabilities.recentSnapshotV1);
|
|
47
48
|
});
|
|
48
49
|
await check("strict-describe-input", () => rejectsInput(METHODS.describe, { unexpected: true }));
|
|
49
50
|
for (let i = 0;i < options.readCases.length; i++)
|
|
@@ -61,6 +62,16 @@ async function runSupplierConformance(options) {
|
|
|
61
62
|
throw new Error("Byte fixture is not oversized");
|
|
62
63
|
await rejectsInput(METHODS.read, oversized);
|
|
63
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
|
+
}
|
|
64
75
|
if (detail) {
|
|
65
76
|
if (!options.detailCases?.length)
|
|
66
77
|
checks.push({ name: "detail-cases-required", passed: false, error: "Advertised detail needs real cases" });
|