@policyengine/calibration-tree 0.1.0 → 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@policyengine/calibration-tree",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Builds, validates, reads and publishes Microcosm calibration tree artifacts (tree schema 6), with the release and staging readers they are built from.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"./calibration-tree-blob": "./src/calibration-tree-blob.ts",
|
|
31
31
|
"./calibration-tree-bundle": "./src/calibration-tree-bundle.ts",
|
|
32
32
|
"./calibration-tree-manifest": "./src/calibration-tree-manifest.ts",
|
|
33
|
+
"./calibration-tree-publication": "./src/calibration-tree-publication.ts",
|
|
33
34
|
"./calibration-tree-worker": "./src/calibration-tree-worker.ts",
|
|
34
35
|
"./chronicle-metadata": "./src/chronicle-metadata.ts",
|
|
35
36
|
"./countries": "./src/countries.ts",
|
|
@@ -60,7 +61,8 @@
|
|
|
60
61
|
"publish:calibration-tree": "bun run src/publish-calibration-tree.ts",
|
|
61
62
|
"test": "bun test",
|
|
62
63
|
"typecheck": "tsc --noEmit",
|
|
63
|
-
"worker": "bun run src/calibration-tree-worker.ts"
|
|
64
|
+
"worker": "bun run src/calibration-tree-worker.ts",
|
|
65
|
+
"publication": "bun run src/calibration-tree-publication.ts"
|
|
64
66
|
},
|
|
65
67
|
"dependencies": {
|
|
66
68
|
"@vercel/blob": "^2.6.1"
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// Publishes a tree the calibration tree worker wrote to disk: uploads each of
|
|
2
|
+
// its gzip parts to the part's immutable path, then adds the build's entry to
|
|
3
|
+
// the manifest with a compare-and-swap. The collector runs it as its own Bun
|
|
4
|
+
// process, after the worker and under its one manifest lock:
|
|
5
|
+
//
|
|
6
|
+
// bun run src/calibration-tree-publication.ts PUBLICATION.json
|
|
7
|
+
//
|
|
8
|
+
// with the store's read-write token in CALIBRATION_TREE_BLOB_READ_WRITE_TOKEN
|
|
9
|
+
// and PUBLICATION.json holding:
|
|
10
|
+
//
|
|
11
|
+
// {
|
|
12
|
+
// "bundleDirectory": "<the worker's output directory>",
|
|
13
|
+
// "worker": <the worker's printed result>,
|
|
14
|
+
// "makeLatest": false
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// Each part is read from disk and checked against the worker's record of its
|
|
18
|
+
// raw bytes before anything is uploaded. A part already stored with the same
|
|
19
|
+
// bytes is kept; one stored with other bytes stops the publication. The index
|
|
20
|
+
// goes last. The manifest is created only if it is still absent, and an
|
|
21
|
+
// existing one is replaced only if its ETag is still the one read, so a
|
|
22
|
+
// manifest another writer changed is never overwritten. A build asked to
|
|
23
|
+
// become the country's latest does so only when it is at least as new
|
|
24
|
+
// (createdAt) as the current latest, so a retried or reprocessed older build
|
|
25
|
+
// never takes the latest back. On success it prints one JSON line and exits
|
|
26
|
+
// 0. The other exit codes:
|
|
27
|
+
//
|
|
28
|
+
// 1 the request could not be followed: no token, a malformed request, or a
|
|
29
|
+
// part on disk that differs from the worker's record;
|
|
30
|
+
// 2 the build is refused: another build is already recorded for the same
|
|
31
|
+
// release or staged run, or the same build with other content (another
|
|
32
|
+
// submission of the same files, whose index records another time or
|
|
33
|
+
// label), or a staging build was to become the latest release; running
|
|
34
|
+
// again on the same tree cannot help;
|
|
35
|
+
// 3 anything else, including a manifest another writer changed meanwhile
|
|
36
|
+
// and any error from the store; running again may succeed.
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
38
|
+
import { readFileSync } from "node:fs";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
import { gunzipSync } from "node:zlib";
|
|
41
|
+
import { get, put } from "@vercel/blob";
|
|
42
|
+
|
|
43
|
+
import { calibrationTreePartPath, type CalibrationTreePart } from "./calibration-tree-artifact";
|
|
44
|
+
import {
|
|
45
|
+
calibrationTreeBlobText,
|
|
46
|
+
getCalibrationTreeBlob,
|
|
47
|
+
readCalibrationTreeManifest,
|
|
48
|
+
type CalibrationTreeBlobClient,
|
|
49
|
+
} from "./calibration-tree-blob";
|
|
50
|
+
import {
|
|
51
|
+
CALIBRATION_TREE_MANIFEST_PATH,
|
|
52
|
+
CALIBRATION_TREE_MANIFEST_SCHEMA_VERSION,
|
|
53
|
+
parseCalibrationTreeManifest,
|
|
54
|
+
serializeCalibrationTreeManifest,
|
|
55
|
+
withCalibrationTreeManifestEntry,
|
|
56
|
+
type CalibrationTreeManifest,
|
|
57
|
+
type CalibrationTreeManifestEntry,
|
|
58
|
+
} from "./calibration-tree-manifest";
|
|
59
|
+
import { isCountry, type MicrocosmCountry } from "./countries";
|
|
60
|
+
|
|
61
|
+
export const CALIBRATION_TREE_PUBLICATION_EXIT_REQUEST_ERROR = 1;
|
|
62
|
+
export const CALIBRATION_TREE_PUBLICATION_EXIT_ENTRY_REFUSED = 2;
|
|
63
|
+
export const CALIBRATION_TREE_PUBLICATION_EXIT_PUBLICATION_ERROR = 3;
|
|
64
|
+
export const CALIBRATION_TREE_PUBLICATION_TOKEN_VARIABLE = "CALIBRATION_TREE_BLOB_READ_WRITE_TOKEN";
|
|
65
|
+
|
|
66
|
+
// As the package's own uploads write them.
|
|
67
|
+
const IMMUTABLE_CACHE_SECONDS = 31_536_000;
|
|
68
|
+
const MANIFEST_CACHE_SECONDS = 60;
|
|
69
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
70
|
+
|
|
71
|
+
/** The request could not be followed; nothing was published. */
|
|
72
|
+
export class CalibrationTreePublicationRequestError extends Error {}
|
|
73
|
+
|
|
74
|
+
/** The build is refused; nothing in the manifest changed. */
|
|
75
|
+
export class CalibrationTreeEntryRefusedError extends Error {}
|
|
76
|
+
|
|
77
|
+
/** Another writer changed the manifest after it was read; nothing was overwritten. */
|
|
78
|
+
export class CalibrationTreeManifestConflictError extends Error {}
|
|
79
|
+
|
|
80
|
+
export interface CalibrationTreePublicationPart {
|
|
81
|
+
part: CalibrationTreePart;
|
|
82
|
+
path: string;
|
|
83
|
+
sha256: string;
|
|
84
|
+
rawBytes: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CalibrationTreePublicationRequest {
|
|
88
|
+
bundleDirectory: string;
|
|
89
|
+
country: MicrocosmCountry;
|
|
90
|
+
buildArtifactId: string;
|
|
91
|
+
files: CalibrationTreePublicationPart[];
|
|
92
|
+
manifestEntry: CalibrationTreeManifestEntry;
|
|
93
|
+
makeLatest: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CalibrationTreePublicationResult {
|
|
97
|
+
buildArtifactId: string;
|
|
98
|
+
kind: CalibrationTreeManifestEntry["kind"];
|
|
99
|
+
uploadedParts: number;
|
|
100
|
+
keptParts: number;
|
|
101
|
+
madeLatest: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type JsonObject = Record<string, unknown>;
|
|
105
|
+
|
|
106
|
+
function requestObject(value: unknown, label: string): JsonObject {
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
108
|
+
throw new CalibrationTreePublicationRequestError(`${label} must be a JSON object.`);
|
|
109
|
+
}
|
|
110
|
+
return value as JsonObject;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requestString(value: JsonObject, field: string, label: string): string {
|
|
114
|
+
const text = value[field];
|
|
115
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
116
|
+
throw new CalibrationTreePublicationRequestError(`${label} must be a non-empty string.`);
|
|
117
|
+
}
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function publicationPart(value: unknown, country: MicrocosmCountry, buildArtifactId: string): CalibrationTreePublicationPart {
|
|
122
|
+
const file = requestObject(value, "worker.files[]");
|
|
123
|
+
const part = requestString(file, "part", "worker.files[].part") as CalibrationTreePart;
|
|
124
|
+
const path = requestString(file, "path", "worker.files[].path");
|
|
125
|
+
const sha256 = requestString(file, "sha256", "worker.files[].sha256");
|
|
126
|
+
const rawBytes = file.rawBytes;
|
|
127
|
+
let expectedPath: string;
|
|
128
|
+
try {
|
|
129
|
+
expectedPath = calibrationTreePartPath(country, buildArtifactId, part);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new CalibrationTreePublicationRequestError(`worker.files[] names an unknown part ${part}.`);
|
|
132
|
+
}
|
|
133
|
+
// A part is only ever written to its own immutable path.
|
|
134
|
+
if (path !== expectedPath) {
|
|
135
|
+
throw new CalibrationTreePublicationRequestError(`Part ${part} must be stored at ${expectedPath}.`);
|
|
136
|
+
}
|
|
137
|
+
if (!SHA256_RE.test(sha256) || !Number.isSafeInteger(rawBytes) || (rawBytes as number) < 0) {
|
|
138
|
+
throw new CalibrationTreePublicationRequestError(`Part ${part} needs a SHA-256 and a raw size.`);
|
|
139
|
+
}
|
|
140
|
+
return { part, path, sha256, rawBytes: rawBytes as number };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Checks a publication request read from JSON. */
|
|
144
|
+
export function parseCalibrationTreePublicationRequest(value: unknown): CalibrationTreePublicationRequest {
|
|
145
|
+
const request = requestObject(value, "The request");
|
|
146
|
+
const unknown = Object.keys(request).filter((field) => !["bundleDirectory", "worker", "makeLatest"].includes(field));
|
|
147
|
+
if (unknown.length > 0) {
|
|
148
|
+
throw new CalibrationTreePublicationRequestError(`The request has unknown fields: ${unknown.join(", ")}.`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof request.makeLatest !== "boolean") {
|
|
151
|
+
throw new CalibrationTreePublicationRequestError("makeLatest must be true or false.");
|
|
152
|
+
}
|
|
153
|
+
const bundleDirectory = requestString(request, "bundleDirectory", "bundleDirectory");
|
|
154
|
+
const worker = requestObject(request.worker, "worker");
|
|
155
|
+
const country = requestString(worker, "country", "worker.country");
|
|
156
|
+
if (!isCountry(country)) {
|
|
157
|
+
throw new CalibrationTreePublicationRequestError(`worker.country ${country} is not a registered country.`);
|
|
158
|
+
}
|
|
159
|
+
const buildArtifactId = requestString(worker, "buildArtifactId", "worker.buildArtifactId");
|
|
160
|
+
if (!SHA256_RE.test(buildArtifactId)) {
|
|
161
|
+
throw new CalibrationTreePublicationRequestError("worker.buildArtifactId must be a build ID.");
|
|
162
|
+
}
|
|
163
|
+
if (!Array.isArray(worker.files) || worker.files.length === 0) {
|
|
164
|
+
throw new CalibrationTreePublicationRequestError("worker.files must list the tree's parts.");
|
|
165
|
+
}
|
|
166
|
+
const files = worker.files.map((file) => publicationPart(file, country, buildArtifactId));
|
|
167
|
+
if (files.filter((file) => file.part === "index").length !== 1) {
|
|
168
|
+
throw new CalibrationTreePublicationRequestError("worker.files must hold exactly one index.");
|
|
169
|
+
}
|
|
170
|
+
const manifestEntry = requestObject(worker.manifestEntry, "worker.manifestEntry") as unknown as CalibrationTreeManifestEntry;
|
|
171
|
+
// The manifest's own rules, checked before anything is uploaded: a
|
|
172
|
+
// malformed entry is a defect in the request, not a refusal of the build.
|
|
173
|
+
try {
|
|
174
|
+
parseCalibrationTreeManifest({
|
|
175
|
+
schemaVersion: CALIBRATION_TREE_MANIFEST_SCHEMA_VERSION,
|
|
176
|
+
countries: { [country]: { latestReleaseBuildArtifactId: null, builds: [manifestEntry] } },
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new CalibrationTreePublicationRequestError(
|
|
180
|
+
`worker.manifestEntry is not a manifest entry: ${error instanceof Error ? error.message : String(error)}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const index = files.find((file) => file.part === "index")!;
|
|
184
|
+
if (
|
|
185
|
+
manifestEntry.buildArtifactId !== buildArtifactId ||
|
|
186
|
+
manifestEntry.indexSha256 !== index.sha256 ||
|
|
187
|
+
manifestEntry.indexBytes !== index.rawBytes
|
|
188
|
+
) {
|
|
189
|
+
throw new CalibrationTreePublicationRequestError("worker.manifestEntry does not describe the tree's index.");
|
|
190
|
+
}
|
|
191
|
+
return { bundleDirectory, country, buildArtifactId, files, manifestEntry, makeLatest: request.makeLatest };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface PartOnDisk extends CalibrationTreePublicationPart {
|
|
195
|
+
compressed: Uint8Array;
|
|
196
|
+
serialized: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Every part is read and checked before anything is uploaded, so a damaged
|
|
200
|
+
// output directory publishes nothing.
|
|
201
|
+
function partsOnDisk(request: CalibrationTreePublicationRequest): PartOnDisk[] {
|
|
202
|
+
return request.files.map((file) => {
|
|
203
|
+
let compressed: Uint8Array;
|
|
204
|
+
let raw: Uint8Array;
|
|
205
|
+
try {
|
|
206
|
+
compressed = new Uint8Array(readFileSync(join(request.bundleDirectory, file.path)));
|
|
207
|
+
raw = new Uint8Array(gunzipSync(compressed));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw new CalibrationTreePublicationRequestError(
|
|
210
|
+
`Part ${file.part} could not be read from ${request.bundleDirectory}: ${error instanceof Error ? error.message : String(error)}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (raw.byteLength !== file.rawBytes || createHash("sha256").update(raw).digest("hex") !== file.sha256) {
|
|
214
|
+
throw new CalibrationTreePublicationRequestError(`Part ${file.part} on disk differs from the worker's record.`);
|
|
215
|
+
}
|
|
216
|
+
return { ...file, compressed, serialized: new TextDecoder("utf-8").decode(raw) };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// As the package's uploadCalibrationTreeFile: an existing part must hold the
|
|
221
|
+
// same raw JSON, and an uploaded one is read back.
|
|
222
|
+
async function uploadPart(
|
|
223
|
+
part: PartOnDisk,
|
|
224
|
+
request: CalibrationTreePublicationRequest,
|
|
225
|
+
token: string,
|
|
226
|
+
client: CalibrationTreeBlobClient,
|
|
227
|
+
): Promise<boolean> {
|
|
228
|
+
const stored = () =>
|
|
229
|
+
getCalibrationTreeBlob({
|
|
230
|
+
country: request.country,
|
|
231
|
+
buildArtifactId: request.buildArtifactId,
|
|
232
|
+
part: part.part,
|
|
233
|
+
token,
|
|
234
|
+
consistent: true,
|
|
235
|
+
client,
|
|
236
|
+
});
|
|
237
|
+
const existing = await stored();
|
|
238
|
+
if (existing) {
|
|
239
|
+
if (await calibrationTreeBlobText(existing.stream) !== part.serialized) {
|
|
240
|
+
// The build ID covers the files, not the time or label the index
|
|
241
|
+
// records, so another submission of the same files lands here, as does
|
|
242
|
+
// a rebuild whose output changed.
|
|
243
|
+
throw new CalibrationTreeEntryRefusedError(
|
|
244
|
+
`Immutable calibration tree part ${part.path} already exists with different content: ` +
|
|
245
|
+
"a tree for these files is already stored with another index.",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
await client.put(part.path, Buffer.from(part.compressed), {
|
|
251
|
+
access: "private",
|
|
252
|
+
token,
|
|
253
|
+
addRandomSuffix: false,
|
|
254
|
+
multipart: true,
|
|
255
|
+
contentType: "application/gzip",
|
|
256
|
+
cacheControlMaxAge: IMMUTABLE_CACHE_SECONDS,
|
|
257
|
+
});
|
|
258
|
+
const uploaded = await stored();
|
|
259
|
+
if (!uploaded || await calibrationTreeBlobText(uploaded.stream) !== part.serialized) {
|
|
260
|
+
throw new Error(`Uploaded calibration tree part ${part.path} failed verification.`);
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// When a build was published: createdAt, else updatedAt; an undated build is
|
|
266
|
+
// older than any dated one.
|
|
267
|
+
function publishedInstant(entry: CalibrationTreeManifestEntry): number {
|
|
268
|
+
const instant = Date.parse(entry.createdAt ?? entry.updatedAt ?? "");
|
|
269
|
+
return Number.isFinite(instant) ? instant : Number.NEGATIVE_INFINITY;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Whether the build is at least as new as the country's current latest.
|
|
273
|
+
function isAtLeastAsNewAsLatest(
|
|
274
|
+
manifest: CalibrationTreeManifest,
|
|
275
|
+
country: MicrocosmCountry,
|
|
276
|
+
entry: CalibrationTreeManifestEntry,
|
|
277
|
+
): boolean {
|
|
278
|
+
const current = manifest.countries[country];
|
|
279
|
+
const latest = current?.builds.find((build) => build.buildArtifactId === current.latestReleaseBuildArtifactId);
|
|
280
|
+
return !latest || latest.buildArtifactId === entry.buildArtifactId ||
|
|
281
|
+
publishedInstant(entry) >= publishedInstant(latest);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function addManifestEntry(
|
|
285
|
+
request: CalibrationTreePublicationRequest,
|
|
286
|
+
token: string,
|
|
287
|
+
client: CalibrationTreeBlobClient,
|
|
288
|
+
): Promise<boolean> {
|
|
289
|
+
const current = await readCalibrationTreeManifest({ token, consistent: true, client });
|
|
290
|
+
// An existing manifest is replaced only on its ETag; without one, the write
|
|
291
|
+
// would carry no condition.
|
|
292
|
+
if (current.etag !== null && !current.etag) {
|
|
293
|
+
throw new CalibrationTreeManifestConflictError(
|
|
294
|
+
"The stored calibration tree manifest was read without an ETag, so it cannot be replaced safely.",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const makeLatest = request.makeLatest &&
|
|
298
|
+
(request.manifestEntry.kind !== "release" ||
|
|
299
|
+
isAtLeastAsNewAsLatest(current.manifest, request.country, request.manifestEntry));
|
|
300
|
+
let updated;
|
|
301
|
+
try {
|
|
302
|
+
updated = withCalibrationTreeManifestEntry(current.manifest, request.country, request.manifestEntry, makeLatest);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new CalibrationTreeEntryRefusedError(error instanceof Error ? error.message : String(error));
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
await client.put(CALIBRATION_TREE_MANIFEST_PATH, serializeCalibrationTreeManifest(updated), {
|
|
308
|
+
access: "private",
|
|
309
|
+
token,
|
|
310
|
+
addRandomSuffix: false,
|
|
311
|
+
// Create only while absent; otherwise replace only the version read.
|
|
312
|
+
allowOverwrite: current.etag != null,
|
|
313
|
+
ifMatch: current.etag ?? undefined,
|
|
314
|
+
contentType: "application/json; charset=utf-8",
|
|
315
|
+
cacheControlMaxAge: MANIFEST_CACHE_SECONDS,
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
throw new CalibrationTreeManifestConflictError(
|
|
319
|
+
`The calibration tree manifest was not written, because another writer changed it or the store refused the write: ${error instanceof Error ? error.message : String(error)}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const verified = await readCalibrationTreeManifest({ token, consistent: true, client });
|
|
323
|
+
const recorded = verified.manifest.countries[request.country];
|
|
324
|
+
const entry = recorded?.builds.find((build) => build.buildArtifactId === request.buildArtifactId);
|
|
325
|
+
if (
|
|
326
|
+
!entry ||
|
|
327
|
+
entry.indexSha256 !== request.manifestEntry.indexSha256 ||
|
|
328
|
+
(makeLatest && recorded?.latestReleaseBuildArtifactId !== request.buildArtifactId)
|
|
329
|
+
) {
|
|
330
|
+
throw new Error("Calibration tree manifest verification failed.");
|
|
331
|
+
}
|
|
332
|
+
return makeLatest;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const DEFAULT_CLIENT: CalibrationTreeBlobClient = { get, put };
|
|
336
|
+
|
|
337
|
+
/** Uploads a built tree and records it in the manifest. */
|
|
338
|
+
export async function publishBuiltCalibrationTree(options: {
|
|
339
|
+
request: CalibrationTreePublicationRequest;
|
|
340
|
+
token: string;
|
|
341
|
+
client?: CalibrationTreeBlobClient;
|
|
342
|
+
}): Promise<CalibrationTreePublicationResult> {
|
|
343
|
+
const { request, token, client = DEFAULT_CLIENT } = options;
|
|
344
|
+
const parts = partsOnDisk(request);
|
|
345
|
+
const ordered = [...parts.filter((part) => part.part !== "index"), ...parts.filter((part) => part.part === "index")];
|
|
346
|
+
let uploadedParts = 0;
|
|
347
|
+
for (const part of ordered) {
|
|
348
|
+
if (await uploadPart(part, request, token, client)) uploadedParts += 1;
|
|
349
|
+
}
|
|
350
|
+
const madeLatest = await addManifestEntry(request, token, client);
|
|
351
|
+
return {
|
|
352
|
+
buildArtifactId: request.buildArtifactId,
|
|
353
|
+
kind: request.manifestEntry.kind,
|
|
354
|
+
uploadedParts,
|
|
355
|
+
keptParts: parts.length - uploadedParts,
|
|
356
|
+
madeLatest,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The exit code for an error that stopped the publication. */
|
|
361
|
+
export function calibrationTreePublicationExitCode(error: unknown): number {
|
|
362
|
+
if (error instanceof CalibrationTreePublicationRequestError) return CALIBRATION_TREE_PUBLICATION_EXIT_REQUEST_ERROR;
|
|
363
|
+
if (error instanceof CalibrationTreeEntryRefusedError) return CALIBRATION_TREE_PUBLICATION_EXIT_ENTRY_REFUSED;
|
|
364
|
+
return CALIBRATION_TREE_PUBLICATION_EXIT_PUBLICATION_ERROR;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The command line: returns the process exit code. */
|
|
368
|
+
export async function calibrationTreePublicationMain(
|
|
369
|
+
argv: string[],
|
|
370
|
+
environment: Record<string, string | undefined> = process.env,
|
|
371
|
+
): Promise<number> {
|
|
372
|
+
try {
|
|
373
|
+
if (argv.length !== 1) {
|
|
374
|
+
throw new CalibrationTreePublicationRequestError("Usage: bun run src/calibration-tree-publication.ts PUBLICATION.json");
|
|
375
|
+
}
|
|
376
|
+
const token = environment[CALIBRATION_TREE_PUBLICATION_TOKEN_VARIABLE]?.trim();
|
|
377
|
+
if (!token) {
|
|
378
|
+
throw new CalibrationTreePublicationRequestError(`${CALIBRATION_TREE_PUBLICATION_TOKEN_VARIABLE} is required.`);
|
|
379
|
+
}
|
|
380
|
+
let parsed: unknown;
|
|
381
|
+
try {
|
|
382
|
+
parsed = JSON.parse(readFileSync(argv[0], "utf8"));
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new CalibrationTreePublicationRequestError(
|
|
385
|
+
`The request ${argv[0]} could not be read as JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const result = await publishBuiltCalibrationTree({ request: parseCalibrationTreePublicationRequest(parsed), token });
|
|
389
|
+
console.log(JSON.stringify(result));
|
|
390
|
+
return 0;
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
393
|
+
return calibrationTreePublicationExitCode(error);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (import.meta.main) {
|
|
398
|
+
process.exitCode = await calibrationTreePublicationMain(Bun.argv.slice(2));
|
|
399
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// The calibration tree worker: builds one release's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
5
|
//
|
|
6
6
|
// bun run src/calibration-tree-worker.ts REQUEST.json OUTPUT_DIRECTORY
|
|
7
7
|
//
|
|
@@ -24,12 +24,25 @@
|
|
|
24
24
|
// }
|
|
25
25
|
// }
|
|
26
26
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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:
|
|
33
46
|
//
|
|
34
47
|
// 1 the request could not be followed: a malformed request, a missing
|
|
35
48
|
// file, an output directory that is not empty;
|
|
@@ -49,10 +62,11 @@ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
49
62
|
import { dirname, join } from "node:path";
|
|
50
63
|
import { createHash } from "node:crypto";
|
|
51
64
|
|
|
52
|
-
import type
|
|
65
|
+
import { CALIBRATION_TREE_SCHEMA_VERSION, type CalibrationTreeSourceArtifact } from "./calibration-tree-artifact";
|
|
53
66
|
import { buildCalibrationTreeBundle, type CalibrationTreeBundle } from "./calibration-tree-bundle";
|
|
67
|
+
import type { CalibrationTreeManifestEntry } from "./calibration-tree-manifest";
|
|
54
68
|
import { isCountry, type MicrocosmCountry } from "./countries";
|
|
55
|
-
import { buildCalibration, InvalidReleaseIdError } from "./latest-artifact";
|
|
69
|
+
import { assertSafeReleaseId, buildCalibration, InvalidReleaseIdError } from "./latest-artifact";
|
|
56
70
|
import { CalibrationTreeArtifactSizeError, enforceConfiguredSizeLimits } from "./publish-calibration-tree";
|
|
57
71
|
import { isBuildFolderId, isReleaseDirectory } from "./release-record";
|
|
58
72
|
import { IncompatibleStagingDataError } from "./staging-contract";
|
|
@@ -76,6 +90,16 @@ const REQUEST_FIELDS = [
|
|
|
76
90
|
"createdAt",
|
|
77
91
|
"artifacts",
|
|
78
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;
|
|
79
103
|
const HF_COMMIT_RE = /^[0-9a-f]{40,64}$/;
|
|
80
104
|
// A date and time with seconds and an explicit offset, so the instant never
|
|
81
105
|
// depends on the machine's time zone.
|
|
@@ -91,7 +115,14 @@ export interface CalibrationTreeWorkerArtifact {
|
|
|
91
115
|
file: string;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
|
-
export interface
|
|
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 {
|
|
95
126
|
kind: "release";
|
|
96
127
|
country: MicrocosmCountry;
|
|
97
128
|
releaseId: string;
|
|
@@ -99,14 +130,26 @@ export interface CalibrationTreeWorkerRequest {
|
|
|
99
130
|
hfRepo: string;
|
|
100
131
|
hfCommitSha: string;
|
|
101
132
|
createdAt: string | null;
|
|
102
|
-
artifacts:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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;
|
|
108
147
|
}
|
|
109
148
|
|
|
149
|
+
export type CalibrationTreeWorkerRequest =
|
|
150
|
+
| CalibrationTreeWorkerReleaseRequest
|
|
151
|
+
| CalibrationTreeWorkerStagingRequest;
|
|
152
|
+
|
|
110
153
|
export interface CalibrationTreeWorkerPart {
|
|
111
154
|
part: string;
|
|
112
155
|
path: string;
|
|
@@ -117,6 +160,7 @@ export interface CalibrationTreeWorkerPart {
|
|
|
117
160
|
|
|
118
161
|
export interface CalibrationTreeWorkerResult {
|
|
119
162
|
country: MicrocosmCountry;
|
|
163
|
+
/** The release id for a release; the label for a staged run. */
|
|
120
164
|
releaseId: string;
|
|
121
165
|
hfRepo: string;
|
|
122
166
|
hfCommitSha: string;
|
|
@@ -124,6 +168,8 @@ export interface CalibrationTreeWorkerResult {
|
|
|
124
168
|
buildArtifactId: string;
|
|
125
169
|
index: Omit<CalibrationTreeWorkerPart, "part">;
|
|
126
170
|
files: CalibrationTreeWorkerPart[];
|
|
171
|
+
/** The entry the publisher would add to the manifest for this build. */
|
|
172
|
+
manifestEntry: CalibrationTreeManifestEntry;
|
|
127
173
|
}
|
|
128
174
|
|
|
129
175
|
/** The request could not be followed; nothing about the files is known. */
|
|
@@ -224,13 +270,23 @@ function workerArtifact(value: unknown, name: ArtifactName): CalibrationTreeWork
|
|
|
224
270
|
};
|
|
225
271
|
}
|
|
226
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
|
+
|
|
227
283
|
/** Checks a request read from JSON, refusing anything the worker would guess at. */
|
|
228
284
|
export function parseCalibrationTreeWorkerRequest(value: unknown): CalibrationTreeWorkerRequest {
|
|
229
285
|
const request = requestObject(value, "The request");
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
throw new CalibrationTreeWorkerRequestError('kind must be "release".');
|
|
286
|
+
if (request.kind !== "release" && request.kind !== "staging") {
|
|
287
|
+
throw new CalibrationTreeWorkerRequestError('kind must be "release" or "staging".');
|
|
233
288
|
}
|
|
289
|
+
onlyFields(request, request.kind === "release" ? REQUEST_FIELDS : STAGING_REQUEST_FIELDS, "The request");
|
|
234
290
|
const country = requestString(request, "country");
|
|
235
291
|
if (!isCountry(country)) {
|
|
236
292
|
throw new CalibrationTreeWorkerRequestError(`country ${country} is not a registered country.`);
|
|
@@ -244,13 +300,31 @@ export function parseCalibrationTreeWorkerRequest(value: unknown): CalibrationTr
|
|
|
244
300
|
throw new CalibrationTreeWorkerRequestError("hfCommitSha must be a full lowercase commit SHA.");
|
|
245
301
|
}
|
|
246
302
|
const createdAt = publicationTime(request.createdAt);
|
|
247
|
-
const
|
|
248
|
-
onlyFields(
|
|
303
|
+
const artifactFields = requestObject(request.artifacts, "artifacts");
|
|
304
|
+
onlyFields(artifactFields, ARTIFACT_NAMES, "artifacts");
|
|
249
305
|
for (const name of ARTIFACT_NAMES) {
|
|
250
|
-
if (!(name in
|
|
306
|
+
if (!(name in artifactFields)) {
|
|
251
307
|
throw new CalibrationTreeWorkerRequestError(`artifacts.${name} is required (null when absent).`);
|
|
252
308
|
}
|
|
253
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
|
+
}
|
|
254
328
|
return {
|
|
255
329
|
kind: "release",
|
|
256
330
|
country,
|
|
@@ -259,12 +333,7 @@ export function parseCalibrationTreeWorkerRequest(value: unknown): CalibrationTr
|
|
|
259
333
|
hfRepo,
|
|
260
334
|
hfCommitSha,
|
|
261
335
|
createdAt,
|
|
262
|
-
artifacts
|
|
263
|
-
calibrationDiagnostics: workerArtifact(artifacts.calibrationDiagnostics, "calibrationDiagnostics")!,
|
|
264
|
-
buildManifest: workerArtifact(artifacts.buildManifest, "buildManifest"),
|
|
265
|
-
releaseManifest: workerArtifact(artifacts.releaseManifest, "releaseManifest"),
|
|
266
|
-
demographics: workerArtifact(artifacts.demographics, "demographics"),
|
|
267
|
-
},
|
|
336
|
+
artifacts,
|
|
268
337
|
};
|
|
269
338
|
}
|
|
270
339
|
|
|
@@ -302,61 +371,183 @@ function readArtifact(artifact: CalibrationTreeWorkerArtifact | null): ReadArtif
|
|
|
302
371
|
};
|
|
303
372
|
}
|
|
304
373
|
|
|
305
|
-
/**
|
|
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
|
+
*/
|
|
306
378
|
export function buildCalibrationTreeWorkerBundle(request: CalibrationTreeWorkerRequest): CalibrationTreeBundle {
|
|
307
379
|
const [diagnostics, buildManifest, releaseManifest, demographics] = ARTIFACT_NAMES.map((name) =>
|
|
308
380
|
readArtifact(request.artifacts[name])
|
|
309
381
|
);
|
|
310
382
|
if (!diagnostics) throw new CalibrationTreeWorkerRequestError("Calibration diagnostics are missing.");
|
|
311
|
-
const { country, releaseId: id, releaseDirectory, hfCommitSha } = request;
|
|
312
383
|
try {
|
|
313
|
-
const
|
|
314
|
-
diagnostics.
|
|
315
|
-
|
|
316
|
-
null,
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
calibrationDiagnostics: diagnostics.source,
|
|
333
|
-
buildManifest: buildManifest?.source ?? null,
|
|
334
|
-
releaseManifest: releaseManifest?.source ?? null,
|
|
335
|
-
demographics: demographics?.source ?? null,
|
|
336
|
-
},
|
|
337
|
-
rows: calibration.rows,
|
|
338
|
-
calibrationProvenance: calibration.calibration_provenance,
|
|
339
|
-
lossAttributionAvailable: calibration.target_loss_attribution.status !== "unavailable",
|
|
340
|
-
comparison: {
|
|
341
|
-
// As in publishRelease: a tag or a build folder compares under its
|
|
342
|
-
// own id, a release folder under the release its diagnostics name.
|
|
343
|
-
releaseId: releaseDirectory === id && !isBuildFolderId(id) ? calibration.release_id : id,
|
|
344
|
-
calibrationProvenance: calibration.calibration_provenance,
|
|
345
|
-
status: calibration.target_loss_attribution.status,
|
|
346
|
-
aggregate: calibration.target_loss_attribution.aggregate,
|
|
347
|
-
cap: calibration.target_loss_attribution.cap,
|
|
348
|
-
basisIdentifier: calibration.target_loss_attribution.basis_identifier,
|
|
349
|
-
targetRepresentation: calibration.target_schema.target_representation,
|
|
350
|
-
},
|
|
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 ?? {},
|
|
351
403
|
});
|
|
352
|
-
for (const file of bundle.files) enforceConfiguredSizeLimits(file);
|
|
353
|
-
return bundle;
|
|
354
404
|
} catch (error) {
|
|
355
405
|
if (!isCalibrationTreeContentRefusal(error)) throw error;
|
|
356
406
|
throw new CalibrationTreeUnbuildableInputError((error as Error).message);
|
|
357
407
|
}
|
|
358
408
|
}
|
|
359
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
|
+
|
|
360
551
|
function emptyOutputDirectory(outputDirectory: string): void {
|
|
361
552
|
try {
|
|
362
553
|
mkdirSync(outputDirectory, { recursive: true });
|
|
@@ -394,13 +585,14 @@ export function runCalibrationTreeWorker(
|
|
|
394
585
|
const { part: _index, ...index } = files.find((file) => file.part === "index")!;
|
|
395
586
|
return {
|
|
396
587
|
country: request.country,
|
|
397
|
-
releaseId: request.releaseId,
|
|
588
|
+
releaseId: request.kind === "staging" ? request.label : request.releaseId,
|
|
398
589
|
hfRepo: request.hfRepo,
|
|
399
590
|
hfCommitSha: request.hfCommitSha,
|
|
400
591
|
createdAt: request.createdAt,
|
|
401
592
|
buildArtifactId: bundle.index.buildArtifactId,
|
|
402
593
|
index,
|
|
403
594
|
files,
|
|
595
|
+
manifestEntry: manifestEntry(request, bundle, index),
|
|
404
596
|
};
|
|
405
597
|
}
|
|
406
598
|
|