@policyengine/calibration-tree 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +58 -2
- package/package.json +73 -4
- package/src/calibration-build-comparison.ts +77 -0
- package/src/calibration-comparison-blob.ts +367 -0
- package/src/calibration-comparison-bundle.ts +149 -0
- package/src/calibration-explorer.ts +202 -0
- package/src/calibration-release-locator.ts +353 -0
- package/src/calibration-tree-artifact.ts +1974 -0
- package/src/calibration-tree-blob.ts +403 -0
- package/src/calibration-tree-bundle.ts +569 -0
- package/src/calibration-tree-manifest.ts +349 -0
- package/src/calibration-tree-publication.ts +399 -0
- package/src/calibration-tree-worker.ts +637 -0
- package/src/calibration-tree.ts +1284 -0
- package/src/chronicle-metadata.ts +42 -0
- package/src/countries.ts +355 -0
- package/src/geography-labels.ts +59 -0
- package/src/hierarchy-catalog-reader.ts +297 -0
- package/src/hierarchy-target-reader.ts +243 -0
- package/src/historical-weight-summary.ts +53 -0
- package/src/latest-artifact.ts +3767 -0
- package/src/legacy-target-reader.ts +337 -0
- package/src/program-label.ts +64 -0
- package/src/publish-calibration-tree.ts +1984 -0
- package/src/reforms.ts +224 -0
- package/src/release-cut.ts +32 -0
- package/src/release-record.ts +277 -0
- package/src/source-label.ts +5 -0
- package/src/source-labels.ts +71 -0
- package/src/staging-artifact.ts +1411 -0
- package/src/staging-contract.ts +916 -0
- package/src/structured-dimension-reader.ts +137 -0
- package/src/structured-target-reader.ts +76 -0
- package/src/target-change.ts +421 -0
- package/src/target-loss-attribution-manifest.ts +436 -0
- package/src/target-loss-attribution.ts +1012 -0
- package/src/target-representation.ts +69 -0
- package/src/target-surface-matcher.ts +266 -0
- package/types/next-fetch-cache.d.ts +8 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BlobPreconditionFailedError,
|
|
3
|
+
get,
|
|
4
|
+
list,
|
|
5
|
+
put,
|
|
6
|
+
type GetBlobResult,
|
|
7
|
+
type ListBlobResultBlob,
|
|
8
|
+
} from "@vercel/blob";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { gunzipSync } from "node:zlib";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
calibrationTreePartPath,
|
|
14
|
+
parseCalibrationTreeIndex,
|
|
15
|
+
type CalibrationTreeIndexArtifact,
|
|
16
|
+
type CalibrationTreePart,
|
|
17
|
+
} from "./calibration-tree-artifact";
|
|
18
|
+
import {
|
|
19
|
+
calibrationTreeBundleFile,
|
|
20
|
+
validateCalibrationTreeBundle,
|
|
21
|
+
type CalibrationTreeBundle,
|
|
22
|
+
type CalibrationTreeBundleFile,
|
|
23
|
+
} from "./calibration-tree-bundle";
|
|
24
|
+
import {
|
|
25
|
+
CALIBRATION_TREE_MANIFEST_PATH,
|
|
26
|
+
emptyCalibrationTreeManifest,
|
|
27
|
+
parseCalibrationTreeManifest,
|
|
28
|
+
serializeCalibrationTreeManifest,
|
|
29
|
+
withCalibrationTreeManifestEntry,
|
|
30
|
+
type CalibrationTreeManifest,
|
|
31
|
+
type CalibrationTreeManifestEntry,
|
|
32
|
+
} from "./calibration-tree-manifest";
|
|
33
|
+
import type { MicrocosmCountry } from "./countries";
|
|
34
|
+
|
|
35
|
+
const IMMUTABLE_CACHE_SECONDS = 31_536_000;
|
|
36
|
+
const MANIFEST_CACHE_SECONDS = 60;
|
|
37
|
+
|
|
38
|
+
export interface CalibrationTreeBlobClient {
|
|
39
|
+
get: typeof get;
|
|
40
|
+
put: typeof put;
|
|
41
|
+
list?: typeof list;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_BLOB_CLIENT: CalibrationTreeBlobClient = { get, put, list };
|
|
45
|
+
|
|
46
|
+
function isManifestWriteConflict(error: unknown): boolean {
|
|
47
|
+
return error instanceof BlobPreconditionFailedError ||
|
|
48
|
+
(error instanceof Error && /precondition failed.*etag mismatch/i.test(error.message));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function retryDelay(): Promise<void> {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, 100));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tokenOption(token?: string): { token: string } | Record<string, never> {
|
|
56
|
+
return token ? { token } : {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function streamText(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
60
|
+
if (!stream) throw new Error("Blob response did not include a body.");
|
|
61
|
+
return new Response(stream).text();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function calibrationTreeBlobText(
|
|
65
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
if (!stream) throw new Error("Compressed calibration tree response did not include a body.");
|
|
68
|
+
const compressed = await new Response(stream).arrayBuffer();
|
|
69
|
+
return gunzipSync(compressed).toString("utf8");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StoredCalibrationTreeManifest {
|
|
73
|
+
manifest: CalibrationTreeManifest;
|
|
74
|
+
etag: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function readCalibrationTreeManifest(options: {
|
|
78
|
+
token?: string;
|
|
79
|
+
consistent?: boolean;
|
|
80
|
+
client?: CalibrationTreeBlobClient;
|
|
81
|
+
} = {}): Promise<StoredCalibrationTreeManifest> {
|
|
82
|
+
const result = await (options.client ?? DEFAULT_BLOB_CLIENT).get(
|
|
83
|
+
CALIBRATION_TREE_MANIFEST_PATH,
|
|
84
|
+
{
|
|
85
|
+
access: "private",
|
|
86
|
+
useCache: options.consistent === false,
|
|
87
|
+
...tokenOption(options.token),
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
if (!result) {
|
|
91
|
+
return { manifest: emptyCalibrationTreeManifest(), etag: null };
|
|
92
|
+
}
|
|
93
|
+
if (result.statusCode !== 200) {
|
|
94
|
+
throw new Error(`Unexpected calibration tree manifest status ${result.statusCode}.`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
manifest: parseCalibrationTreeManifest(JSON.parse(await streamText(result.stream))),
|
|
98
|
+
etag: result.blob.etag,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function getCalibrationTreeBlob(options: {
|
|
103
|
+
country: MicrocosmCountry;
|
|
104
|
+
buildArtifactId: string;
|
|
105
|
+
part: CalibrationTreePart;
|
|
106
|
+
token?: string;
|
|
107
|
+
consistent?: boolean;
|
|
108
|
+
ifNoneMatch?: string;
|
|
109
|
+
client?: CalibrationTreeBlobClient;
|
|
110
|
+
}): Promise<GetBlobResult | null> {
|
|
111
|
+
return (options.client ?? DEFAULT_BLOB_CLIENT).get(
|
|
112
|
+
calibrationTreePartPath(
|
|
113
|
+
options.country,
|
|
114
|
+
options.buildArtifactId,
|
|
115
|
+
options.part,
|
|
116
|
+
),
|
|
117
|
+
{
|
|
118
|
+
access: "private",
|
|
119
|
+
useCache: options.consistent === true ? false : true,
|
|
120
|
+
ifNoneMatch: options.ifNoneMatch,
|
|
121
|
+
...tokenOption(options.token),
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function listCalibrationTreeBlobs(options: {
|
|
127
|
+
country: MicrocosmCountry;
|
|
128
|
+
token?: string;
|
|
129
|
+
client?: CalibrationTreeBlobClient;
|
|
130
|
+
}): Promise<Map<string, ListBlobResultBlob>> {
|
|
131
|
+
const client = options.client ?? DEFAULT_BLOB_CLIENT;
|
|
132
|
+
if (!client.list) throw new Error("Calibration tree Blob listing is unavailable.");
|
|
133
|
+
const blobs = new Map<string, ListBlobResultBlob>();
|
|
134
|
+
let cursor: string | undefined;
|
|
135
|
+
do {
|
|
136
|
+
const page = await client.list({
|
|
137
|
+
prefix: `calibration-trees/${options.country}/`,
|
|
138
|
+
limit: 1_000,
|
|
139
|
+
...(cursor ? { cursor } : {}),
|
|
140
|
+
...tokenOption(options.token),
|
|
141
|
+
});
|
|
142
|
+
for (const blob of page.blobs) blobs.set(blob.pathname, blob);
|
|
143
|
+
cursor = page.hasMore ? page.cursor : undefined;
|
|
144
|
+
if (page.hasMore && !cursor) {
|
|
145
|
+
throw new Error("Calibration tree Blob listing omitted its next cursor.");
|
|
146
|
+
}
|
|
147
|
+
} while (cursor);
|
|
148
|
+
return blobs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface CalibrationTreeBuildAudit {
|
|
152
|
+
complete: boolean;
|
|
153
|
+
repairable: boolean;
|
|
154
|
+
reasons: string[];
|
|
155
|
+
index: CalibrationTreeIndexArtifact | null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function auditCalibrationTreeBuild(options: {
|
|
159
|
+
country: MicrocosmCountry;
|
|
160
|
+
entry: CalibrationTreeManifestEntry;
|
|
161
|
+
blobs: Map<string, ListBlobResultBlob>;
|
|
162
|
+
token?: string;
|
|
163
|
+
client?: CalibrationTreeBlobClient;
|
|
164
|
+
}): Promise<CalibrationTreeBuildAudit> {
|
|
165
|
+
const reasons: string[] = [];
|
|
166
|
+
const indexResult = await getCalibrationTreeBlob({
|
|
167
|
+
country: options.country,
|
|
168
|
+
buildArtifactId: options.entry.buildArtifactId,
|
|
169
|
+
part: "index",
|
|
170
|
+
token: options.token,
|
|
171
|
+
consistent: true,
|
|
172
|
+
client: options.client,
|
|
173
|
+
});
|
|
174
|
+
if (!indexResult) {
|
|
175
|
+
return {
|
|
176
|
+
complete: false,
|
|
177
|
+
repairable: true,
|
|
178
|
+
reasons: ["index is missing"],
|
|
179
|
+
index: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (indexResult.statusCode !== 200) {
|
|
183
|
+
return {
|
|
184
|
+
complete: false,
|
|
185
|
+
repairable: false,
|
|
186
|
+
reasons: [`index returned status ${indexResult.statusCode}`],
|
|
187
|
+
index: null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let index: CalibrationTreeIndexArtifact | null = null;
|
|
192
|
+
try {
|
|
193
|
+
const text = await calibrationTreeBlobText(indexResult.stream);
|
|
194
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
195
|
+
const sha256 = createHash("sha256").update(text, "utf8").digest("hex");
|
|
196
|
+
if (bytes !== options.entry.indexBytes) reasons.push("index raw byte count differs");
|
|
197
|
+
if (sha256 !== options.entry.indexSha256) reasons.push("index digest differs");
|
|
198
|
+
index = parseCalibrationTreeIndex(JSON.parse(text));
|
|
199
|
+
if (index.country !== options.country) reasons.push("index country differs");
|
|
200
|
+
if (index.buildArtifactId !== options.entry.buildArtifactId) {
|
|
201
|
+
reasons.push("index build id differs");
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
reasons.push(
|
|
205
|
+
`index cannot be parsed: ${error instanceof Error ? error.message : String(error)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!index) {
|
|
209
|
+
return { complete: false, repairable: false, reasons, index: null };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const descriptors = [
|
|
213
|
+
index.parts.filterIndex,
|
|
214
|
+
...index.parts.targetSummaries,
|
|
215
|
+
...index.parts.targetDetails,
|
|
216
|
+
...index.parts.tiers,
|
|
217
|
+
];
|
|
218
|
+
for (const descriptor of descriptors) {
|
|
219
|
+
const blob = options.blobs.get(descriptor.path);
|
|
220
|
+
if (!blob) {
|
|
221
|
+
reasons.push(`missing ${descriptor.path}`);
|
|
222
|
+
} else if (blob.size !== descriptor.gzipBytes) {
|
|
223
|
+
reasons.push(
|
|
224
|
+
`compressed byte count differs for ${descriptor.path}: ${blob.size} != ${descriptor.gzipBytes}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
complete: reasons.length === 0,
|
|
230
|
+
repairable: reasons.every((reason) => reason.startsWith("missing ")),
|
|
231
|
+
reasons,
|
|
232
|
+
index,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function uploadCalibrationTreeFile(options: {
|
|
237
|
+
file: CalibrationTreeBundleFile;
|
|
238
|
+
country: MicrocosmCountry;
|
|
239
|
+
buildArtifactId: string;
|
|
240
|
+
token: string;
|
|
241
|
+
client: CalibrationTreeBlobClient;
|
|
242
|
+
}): Promise<{ pathname: string; sha256: string; bytes: number; created: boolean }> {
|
|
243
|
+
const { file, country, buildArtifactId, token, client } = options;
|
|
244
|
+
const existing = await getCalibrationTreeBlob({
|
|
245
|
+
country,
|
|
246
|
+
buildArtifactId,
|
|
247
|
+
part: file.part,
|
|
248
|
+
token,
|
|
249
|
+
consistent: true,
|
|
250
|
+
client,
|
|
251
|
+
});
|
|
252
|
+
if (existing) {
|
|
253
|
+
const existingText = await calibrationTreeBlobText(existing.stream);
|
|
254
|
+
const verified = new TextEncoder().encode(existingText);
|
|
255
|
+
const expected = new TextEncoder().encode(file.serialized);
|
|
256
|
+
if (
|
|
257
|
+
verified.length !== expected.length ||
|
|
258
|
+
verified.some((byte, index) => byte !== expected[index])
|
|
259
|
+
) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Immutable calibration tree part ${file.path} already exists with different content.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
pathname: file.path,
|
|
266
|
+
sha256: file.sha256,
|
|
267
|
+
bytes: file.rawBytes,
|
|
268
|
+
created: false,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await client.put(file.path, Buffer.from(file.compressed), {
|
|
273
|
+
access: "private",
|
|
274
|
+
token,
|
|
275
|
+
addRandomSuffix: false,
|
|
276
|
+
multipart: true,
|
|
277
|
+
contentType: "application/gzip",
|
|
278
|
+
cacheControlMaxAge: IMMUTABLE_CACHE_SECONDS,
|
|
279
|
+
});
|
|
280
|
+
const uploaded = await getCalibrationTreeBlob({
|
|
281
|
+
country,
|
|
282
|
+
buildArtifactId,
|
|
283
|
+
part: file.part,
|
|
284
|
+
token,
|
|
285
|
+
consistent: true,
|
|
286
|
+
client,
|
|
287
|
+
});
|
|
288
|
+
if (!uploaded || await calibrationTreeBlobText(uploaded.stream) !== file.serialized) {
|
|
289
|
+
throw new Error(`Uploaded calibration tree part ${file.path} failed verification.`);
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
pathname: file.path,
|
|
293
|
+
sha256: file.sha256,
|
|
294
|
+
bytes: file.rawBytes,
|
|
295
|
+
created: true,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function uploadCalibrationTreeBundle(options: {
|
|
300
|
+
bundle: CalibrationTreeBundle;
|
|
301
|
+
token: string;
|
|
302
|
+
client?: CalibrationTreeBlobClient;
|
|
303
|
+
}): Promise<{
|
|
304
|
+
files: Array<{ pathname: string; sha256: string; bytes: number; created: boolean }>;
|
|
305
|
+
index: { pathname: string; sha256: string; bytes: number; created: boolean };
|
|
306
|
+
}> {
|
|
307
|
+
const { bundle, token, client = DEFAULT_BLOB_CLIENT } = options;
|
|
308
|
+
validateCalibrationTreeBundle(bundle);
|
|
309
|
+
const uploaded = [];
|
|
310
|
+
for (const file of bundle.files) {
|
|
311
|
+
uploaded.push(await uploadCalibrationTreeFile({
|
|
312
|
+
file,
|
|
313
|
+
country: bundle.index.country,
|
|
314
|
+
buildArtifactId: bundle.index.buildArtifactId,
|
|
315
|
+
token,
|
|
316
|
+
client,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
files: uploaded,
|
|
321
|
+
index: uploaded[bundle.files.indexOf(calibrationTreeBundleFile(bundle, "index"))],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function updateCalibrationTreeManifest(options: {
|
|
326
|
+
country: MicrocosmCountry;
|
|
327
|
+
entry: CalibrationTreeManifestEntry;
|
|
328
|
+
makeLatest?: boolean;
|
|
329
|
+
token: string;
|
|
330
|
+
client?: CalibrationTreeBlobClient;
|
|
331
|
+
}): Promise<CalibrationTreeManifest> {
|
|
332
|
+
const client = options.client ?? DEFAULT_BLOB_CLIENT;
|
|
333
|
+
let current = await readCalibrationTreeManifest({
|
|
334
|
+
token: options.token,
|
|
335
|
+
consistent: true,
|
|
336
|
+
client,
|
|
337
|
+
});
|
|
338
|
+
let updated = withCalibrationTreeManifestEntry(
|
|
339
|
+
current.manifest,
|
|
340
|
+
options.country,
|
|
341
|
+
options.entry,
|
|
342
|
+
options.makeLatest,
|
|
343
|
+
);
|
|
344
|
+
try {
|
|
345
|
+
await client.put(
|
|
346
|
+
CALIBRATION_TREE_MANIFEST_PATH,
|
|
347
|
+
serializeCalibrationTreeManifest(updated),
|
|
348
|
+
{
|
|
349
|
+
access: "private",
|
|
350
|
+
token: options.token,
|
|
351
|
+
addRandomSuffix: false,
|
|
352
|
+
allowOverwrite: current.etag != null,
|
|
353
|
+
ifMatch: current.etag ?? undefined,
|
|
354
|
+
contentType: "application/json; charset=utf-8",
|
|
355
|
+
cacheControlMaxAge: MANIFEST_CACHE_SECONDS,
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (!isManifestWriteConflict(error)) throw error;
|
|
360
|
+
|
|
361
|
+
await retryDelay();
|
|
362
|
+
current = await readCalibrationTreeManifest({
|
|
363
|
+
token: options.token,
|
|
364
|
+
consistent: true,
|
|
365
|
+
client,
|
|
366
|
+
});
|
|
367
|
+
updated = withCalibrationTreeManifestEntry(
|
|
368
|
+
current.manifest,
|
|
369
|
+
options.country,
|
|
370
|
+
options.entry,
|
|
371
|
+
options.makeLatest,
|
|
372
|
+
);
|
|
373
|
+
await client.put(
|
|
374
|
+
CALIBRATION_TREE_MANIFEST_PATH,
|
|
375
|
+
serializeCalibrationTreeManifest(updated),
|
|
376
|
+
{
|
|
377
|
+
access: "private",
|
|
378
|
+
token: options.token,
|
|
379
|
+
addRandomSuffix: false,
|
|
380
|
+
allowOverwrite: true,
|
|
381
|
+
contentType: "application/json; charset=utf-8",
|
|
382
|
+
cacheControlMaxAge: MANIFEST_CACHE_SECONDS,
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const verified = await readCalibrationTreeManifest({
|
|
388
|
+
token: options.token,
|
|
389
|
+
consistent: true,
|
|
390
|
+
client,
|
|
391
|
+
});
|
|
392
|
+
const verifiedEntry = verified.manifest.countries[options.country]?.builds.find(
|
|
393
|
+
(entry) => entry.buildArtifactId === options.entry.buildArtifactId,
|
|
394
|
+
);
|
|
395
|
+
if (
|
|
396
|
+
!verifiedEntry ||
|
|
397
|
+
verifiedEntry.hfCommitSha !== options.entry.hfCommitSha ||
|
|
398
|
+
verifiedEntry.indexSha256 !== options.entry.indexSha256
|
|
399
|
+
) {
|
|
400
|
+
throw new Error("Calibration tree manifest verification failed.");
|
|
401
|
+
}
|
|
402
|
+
return verified.manifest;
|
|
403
|
+
}
|