@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,637 @@
|
|
|
1
|
+
// The calibration tree worker: builds one release's or staged run's tree
|
|
2
|
+
// bundle from local copies of a build's files, and writes each part's gzip
|
|
3
|
+
// bytes under an output directory, at the path the publisher uploads it to.
|
|
4
|
+
// The collector runs it as its own Bun process:
|
|
5
|
+
//
|
|
6
|
+
// bun run src/calibration-tree-worker.ts REQUEST.json OUTPUT_DIRECTORY
|
|
7
|
+
//
|
|
8
|
+
// REQUEST.json names the release, where its files came from and where they
|
|
9
|
+
// are on disk:
|
|
10
|
+
//
|
|
11
|
+
// {
|
|
12
|
+
// "kind": "release",
|
|
13
|
+
// "country": "uk",
|
|
14
|
+
// "releaseId": "<release, tag or <release>/<build> folder>",
|
|
15
|
+
// "releaseDirectory": "<folder under releases/ the files were read from>",
|
|
16
|
+
// "hfRepo": "<owner>/<dataset>",
|
|
17
|
+
// "hfCommitSha": "<full commit SHA>",
|
|
18
|
+
// "createdAt": "<ISO 8601 publication time with Z or an offset>" or null,
|
|
19
|
+
// "artifacts": {
|
|
20
|
+
// "calibrationDiagnostics": {"path": "<path in the dataset>", "file": "<local file>"},
|
|
21
|
+
// "buildManifest": {...} or null,
|
|
22
|
+
// "releaseManifest": {...} or null,
|
|
23
|
+
// "demographics": {...} or null
|
|
24
|
+
// }
|
|
25
|
+
// }
|
|
26
|
+
//
|
|
27
|
+
// or a staged run's, built from a staged bundle's submitted files:
|
|
28
|
+
//
|
|
29
|
+
// {
|
|
30
|
+
// "kind": "staging",
|
|
31
|
+
// "country": "uk",
|
|
32
|
+
// "runId": "<the staged run>",
|
|
33
|
+
// "label": "<the candidate release, or the run id when it names none>",
|
|
34
|
+
// "hfRepo": ..., "hfCommitSha": ..., "createdAt": ..., "artifacts": {...}
|
|
35
|
+
// }
|
|
36
|
+
//
|
|
37
|
+
// A release bundle is built exactly as the publisher's `publishRelease` builds
|
|
38
|
+
// it from the same files, commit and publication time, so both write the same
|
|
39
|
+
// bytes: the golden tests hold each to the recorded trees. A staged run's is
|
|
40
|
+
// built as the publisher's `publishStagingSource` builds a staging tree, but
|
|
41
|
+
// from the files given here rather than the staging repo's run files. Like the
|
|
42
|
+
// publisher, it records the publication time as Date.prototype.toISOString()
|
|
43
|
+
// writes it. On success it prints one JSON line describing the written parts,
|
|
44
|
+
// with the manifest entry the publisher would record for the build, and exits
|
|
45
|
+
// 0. It writes nothing unless the whole bundle builds. The other exit codes:
|
|
46
|
+
//
|
|
47
|
+
// 1 the request could not be followed: a malformed request, a missing
|
|
48
|
+
// file, an output directory that is not empty;
|
|
49
|
+
// 2 the files cannot be built into a tree: the builder refused them, so
|
|
50
|
+
// running again on the same files cannot help;
|
|
51
|
+
// 3 anything else: a size limit configured as something other than a
|
|
52
|
+
// positive integer, or an unexpected error, such as a defect in the
|
|
53
|
+
// builder, which may succeed once fixed.
|
|
54
|
+
//
|
|
55
|
+
// The split between 2 and 3 follows the error's class, so it is approximate:
|
|
56
|
+
// the builder refuses most bad content with plain Errors (2), but a malformed
|
|
57
|
+
// row can also end in a TypeError (3), and the bundle's checks of its own
|
|
58
|
+
// output throw plain Errors (2) that only a defect should cause. A non-zero
|
|
59
|
+
// exit can leave parts already written when a write fails, so each attempt
|
|
60
|
+
// needs a new output directory.
|
|
61
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
62
|
+
import { dirname, join } from "node:path";
|
|
63
|
+
import { createHash } from "node:crypto";
|
|
64
|
+
|
|
65
|
+
import { CALIBRATION_TREE_SCHEMA_VERSION, type CalibrationTreeSourceArtifact } from "./calibration-tree-artifact";
|
|
66
|
+
import { buildCalibrationTreeBundle, type CalibrationTreeBundle } from "./calibration-tree-bundle";
|
|
67
|
+
import type { CalibrationTreeManifestEntry } from "./calibration-tree-manifest";
|
|
68
|
+
import { isCountry, type MicrocosmCountry } from "./countries";
|
|
69
|
+
import { assertSafeReleaseId, buildCalibration, InvalidReleaseIdError } from "./latest-artifact";
|
|
70
|
+
import { CalibrationTreeArtifactSizeError, enforceConfiguredSizeLimits } from "./publish-calibration-tree";
|
|
71
|
+
import { isBuildFolderId, isReleaseDirectory } from "./release-record";
|
|
72
|
+
import { IncompatibleStagingDataError } from "./staging-contract";
|
|
73
|
+
import { UnsupportedCalibrationDiagnosticsSchemaError } from "./target-representation";
|
|
74
|
+
|
|
75
|
+
type JsonObject = Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
export const CALIBRATION_TREE_WORKER_EXIT_REQUEST_ERROR = 1;
|
|
78
|
+
export const CALIBRATION_TREE_WORKER_EXIT_UNBUILDABLE_INPUT = 2;
|
|
79
|
+
export const CALIBRATION_TREE_WORKER_EXIT_WORKER_ERROR = 3;
|
|
80
|
+
|
|
81
|
+
const ARTIFACT_NAMES = ["calibrationDiagnostics", "buildManifest", "releaseManifest", "demographics"] as const;
|
|
82
|
+
type ArtifactName = (typeof ARTIFACT_NAMES)[number];
|
|
83
|
+
const REQUEST_FIELDS = [
|
|
84
|
+
"kind",
|
|
85
|
+
"country",
|
|
86
|
+
"releaseId",
|
|
87
|
+
"releaseDirectory",
|
|
88
|
+
"hfRepo",
|
|
89
|
+
"hfCommitSha",
|
|
90
|
+
"createdAt",
|
|
91
|
+
"artifacts",
|
|
92
|
+
] as const;
|
|
93
|
+
const STAGING_REQUEST_FIELDS = [
|
|
94
|
+
"kind",
|
|
95
|
+
"country",
|
|
96
|
+
"runId",
|
|
97
|
+
"label",
|
|
98
|
+
"hfRepo",
|
|
99
|
+
"hfCommitSha",
|
|
100
|
+
"createdAt",
|
|
101
|
+
"artifacts",
|
|
102
|
+
] as const;
|
|
103
|
+
const HF_COMMIT_RE = /^[0-9a-f]{40,64}$/;
|
|
104
|
+
// A date and time with seconds and an explicit offset, so the instant never
|
|
105
|
+
// depends on the machine's time zone.
|
|
106
|
+
const PUBLICATION_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
107
|
+
// The size limits enforceConfiguredSizeLimits reads.
|
|
108
|
+
const SIZE_LIMIT_VARIABLES = ["CALIBRATION_TREE_MAX_RAW_BYTES", "CALIBRATION_TREE_MAX_GZIP_BYTES"] as const;
|
|
109
|
+
const HF_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,95}\/[A-Za-z0-9][A-Za-z0-9._-]{0,95}$/;
|
|
110
|
+
|
|
111
|
+
export interface CalibrationTreeWorkerArtifact {
|
|
112
|
+
/** The file's path in the Hugging Face dataset, recorded in the index. */
|
|
113
|
+
path: string;
|
|
114
|
+
/** Where the file's bytes are on disk. */
|
|
115
|
+
file: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CalibrationTreeWorkerArtifacts {
|
|
119
|
+
calibrationDiagnostics: CalibrationTreeWorkerArtifact;
|
|
120
|
+
buildManifest: CalibrationTreeWorkerArtifact | null;
|
|
121
|
+
releaseManifest: CalibrationTreeWorkerArtifact | null;
|
|
122
|
+
demographics: CalibrationTreeWorkerArtifact | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CalibrationTreeWorkerReleaseRequest {
|
|
126
|
+
kind: "release";
|
|
127
|
+
country: MicrocosmCountry;
|
|
128
|
+
releaseId: string;
|
|
129
|
+
releaseDirectory: string;
|
|
130
|
+
hfRepo: string;
|
|
131
|
+
hfCommitSha: string;
|
|
132
|
+
createdAt: string | null;
|
|
133
|
+
artifacts: CalibrationTreeWorkerArtifacts;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A staged run, built from its staged bundle's files. */
|
|
137
|
+
export interface CalibrationTreeWorkerStagingRequest {
|
|
138
|
+
kind: "staging";
|
|
139
|
+
country: MicrocosmCountry;
|
|
140
|
+
runId: string;
|
|
141
|
+
/** The candidate release the run was staged for, or the run id. */
|
|
142
|
+
label: string;
|
|
143
|
+
hfRepo: string;
|
|
144
|
+
hfCommitSha: string;
|
|
145
|
+
createdAt: string | null;
|
|
146
|
+
artifacts: CalibrationTreeWorkerArtifacts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type CalibrationTreeWorkerRequest =
|
|
150
|
+
| CalibrationTreeWorkerReleaseRequest
|
|
151
|
+
| CalibrationTreeWorkerStagingRequest;
|
|
152
|
+
|
|
153
|
+
export interface CalibrationTreeWorkerPart {
|
|
154
|
+
part: string;
|
|
155
|
+
path: string;
|
|
156
|
+
sha256: string;
|
|
157
|
+
rawBytes: number;
|
|
158
|
+
gzipBytes: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface CalibrationTreeWorkerResult {
|
|
162
|
+
country: MicrocosmCountry;
|
|
163
|
+
/** The release id for a release; the label for a staged run. */
|
|
164
|
+
releaseId: string;
|
|
165
|
+
hfRepo: string;
|
|
166
|
+
hfCommitSha: string;
|
|
167
|
+
createdAt: string | null;
|
|
168
|
+
buildArtifactId: string;
|
|
169
|
+
index: Omit<CalibrationTreeWorkerPart, "part">;
|
|
170
|
+
files: CalibrationTreeWorkerPart[];
|
|
171
|
+
/** The entry the publisher would add to the manifest for this build. */
|
|
172
|
+
manifestEntry: CalibrationTreeManifestEntry;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The request could not be followed; nothing about the files is known. */
|
|
176
|
+
export class CalibrationTreeWorkerRequestError extends Error {}
|
|
177
|
+
|
|
178
|
+
/** The files were read but cannot be built into a tree. */
|
|
179
|
+
export class CalibrationTreeUnbuildableInputError extends Error {}
|
|
180
|
+
|
|
181
|
+
/** The worker is configured in a way the publisher would also refuse. */
|
|
182
|
+
export class CalibrationTreeWorkerConfigurationError extends Error {}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Whether an error from building the tree is the builder refusing the files.
|
|
186
|
+
* The builder refuses content with plain Errors and its own error classes;
|
|
187
|
+
* any other error, such as a TypeError, is treated as a defect, not as a
|
|
188
|
+
* judgement about the files.
|
|
189
|
+
*/
|
|
190
|
+
export function isCalibrationTreeContentRefusal(error: unknown): boolean {
|
|
191
|
+
if (!(error instanceof Error)) return false;
|
|
192
|
+
return error.constructor === Error ||
|
|
193
|
+
error instanceof CalibrationTreeArtifactSizeError ||
|
|
194
|
+
error instanceof IncompatibleStagingDataError ||
|
|
195
|
+
error instanceof UnsupportedCalibrationDiagnosticsSchemaError ||
|
|
196
|
+
error instanceof InvalidReleaseIdError;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The exit code for an error that stopped the worker. */
|
|
200
|
+
export function calibrationTreeWorkerExitCode(error: unknown): number {
|
|
201
|
+
if (error instanceof CalibrationTreeWorkerRequestError) return CALIBRATION_TREE_WORKER_EXIT_REQUEST_ERROR;
|
|
202
|
+
if (error instanceof CalibrationTreeUnbuildableInputError) return CALIBRATION_TREE_WORKER_EXIT_UNBUILDABLE_INPUT;
|
|
203
|
+
return CALIBRATION_TREE_WORKER_EXIT_WORKER_ERROR;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// enforceConfiguredSizeLimits refuses a limit that is not a positive integer
|
|
207
|
+
// with a plain Error, which would read as a refusal of the files, so the
|
|
208
|
+
// worker checks the same rule before it builds anything.
|
|
209
|
+
function checkConfiguredSizeLimits(): void {
|
|
210
|
+
for (const name of SIZE_LIMIT_VARIABLES) {
|
|
211
|
+
const configured = process.env[name]?.trim();
|
|
212
|
+
if (!configured) continue;
|
|
213
|
+
const limit = Number(configured);
|
|
214
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
215
|
+
throw new CalibrationTreeWorkerConfigurationError(`${name} must be a positive integer.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The publisher stores each publication time as toISOString() writes it.
|
|
221
|
+
function publicationTime(value: unknown): string | null {
|
|
222
|
+
if (value === null) return null;
|
|
223
|
+
const instant = typeof value === "string" && PUBLICATION_TIME_RE.test(value) ? Date.parse(value) : Number.NaN;
|
|
224
|
+
if (!Number.isFinite(instant)) {
|
|
225
|
+
throw new CalibrationTreeWorkerRequestError(
|
|
226
|
+
"createdAt must be an ISO 8601 date and time with Z or a UTC offset, or null.",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return new Date(instant).toISOString();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function requestObject(value: unknown, label: string): JsonObject {
|
|
233
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
234
|
+
throw new CalibrationTreeWorkerRequestError(`${label} must be a JSON object.`);
|
|
235
|
+
}
|
|
236
|
+
return value as JsonObject;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function onlyFields(value: JsonObject, fields: readonly string[], label: string): void {
|
|
240
|
+
const unknown = Object.keys(value).filter((field) => !fields.includes(field));
|
|
241
|
+
if (unknown.length > 0) {
|
|
242
|
+
throw new CalibrationTreeWorkerRequestError(`${label} has unknown fields: ${unknown.join(", ")}.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function requestString(value: JsonObject, field: string, label = field): string {
|
|
247
|
+
const text = value[field];
|
|
248
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
249
|
+
throw new CalibrationTreeWorkerRequestError(`${label} must be a non-empty string.`);
|
|
250
|
+
}
|
|
251
|
+
return text;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function releaseFolder(value: JsonObject, field: string): string {
|
|
255
|
+
const text = requestString(value, field);
|
|
256
|
+
// The publisher refuses the same values (releaseOrBuildFolderId).
|
|
257
|
+
if (!isReleaseDirectory(text) || text === "latest") {
|
|
258
|
+
throw new CalibrationTreeWorkerRequestError(`${field} must be a release id or a <release>/<build> folder.`);
|
|
259
|
+
}
|
|
260
|
+
return text;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function workerArtifact(value: unknown, name: ArtifactName): CalibrationTreeWorkerArtifact | null {
|
|
264
|
+
if (value === null && name !== "calibrationDiagnostics") return null;
|
|
265
|
+
const artifact = requestObject(value, `artifacts.${name}`);
|
|
266
|
+
onlyFields(artifact, ["path", "file"], `artifacts.${name}`);
|
|
267
|
+
return {
|
|
268
|
+
path: requestString(artifact, "path", `artifacts.${name}.path`),
|
|
269
|
+
file: requestString(artifact, "file", `artifacts.${name}.file`),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function stagedRunId(value: JsonObject): string {
|
|
274
|
+
const runId = requestString(value, "runId");
|
|
275
|
+
try {
|
|
276
|
+
if (runId === "latest") throw new InvalidReleaseIdError("Invalid run id");
|
|
277
|
+
return assertSafeReleaseId(runId, "run");
|
|
278
|
+
} catch {
|
|
279
|
+
throw new CalibrationTreeWorkerRequestError("runId must be a run id: letters, digits, dots, underscores and hyphens.");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Checks a request read from JSON, refusing anything the worker would guess at. */
|
|
284
|
+
export function parseCalibrationTreeWorkerRequest(value: unknown): CalibrationTreeWorkerRequest {
|
|
285
|
+
const request = requestObject(value, "The request");
|
|
286
|
+
if (request.kind !== "release" && request.kind !== "staging") {
|
|
287
|
+
throw new CalibrationTreeWorkerRequestError('kind must be "release" or "staging".');
|
|
288
|
+
}
|
|
289
|
+
onlyFields(request, request.kind === "release" ? REQUEST_FIELDS : STAGING_REQUEST_FIELDS, "The request");
|
|
290
|
+
const country = requestString(request, "country");
|
|
291
|
+
if (!isCountry(country)) {
|
|
292
|
+
throw new CalibrationTreeWorkerRequestError(`country ${country} is not a registered country.`);
|
|
293
|
+
}
|
|
294
|
+
const hfRepo = requestString(request, "hfRepo");
|
|
295
|
+
if (!HF_REPO_RE.test(hfRepo)) {
|
|
296
|
+
throw new CalibrationTreeWorkerRequestError("hfRepo must be an <owner>/<dataset> repository.");
|
|
297
|
+
}
|
|
298
|
+
const hfCommitSha = requestString(request, "hfCommitSha");
|
|
299
|
+
if (!HF_COMMIT_RE.test(hfCommitSha)) {
|
|
300
|
+
throw new CalibrationTreeWorkerRequestError("hfCommitSha must be a full lowercase commit SHA.");
|
|
301
|
+
}
|
|
302
|
+
const createdAt = publicationTime(request.createdAt);
|
|
303
|
+
const artifactFields = requestObject(request.artifacts, "artifacts");
|
|
304
|
+
onlyFields(artifactFields, ARTIFACT_NAMES, "artifacts");
|
|
305
|
+
for (const name of ARTIFACT_NAMES) {
|
|
306
|
+
if (!(name in artifactFields)) {
|
|
307
|
+
throw new CalibrationTreeWorkerRequestError(`artifacts.${name} is required (null when absent).`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const artifacts = {
|
|
311
|
+
calibrationDiagnostics: workerArtifact(artifactFields.calibrationDiagnostics, "calibrationDiagnostics")!,
|
|
312
|
+
buildManifest: workerArtifact(artifactFields.buildManifest, "buildManifest"),
|
|
313
|
+
releaseManifest: workerArtifact(artifactFields.releaseManifest, "releaseManifest"),
|
|
314
|
+
demographics: workerArtifact(artifactFields.demographics, "demographics"),
|
|
315
|
+
};
|
|
316
|
+
if (request.kind === "staging") {
|
|
317
|
+
return {
|
|
318
|
+
kind: "staging",
|
|
319
|
+
country,
|
|
320
|
+
runId: stagedRunId(request),
|
|
321
|
+
label: requestString(request, "label"),
|
|
322
|
+
hfRepo,
|
|
323
|
+
hfCommitSha,
|
|
324
|
+
createdAt,
|
|
325
|
+
artifacts,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
kind: "release",
|
|
330
|
+
country,
|
|
331
|
+
releaseId: releaseFolder(request, "releaseId"),
|
|
332
|
+
releaseDirectory: releaseFolder(request, "releaseDirectory"),
|
|
333
|
+
hfRepo,
|
|
334
|
+
hfCommitSha,
|
|
335
|
+
createdAt,
|
|
336
|
+
artifacts,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
interface ReadArtifact {
|
|
341
|
+
json: JsonObject;
|
|
342
|
+
source: CalibrationTreeSourceArtifact;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The publisher hashes and parses Response.text(): UTF-8, with a leading
|
|
346
|
+
// byte-order mark dropped and invalid bytes replaced. TextDecoder's defaults
|
|
347
|
+
// decode the same way, so a file gives the publisher's digest and document.
|
|
348
|
+
function readArtifact(artifact: CalibrationTreeWorkerArtifact | null): ReadArtifact | null {
|
|
349
|
+
if (artifact == null) return null;
|
|
350
|
+
let bytes: Uint8Array;
|
|
351
|
+
try {
|
|
352
|
+
bytes = readFileSync(artifact.file);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
throw new CalibrationTreeWorkerRequestError(
|
|
355
|
+
`${artifact.path} could not be read from ${artifact.file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const text = new TextDecoder("utf-8").decode(bytes);
|
|
359
|
+
let parsed: unknown;
|
|
360
|
+
try {
|
|
361
|
+
parsed = JSON.parse(text);
|
|
362
|
+
} catch {
|
|
363
|
+
throw new CalibrationTreeUnbuildableInputError(`${artifact.path} is not valid JSON.`);
|
|
364
|
+
}
|
|
365
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
366
|
+
throw new CalibrationTreeUnbuildableInputError(`${artifact.path} must be a JSON object.`);
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
json: parsed as JsonObject,
|
|
370
|
+
source: { path: artifact.path, sha256: createHash("sha256").update(text, "utf8").digest("hex") },
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Builds the requested bundle in memory: a release as the publisher's
|
|
376
|
+
* publishRelease does, a staged run as its publishStagingSource does.
|
|
377
|
+
*/
|
|
378
|
+
export function buildCalibrationTreeWorkerBundle(request: CalibrationTreeWorkerRequest): CalibrationTreeBundle {
|
|
379
|
+
const [diagnostics, buildManifest, releaseManifest, demographics] = ARTIFACT_NAMES.map((name) =>
|
|
380
|
+
readArtifact(request.artifacts[name])
|
|
381
|
+
);
|
|
382
|
+
if (!diagnostics) throw new CalibrationTreeWorkerRequestError("Calibration diagnostics are missing.");
|
|
383
|
+
try {
|
|
384
|
+
const sourceArtifacts = {
|
|
385
|
+
calibrationDiagnostics: diagnostics.source,
|
|
386
|
+
buildManifest: buildManifest?.source ?? null,
|
|
387
|
+
releaseManifest: releaseManifest?.source ?? null,
|
|
388
|
+
demographics: demographics?.source ?? null,
|
|
389
|
+
};
|
|
390
|
+
if (request.kind === "staging") {
|
|
391
|
+
return buildStagingBundle(request, sourceArtifacts, {
|
|
392
|
+
diagnostics: diagnostics.json,
|
|
393
|
+
buildManifest: buildManifest?.json ?? {},
|
|
394
|
+
releaseManifest: releaseManifest?.json ?? {},
|
|
395
|
+
demographics: demographics?.json ?? {},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return buildReleaseBundle(request, sourceArtifacts, {
|
|
399
|
+
diagnostics: diagnostics.json,
|
|
400
|
+
buildManifest: buildManifest?.json ?? {},
|
|
401
|
+
releaseManifest: releaseManifest?.json ?? {},
|
|
402
|
+
demographics: demographics?.json ?? {},
|
|
403
|
+
});
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (!isCalibrationTreeContentRefusal(error)) throw error;
|
|
406
|
+
throw new CalibrationTreeUnbuildableInputError((error as Error).message);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
interface WorkerDocuments {
|
|
411
|
+
diagnostics: JsonObject;
|
|
412
|
+
buildManifest: JsonObject;
|
|
413
|
+
releaseManifest: JsonObject;
|
|
414
|
+
demographics: JsonObject;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
type WorkerSourceArtifacts = Parameters<typeof buildCalibrationTreeBundle>[0]["sourceArtifacts"];
|
|
418
|
+
|
|
419
|
+
// As publishStagingSource builds a new staged run's tree: a staging build of
|
|
420
|
+
// the run, labelled and released under its candidate release, comparing
|
|
421
|
+
// under the release its diagnostics name.
|
|
422
|
+
function buildStagingBundle(
|
|
423
|
+
request: CalibrationTreeWorkerStagingRequest,
|
|
424
|
+
sourceArtifacts: WorkerSourceArtifacts,
|
|
425
|
+
documents: WorkerDocuments,
|
|
426
|
+
): CalibrationTreeBundle {
|
|
427
|
+
const { country, runId, label, hfRepo, hfCommitSha, createdAt } = request;
|
|
428
|
+
const calibration = buildCalibration(
|
|
429
|
+
documents.diagnostics,
|
|
430
|
+
label,
|
|
431
|
+
createdAt,
|
|
432
|
+
documents.buildManifest,
|
|
433
|
+
documents.releaseManifest,
|
|
434
|
+
documents.demographics,
|
|
435
|
+
country,
|
|
436
|
+
"huggingface_staged_bundle",
|
|
437
|
+
sourceArtifacts.calibrationDiagnostics?.sha256 ?? null,
|
|
438
|
+
hfCommitSha,
|
|
439
|
+
);
|
|
440
|
+
const bundle = buildCalibrationTreeBundle({
|
|
441
|
+
country,
|
|
442
|
+
buildKind: "staging",
|
|
443
|
+
sourceId: runId,
|
|
444
|
+
label,
|
|
445
|
+
createdAt,
|
|
446
|
+
releaseId: label,
|
|
447
|
+
hfRepo,
|
|
448
|
+
hfCommitSha,
|
|
449
|
+
sourceArtifacts,
|
|
450
|
+
rows: calibration.rows,
|
|
451
|
+
calibrationProvenance: calibration.calibration_provenance,
|
|
452
|
+
lossAttributionAvailable: calibration.target_loss_attribution.status !== "unavailable",
|
|
453
|
+
comparison: {
|
|
454
|
+
releaseId: calibration.release_id,
|
|
455
|
+
calibrationProvenance: calibration.calibration_provenance,
|
|
456
|
+
status: calibration.target_loss_attribution.status,
|
|
457
|
+
aggregate: calibration.target_loss_attribution.aggregate,
|
|
458
|
+
cap: calibration.target_loss_attribution.cap,
|
|
459
|
+
basisIdentifier: calibration.target_loss_attribution.basis_identifier,
|
|
460
|
+
targetRepresentation: calibration.target_schema.target_representation,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
for (const file of bundle.files) enforceConfiguredSizeLimits(file);
|
|
464
|
+
return bundle;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function buildReleaseBundle(
|
|
468
|
+
request: CalibrationTreeWorkerReleaseRequest,
|
|
469
|
+
sourceArtifacts: WorkerSourceArtifacts,
|
|
470
|
+
documents: WorkerDocuments,
|
|
471
|
+
): CalibrationTreeBundle {
|
|
472
|
+
const { country, releaseId: id, releaseDirectory, hfCommitSha } = request;
|
|
473
|
+
const calibration = buildCalibration(
|
|
474
|
+
documents.diagnostics,
|
|
475
|
+
id,
|
|
476
|
+
null,
|
|
477
|
+
documents.buildManifest,
|
|
478
|
+
documents.releaseManifest,
|
|
479
|
+
documents.demographics,
|
|
480
|
+
country,
|
|
481
|
+
"huggingface_immutable",
|
|
482
|
+
sourceArtifacts.calibrationDiagnostics?.sha256 ?? null,
|
|
483
|
+
hfCommitSha,
|
|
484
|
+
);
|
|
485
|
+
const bundle = buildCalibrationTreeBundle({
|
|
486
|
+
country,
|
|
487
|
+
createdAt: request.createdAt,
|
|
488
|
+
releaseId: id,
|
|
489
|
+
hfRepo: request.hfRepo,
|
|
490
|
+
hfCommitSha,
|
|
491
|
+
sourceArtifacts,
|
|
492
|
+
rows: calibration.rows,
|
|
493
|
+
calibrationProvenance: calibration.calibration_provenance,
|
|
494
|
+
lossAttributionAvailable: calibration.target_loss_attribution.status !== "unavailable",
|
|
495
|
+
comparison: {
|
|
496
|
+
// As in publishRelease: a tag or a build folder compares under its
|
|
497
|
+
// own id, a release folder under the release its diagnostics name.
|
|
498
|
+
releaseId: releaseDirectory === id && !isBuildFolderId(id) ? calibration.release_id : id,
|
|
499
|
+
calibrationProvenance: calibration.calibration_provenance,
|
|
500
|
+
status: calibration.target_loss_attribution.status,
|
|
501
|
+
aggregate: calibration.target_loss_attribution.aggregate,
|
|
502
|
+
cap: calibration.target_loss_attribution.cap,
|
|
503
|
+
basisIdentifier: calibration.target_loss_attribution.basis_identifier,
|
|
504
|
+
targetRepresentation: calibration.target_schema.target_representation,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
for (const file of bundle.files) enforceConfiguredSizeLimits(file);
|
|
508
|
+
return bundle;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// The entry the publisher adds to the manifest for the build: releaseManifestEntry
|
|
512
|
+
// for a release, publishStagingSource's new entry for a staged run.
|
|
513
|
+
function manifestEntry(
|
|
514
|
+
request: CalibrationTreeWorkerRequest,
|
|
515
|
+
bundle: CalibrationTreeBundle,
|
|
516
|
+
index: Omit<CalibrationTreeWorkerPart, "part">,
|
|
517
|
+
): CalibrationTreeManifestEntry {
|
|
518
|
+
const build = bundle.index.build;
|
|
519
|
+
const shared = {
|
|
520
|
+
buildArtifactId: build.buildArtifactId,
|
|
521
|
+
hfRepo: request.hfRepo,
|
|
522
|
+
hfCommitSha: request.hfCommitSha,
|
|
523
|
+
treeSchemaVersion: CALIBRATION_TREE_SCHEMA_VERSION,
|
|
524
|
+
indexSha256: index.sha256,
|
|
525
|
+
indexBytes: index.rawBytes,
|
|
526
|
+
createdAt: build.createdAt,
|
|
527
|
+
updatedAt: build.createdAt,
|
|
528
|
+
};
|
|
529
|
+
if (request.kind === "staging") {
|
|
530
|
+
return {
|
|
531
|
+
...shared,
|
|
532
|
+
kind: "staging",
|
|
533
|
+
sourceId: request.runId,
|
|
534
|
+
label: request.label,
|
|
535
|
+
releaseId: null,
|
|
536
|
+
stagingRunId: request.runId,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
...shared,
|
|
541
|
+
kind: "release",
|
|
542
|
+
sourceId: build.sourceId,
|
|
543
|
+
label: build.label,
|
|
544
|
+
releaseId: request.releaseId,
|
|
545
|
+
// Recorded only for a tag read from its release's folder.
|
|
546
|
+
...(request.releaseDirectory !== request.releaseId ? { releaseDirectory: request.releaseDirectory } : {}),
|
|
547
|
+
stagingRunId: null,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function emptyOutputDirectory(outputDirectory: string): void {
|
|
552
|
+
try {
|
|
553
|
+
mkdirSync(outputDirectory, { recursive: true });
|
|
554
|
+
if (readdirSync(outputDirectory).length > 0) {
|
|
555
|
+
throw new CalibrationTreeWorkerRequestError(`The output directory ${outputDirectory} must be empty.`);
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error instanceof CalibrationTreeWorkerRequestError) throw error;
|
|
559
|
+
throw new CalibrationTreeWorkerRequestError(
|
|
560
|
+
`The output directory ${outputDirectory} cannot be used: ${error instanceof Error ? error.message : String(error)}`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Builds the requested tree and writes its parts; returns what was written. */
|
|
566
|
+
export function runCalibrationTreeWorker(
|
|
567
|
+
request: CalibrationTreeWorkerRequest,
|
|
568
|
+
outputDirectory: string,
|
|
569
|
+
): CalibrationTreeWorkerResult {
|
|
570
|
+
checkConfiguredSizeLimits();
|
|
571
|
+
emptyOutputDirectory(outputDirectory);
|
|
572
|
+
const bundle = buildCalibrationTreeWorkerBundle(request);
|
|
573
|
+
for (const file of bundle.files) {
|
|
574
|
+
const destination = join(outputDirectory, file.path);
|
|
575
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
576
|
+
writeFileSync(destination, file.compressed, { flag: "wx" });
|
|
577
|
+
}
|
|
578
|
+
const files = bundle.files.map((file) => ({
|
|
579
|
+
part: file.part,
|
|
580
|
+
path: file.path,
|
|
581
|
+
sha256: file.sha256,
|
|
582
|
+
rawBytes: file.rawBytes,
|
|
583
|
+
gzipBytes: file.gzipBytes,
|
|
584
|
+
}));
|
|
585
|
+
const { part: _index, ...index } = files.find((file) => file.part === "index")!;
|
|
586
|
+
return {
|
|
587
|
+
country: request.country,
|
|
588
|
+
releaseId: request.kind === "staging" ? request.label : request.releaseId,
|
|
589
|
+
hfRepo: request.hfRepo,
|
|
590
|
+
hfCommitSha: request.hfCommitSha,
|
|
591
|
+
createdAt: request.createdAt,
|
|
592
|
+
buildArtifactId: bundle.index.buildArtifactId,
|
|
593
|
+
index,
|
|
594
|
+
files,
|
|
595
|
+
manifestEntry: manifestEntry(request, bundle, index),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function readRequestFile(requestPath: string): CalibrationTreeWorkerRequest {
|
|
600
|
+
let text: string;
|
|
601
|
+
try {
|
|
602
|
+
text = readFileSync(requestPath, "utf8");
|
|
603
|
+
} catch (error) {
|
|
604
|
+
throw new CalibrationTreeWorkerRequestError(
|
|
605
|
+
`The request ${requestPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
let parsed: unknown;
|
|
609
|
+
try {
|
|
610
|
+
parsed = JSON.parse(text);
|
|
611
|
+
} catch {
|
|
612
|
+
throw new CalibrationTreeWorkerRequestError(`The request ${requestPath} is not valid JSON.`);
|
|
613
|
+
}
|
|
614
|
+
return parseCalibrationTreeWorkerRequest(parsed);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** The worker's command line: returns the process exit code. */
|
|
618
|
+
export function calibrationTreeWorkerMain(argv: string[]): number {
|
|
619
|
+
try {
|
|
620
|
+
if (argv.length !== 2) {
|
|
621
|
+
throw new CalibrationTreeWorkerRequestError(
|
|
622
|
+
"Usage: bun run src/calibration-tree-worker.ts REQUEST.json OUTPUT_DIRECTORY",
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const [requestPath, outputDirectory] = argv;
|
|
626
|
+
const result = runCalibrationTreeWorker(readRequestFile(requestPath), outputDirectory);
|
|
627
|
+
console.log(JSON.stringify(result));
|
|
628
|
+
return 0;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
631
|
+
return calibrationTreeWorkerExitCode(error);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (import.meta.main) {
|
|
636
|
+
process.exitCode = calibrationTreeWorkerMain(Bun.argv.slice(2));
|
|
637
|
+
}
|