@policyengine/calibration-tree 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/package.json +71 -4
  4. package/src/calibration-build-comparison.ts +77 -0
  5. package/src/calibration-comparison-blob.ts +367 -0
  6. package/src/calibration-comparison-bundle.ts +149 -0
  7. package/src/calibration-explorer.ts +202 -0
  8. package/src/calibration-release-locator.ts +353 -0
  9. package/src/calibration-tree-artifact.ts +1974 -0
  10. package/src/calibration-tree-blob.ts +403 -0
  11. package/src/calibration-tree-bundle.ts +569 -0
  12. package/src/calibration-tree-manifest.ts +349 -0
  13. package/src/calibration-tree-worker.ts +445 -0
  14. package/src/calibration-tree.ts +1284 -0
  15. package/src/chronicle-metadata.ts +42 -0
  16. package/src/countries.ts +355 -0
  17. package/src/geography-labels.ts +59 -0
  18. package/src/hierarchy-catalog-reader.ts +297 -0
  19. package/src/hierarchy-target-reader.ts +243 -0
  20. package/src/historical-weight-summary.ts +53 -0
  21. package/src/latest-artifact.ts +3767 -0
  22. package/src/legacy-target-reader.ts +337 -0
  23. package/src/program-label.ts +64 -0
  24. package/src/publish-calibration-tree.ts +1984 -0
  25. package/src/reforms.ts +224 -0
  26. package/src/release-cut.ts +32 -0
  27. package/src/release-record.ts +277 -0
  28. package/src/source-label.ts +5 -0
  29. package/src/source-labels.ts +71 -0
  30. package/src/staging-artifact.ts +1411 -0
  31. package/src/staging-contract.ts +916 -0
  32. package/src/structured-dimension-reader.ts +137 -0
  33. package/src/structured-target-reader.ts +76 -0
  34. package/src/target-change.ts +421 -0
  35. package/src/target-loss-attribution-manifest.ts +436 -0
  36. package/src/target-loss-attribution.ts +1012 -0
  37. package/src/target-representation.ts +69 -0
  38. package/src/target-surface-matcher.ts +266 -0
  39. package/types/next-fetch-cache.d.ts +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PolicyEngine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # @policyengine/calibration-tree
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Builds, validates, reads and publishes Microcosm calibration tree artifacts
4
+ (tree schema 6), with the release and staging readers they are built from.
5
+ The PolicyEngine calibration dashboard reads trees with it, and the Microcosm
6
+ collector builds them with it.
7
+
8
+ ## Using it
9
+
10
+ The package ships TypeScript source, not compiled JavaScript. Each module is
11
+ its own entry point, for example:
12
+
13
+ ```ts
14
+ import { parseCalibrationTreeIndex } from "@policyengine/calibration-tree/calibration-tree-artifact";
15
+ import { COUNTRY_REGISTRY } from "@policyengine/calibration-tree/countries";
16
+ ```
17
+
18
+ - **Bun** runs the modules directly.
19
+ - **Next.js** compiles them when the package is listed in `transpilePackages`
20
+ in `next.config.ts`.
21
+ - **TypeScript** checks them with `moduleResolution` set to `bundler`. Some
22
+ modules use Bun's globals (`Bun.argv`, `import.meta.main`), so the checking
23
+ project needs `@types/bun`; `types/next-fetch-cache.d.ts` declares the
24
+ `next.revalidate` fetch option the readers pass, for projects without
25
+ Next.js.
26
+
27
+ Plain Node.js cannot load the modules, because they are TypeScript.
28
+
29
+ ## The tree worker
30
+
31
+ `src/calibration-tree-worker.ts` builds one release's tree from local copies
32
+ of the files the publisher reads from Hugging Face, and writes each part's
33
+ gzip bytes under an output directory:
34
+
35
+ ```bash
36
+ bun run src/calibration-tree-worker.ts REQUEST.json OUTPUT_DIRECTORY
37
+ ```
38
+
39
+ The module's opening comment describes the request. The worker writes the
40
+ same bytes as the publisher (`src/publish-calibration-tree.ts`) for the same
41
+ files, commit and publication time; the golden tests hold both to the trees
42
+ recorded in `src/fixtures/calibration-tree-golden.json`. It exits 0 after
43
+ printing a JSON description of the written parts, 1 when the request cannot be
44
+ followed, 2 when the builder refuses the files, so another run on the same
45
+ files cannot help, and 3 for anything else: a size limit that is not a
46
+ positive integer, or an unexpected error such as a defect in the builder.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ bun install --frozen-lockfile
52
+ bun test src/calibration-tree-worker.test.ts
53
+ bun run typecheck
54
+ ```
55
+
56
+ The source lives in
57
+ [PolicyEngine/microcosm-collector](https://github.com/PolicyEngine/microcosm-collector)
58
+ under `packages/calibration-tree`; its README describes how the package is
59
+ released.
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
1
  {
2
2
  "name": "@policyengine/calibration-tree",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/PolicyEngine/microcosm-collector.git",
9
+ "directory": "packages/calibration-tree"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "files": [
16
+ "src/**/*.ts",
17
+ "!src/**/*.test.ts",
18
+ "!src/test-support/**",
19
+ "!src/fixtures/**",
20
+ "types/**/*.d.ts"
21
+ ],
22
+ "exports": {
23
+ "./calibration-build-comparison": "./src/calibration-build-comparison.ts",
24
+ "./calibration-comparison-blob": "./src/calibration-comparison-blob.ts",
25
+ "./calibration-comparison-bundle": "./src/calibration-comparison-bundle.ts",
26
+ "./calibration-explorer": "./src/calibration-explorer.ts",
27
+ "./calibration-release-locator": "./src/calibration-release-locator.ts",
28
+ "./calibration-tree": "./src/calibration-tree.ts",
29
+ "./calibration-tree-artifact": "./src/calibration-tree-artifact.ts",
30
+ "./calibration-tree-blob": "./src/calibration-tree-blob.ts",
31
+ "./calibration-tree-bundle": "./src/calibration-tree-bundle.ts",
32
+ "./calibration-tree-manifest": "./src/calibration-tree-manifest.ts",
33
+ "./calibration-tree-worker": "./src/calibration-tree-worker.ts",
34
+ "./chronicle-metadata": "./src/chronicle-metadata.ts",
35
+ "./countries": "./src/countries.ts",
36
+ "./geography-labels": "./src/geography-labels.ts",
37
+ "./hierarchy-catalog-reader": "./src/hierarchy-catalog-reader.ts",
38
+ "./hierarchy-target-reader": "./src/hierarchy-target-reader.ts",
39
+ "./historical-weight-summary": "./src/historical-weight-summary.ts",
40
+ "./latest-artifact": "./src/latest-artifact.ts",
41
+ "./legacy-target-reader": "./src/legacy-target-reader.ts",
42
+ "./program-label": "./src/program-label.ts",
43
+ "./publish-calibration-tree": "./src/publish-calibration-tree.ts",
44
+ "./reforms": "./src/reforms.ts",
45
+ "./release-cut": "./src/release-cut.ts",
46
+ "./release-record": "./src/release-record.ts",
47
+ "./source-label": "./src/source-label.ts",
48
+ "./source-labels": "./src/source-labels.ts",
49
+ "./staging-artifact": "./src/staging-artifact.ts",
50
+ "./staging-contract": "./src/staging-contract.ts",
51
+ "./structured-dimension-reader": "./src/structured-dimension-reader.ts",
52
+ "./structured-target-reader": "./src/structured-target-reader.ts",
53
+ "./target-change": "./src/target-change.ts",
54
+ "./target-loss-attribution": "./src/target-loss-attribution.ts",
55
+ "./target-loss-attribution-manifest": "./src/target-loss-attribution-manifest.ts",
56
+ "./target-representation": "./src/target-representation.ts",
57
+ "./target-surface-matcher": "./src/target-surface-matcher.ts"
58
+ },
59
+ "scripts": {
60
+ "publish:calibration-tree": "bun run src/publish-calibration-tree.ts",
61
+ "test": "bun test",
62
+ "typecheck": "tsc --noEmit",
63
+ "worker": "bun run src/calibration-tree-worker.ts"
64
+ },
65
+ "dependencies": {
66
+ "@vercel/blob": "^2.6.1"
67
+ },
68
+ "devDependencies": {
69
+ "@types/bun": "^1.3.14",
70
+ "fast-check": "^4.10.2",
71
+ "typescript": "^5.8.0"
72
+ }
73
+ }
@@ -0,0 +1,77 @@
1
+ import type { Calibration } from "./latest-artifact";
2
+ import type {
3
+ CalibrationTreeComparisonMetadata,
4
+ CalibrationTreeTargetSummary,
5
+ } from "./calibration-tree-artifact";
6
+ import type { MicrocosmCountry } from "./countries";
7
+ import { buildTargetChangeDataset, type TargetChangeDataset } from "./target-change";
8
+
9
+ export interface CalibrationTreeComparisonInput {
10
+ country: MicrocosmCountry;
11
+ comparison: CalibrationTreeComparisonMetadata;
12
+ targets: CalibrationTreeTargetSummary[];
13
+ }
14
+
15
+ function calibrationFromTargetSummaries(
16
+ input: CalibrationTreeComparisonInput,
17
+ ): Calibration {
18
+ const comparison = input.comparison;
19
+ return {
20
+ release_id: comparison.releaseId,
21
+ calibration_provenance: comparison.calibrationProvenance ?? {
22
+ mode: "direct",
23
+ dataset_release_id: comparison.releaseId,
24
+ calibration_source_id: comparison.releaseId,
25
+ diagnostics_sha256: null,
26
+ target_surface_sha256: null,
27
+ validation: { status: "not_applicable", reason: null },
28
+ },
29
+ target_schema: {
30
+ diagnostics_schema_version: null,
31
+ structured_dimensions:
32
+ comparison.targetRepresentation === "structured" ||
33
+ comparison.targetRepresentation === "hierarchy" ||
34
+ comparison.targetRepresentation === "mixed",
35
+ target_representation: comparison.targetRepresentation,
36
+ },
37
+ target_loss_attribution: {
38
+ status: comparison.status,
39
+ aggregate: comparison.aggregate,
40
+ historical_final_loss: null,
41
+ cap: comparison.cap,
42
+ basis_identifier: comparison.basisIdentifier,
43
+ basis_hash: null,
44
+ verification: null,
45
+ producer_warnings: [],
46
+ reason:
47
+ comparison.status === "unavailable"
48
+ ? "The immutable build does not include target-loss attribution."
49
+ : null,
50
+ targets: [],
51
+ },
52
+ rows: input.targets.map((target) => target.comparison.row),
53
+ } as unknown as Calibration;
54
+ }
55
+
56
+ export function buildTargetChangeDatasetFromSummaries(
57
+ current: CalibrationTreeComparisonInput,
58
+ candidate: CalibrationTreeComparisonInput,
59
+ ): TargetChangeDataset {
60
+ if (current.country !== candidate.country) {
61
+ throw new Error("Calibration builds from different countries cannot be compared.");
62
+ }
63
+ return buildTargetChangeDataset(
64
+ calibrationFromTargetSummaries(current),
65
+ calibrationFromTargetSummaries(candidate),
66
+ );
67
+ }
68
+
69
+ export function buildTargetChangeDatasetFromSummaryAndCalibration(
70
+ current: CalibrationTreeComparisonInput,
71
+ candidate: Calibration,
72
+ ): TargetChangeDataset {
73
+ return buildTargetChangeDataset(
74
+ calibrationFromTargetSummaries(current),
75
+ candidate,
76
+ );
77
+ }
@@ -0,0 +1,367 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ buildCalibrationComparisonBundles,
5
+ calibrationComparisonBuildArtifactId,
6
+ calibrationComparisonPairArtifactId,
7
+ type ComparisonBundleSource,
8
+ } from "./calibration-comparison-bundle";
9
+ import {
10
+ CALIBRATION_TREE_SCHEMA_VERSION,
11
+ calibrationTreeTargetDetailSelection,
12
+ calibrationTreeTargetsFromSummaries,
13
+ parseCalibrationTreeIndex,
14
+ parseCalibrationTreeTargetDetails,
15
+ parseCalibrationTreeTargetSummary,
16
+ type CalibrationTreeComparisonTargetDetailResponse,
17
+ type CalibrationTreeIndexArtifact,
18
+ type CalibrationTreeTargetDetailsArtifact,
19
+ } from "./calibration-tree-artifact";
20
+ import {
21
+ calibrationTreeBlobText,
22
+ getCalibrationTreeBlob,
23
+ uploadCalibrationTreeBundle,
24
+ } from "./calibration-tree-blob";
25
+ import type { MicrocosmCountry } from "./countries";
26
+ import type { TargetChangeRow } from "./target-change";
27
+
28
+ interface ComparisonBuildIds {
29
+ pairArtifactId: string;
30
+ reportedBuildArtifactId: string;
31
+ sharedBuildArtifactId: string;
32
+ }
33
+
34
+ const activeBuilds = new Map<string, Promise<ComparisonBuildIds>>();
35
+ const ACTIVE_BUILD_LIMIT = 8;
36
+
37
+ function ids(
38
+ country: MicrocosmCountry,
39
+ currentBuildArtifactId: string,
40
+ candidateBuildArtifactId: string,
41
+ ): ComparisonBuildIds {
42
+ const pairArtifactId = calibrationComparisonPairArtifactId(
43
+ country,
44
+ currentBuildArtifactId,
45
+ candidateBuildArtifactId,
46
+ );
47
+ return {
48
+ pairArtifactId,
49
+ reportedBuildArtifactId: calibrationComparisonBuildArtifactId(
50
+ pairArtifactId,
51
+ "reported",
52
+ ),
53
+ sharedBuildArtifactId: calibrationComparisonBuildArtifactId(
54
+ pairArtifactId,
55
+ "shared",
56
+ ),
57
+ };
58
+ }
59
+
60
+ async function textFromBlob(
61
+ result: Awaited<ReturnType<typeof getCalibrationTreeBlob>>,
62
+ label: string,
63
+ ): Promise<string> {
64
+ if (!result || result.statusCode !== 200 || !result.stream) {
65
+ throw new Error(`${label} is unavailable.`);
66
+ }
67
+ return calibrationTreeBlobText(result.stream);
68
+ }
69
+
70
+ function sha256(value: string): string {
71
+ return createHash("sha256").update(value, "utf8").digest("hex");
72
+ }
73
+
74
+ export async function loadCalibrationComparisonSource(
75
+ country: MicrocosmCountry,
76
+ buildArtifactId: string,
77
+ getBlob: typeof getCalibrationTreeBlob = getCalibrationTreeBlob,
78
+ ): Promise<ComparisonBundleSource> {
79
+ const indexText = await textFromBlob(
80
+ await getBlob({
81
+ country,
82
+ buildArtifactId,
83
+ part: "index",
84
+ consistent: true,
85
+ }),
86
+ `Calibration build ${buildArtifactId} index`,
87
+ );
88
+ const index = parseCalibrationTreeIndex(JSON.parse(indexText));
89
+ if (index.country !== country || index.buildArtifactId !== buildArtifactId) {
90
+ throw new Error(`Calibration build ${buildArtifactId} index identity is inconsistent.`);
91
+ }
92
+ if (index.build.kind === "comparison") {
93
+ throw new Error("A derived comparison cannot be used as a source build.");
94
+ }
95
+ const targetSummaries = await Promise.all(
96
+ index.parts.targetSummaries.map(async (descriptor) => {
97
+ const text = await textFromBlob(
98
+ await getBlob({
99
+ country,
100
+ buildArtifactId,
101
+ part: descriptor.part,
102
+ consistent: true,
103
+ }),
104
+ `Calibration build ${buildArtifactId} ${descriptor.part}`,
105
+ );
106
+ if (sha256(text) !== descriptor.sha256) {
107
+ throw new Error(
108
+ `Calibration build ${buildArtifactId} ${descriptor.part} digest is invalid.`,
109
+ );
110
+ }
111
+ return parseCalibrationTreeTargetSummary(JSON.parse(text), descriptor.part);
112
+ }),
113
+ );
114
+ return {
115
+ index,
116
+ indexSha256: sha256(indexText),
117
+ targetSummaries: calibrationTreeTargetsFromSummaries(index, targetSummaries),
118
+ };
119
+ }
120
+
121
+ async function loadIndex(options: {
122
+ country: MicrocosmCountry;
123
+ buildArtifactId: string;
124
+ expectedSha256?: string;
125
+ getBlob: typeof getCalibrationTreeBlob;
126
+ }): Promise<CalibrationTreeIndexArtifact> {
127
+ const text = await textFromBlob(
128
+ await options.getBlob({
129
+ country: options.country,
130
+ buildArtifactId: options.buildArtifactId,
131
+ part: "index",
132
+ consistent: true,
133
+ }),
134
+ `Calibration build ${options.buildArtifactId} index`,
135
+ );
136
+ if (options.expectedSha256 && sha256(text) !== options.expectedSha256) {
137
+ throw new Error(`Calibration build ${options.buildArtifactId} index digest is invalid.`);
138
+ }
139
+ const index = parseCalibrationTreeIndex(JSON.parse(text));
140
+ if (
141
+ index.country !== options.country ||
142
+ index.buildArtifactId !== options.buildArtifactId
143
+ ) {
144
+ throw new Error(
145
+ `Calibration build ${options.buildArtifactId} index identity is inconsistent.`,
146
+ );
147
+ }
148
+ return index;
149
+ }
150
+
151
+ export async function loadCalibrationComparisonTargetDetail(options: {
152
+ country: MicrocosmCountry;
153
+ buildArtifactId: string;
154
+ targetOrdinal: number;
155
+ getBlob?: typeof getCalibrationTreeBlob;
156
+ }): Promise<CalibrationTreeComparisonTargetDetailResponse> {
157
+ const getBlob = options.getBlob ?? getCalibrationTreeBlob;
158
+ const comparisonIndex = await loadIndex({
159
+ country: options.country,
160
+ buildArtifactId: options.buildArtifactId,
161
+ getBlob,
162
+ });
163
+ if (
164
+ comparisonIndex.build.kind !== "comparison" ||
165
+ comparisonIndex.parts.targetDetailStrategy !== "source-targets" ||
166
+ !comparisonIndex.comparison
167
+ ) {
168
+ throw new Error("The requested build does not use source-backed comparison details.");
169
+ }
170
+ if (
171
+ !Number.isSafeInteger(options.targetOrdinal) ||
172
+ options.targetOrdinal < 0 ||
173
+ options.targetOrdinal >= comparisonIndex.targetCount
174
+ ) {
175
+ throw new Error("The requested comparison target ordinal is out of range.");
176
+ }
177
+ const summaryDescriptor = comparisonIndex.parts.targetSummaries.find(
178
+ (descriptor) =>
179
+ options.targetOrdinal >= descriptor.startTargetOrdinal &&
180
+ options.targetOrdinal < descriptor.endTargetOrdinalExclusive,
181
+ );
182
+ if (!summaryDescriptor) {
183
+ throw new Error("The requested comparison target has no summary shard.");
184
+ }
185
+ const summaryText = await textFromBlob(
186
+ await getBlob({
187
+ country: options.country,
188
+ buildArtifactId: options.buildArtifactId,
189
+ part: summaryDescriptor.part,
190
+ consistent: true,
191
+ }),
192
+ `Calibration comparison ${options.buildArtifactId} ${summaryDescriptor.part}`,
193
+ );
194
+ if (sha256(summaryText) !== summaryDescriptor.sha256) {
195
+ throw new Error("The comparison target-summary digest is invalid.");
196
+ }
197
+ const summaryArtifact = parseCalibrationTreeTargetSummary(
198
+ JSON.parse(summaryText),
199
+ summaryDescriptor.part,
200
+ );
201
+ const summary = summaryArtifact.targets[
202
+ options.targetOrdinal - summaryDescriptor.startTargetOrdinal
203
+ ];
204
+ if (!summary?.detailSource) {
205
+ throw new Error("The comparison target does not identify its source targets.");
206
+ }
207
+
208
+ const sourceIndexes = new Map<string, Promise<CalibrationTreeIndexArtifact>>();
209
+ const sourceDetails = new Map<string, Promise<CalibrationTreeTargetDetailsArtifact>>();
210
+ const sourceIndex = (
211
+ buildArtifactId: string,
212
+ expectedSha256: string,
213
+ ): Promise<CalibrationTreeIndexArtifact> => {
214
+ const existing = sourceIndexes.get(buildArtifactId);
215
+ if (existing) return existing;
216
+ const loading = loadIndex({
217
+ country: options.country,
218
+ buildArtifactId,
219
+ expectedSha256,
220
+ getBlob,
221
+ });
222
+ sourceIndexes.set(buildArtifactId, loading);
223
+ return loading;
224
+ };
225
+ const sourceDetail = async (
226
+ buildArtifactId: string,
227
+ expectedIndexSha256: string,
228
+ targetOrdinal: number | null,
229
+ ) => {
230
+ if (targetOrdinal == null) return null;
231
+ const index = await sourceIndex(buildArtifactId, expectedIndexSha256);
232
+ if (index.build.kind === "comparison") {
233
+ throw new Error("Comparison target details must resolve to source builds.");
234
+ }
235
+ const selection = calibrationTreeTargetDetailSelection(index, targetOrdinal);
236
+ const key = `${buildArtifactId}:${selection.descriptor.part}`;
237
+ let loading = sourceDetails.get(key);
238
+ if (!loading) {
239
+ loading = (async () => {
240
+ const text = await textFromBlob(
241
+ await getBlob({
242
+ country: options.country,
243
+ buildArtifactId,
244
+ part: selection.descriptor.part,
245
+ consistent: true,
246
+ }),
247
+ `Calibration build ${buildArtifactId} ${selection.descriptor.part}`,
248
+ );
249
+ if (sha256(text) !== selection.descriptor.sha256) {
250
+ throw new Error(
251
+ `Calibration build ${buildArtifactId} ${selection.descriptor.part} digest is invalid.`,
252
+ );
253
+ }
254
+ return parseCalibrationTreeTargetDetails(
255
+ JSON.parse(text),
256
+ selection.descriptor.part,
257
+ );
258
+ })();
259
+ sourceDetails.set(key, loading);
260
+ }
261
+ const artifact = await loading;
262
+ return artifact.targets[selection.offset] ?? null;
263
+ };
264
+
265
+ const currentSource = comparisonIndex.build.sourceArtifacts.comparisonCurrentIndex;
266
+ const candidateSource = comparisonIndex.build.sourceArtifacts.comparisonCandidateIndex;
267
+ if (!currentSource || !candidateSource) {
268
+ throw new Error("The comparison build does not identify both source indexes.");
269
+ }
270
+ const { currentBuildArtifactId, candidateBuildArtifactId } = comparisonIndex.comparison;
271
+ const [currentDetail, candidateDetail] = await Promise.all([
272
+ sourceDetail(
273
+ currentBuildArtifactId,
274
+ currentSource.sha256,
275
+ summary.detailSource.currentTargetOrdinal,
276
+ ),
277
+ sourceDetail(
278
+ candidateBuildArtifactId,
279
+ candidateSource.sha256,
280
+ summary.detailSource.candidateTargetOrdinal,
281
+ ),
282
+ ]);
283
+ const compact = summary.comparison.row as TargetChangeRow;
284
+ return {
285
+ schemaVersion: CALIBRATION_TREE_SCHEMA_VERSION,
286
+ country: options.country,
287
+ buildArtifactId: options.buildArtifactId,
288
+ targetOrdinal: options.targetOrdinal,
289
+ targetId: summary.id,
290
+ target: {
291
+ ...compact,
292
+ currentDetail,
293
+ candidateDetail,
294
+ },
295
+ };
296
+ }
297
+
298
+ async function comparisonExists(
299
+ country: MicrocosmCountry,
300
+ buildArtifactId: string,
301
+ ): Promise<boolean> {
302
+ return Boolean(await getCalibrationTreeBlob({
303
+ country,
304
+ buildArtifactId,
305
+ part: "index",
306
+ consistent: true,
307
+ }));
308
+ }
309
+
310
+ async function buildAndPublishComparison(options: {
311
+ country: MicrocosmCountry;
312
+ currentBuildArtifactId: string;
313
+ candidateBuildArtifactId: string;
314
+ token: string;
315
+ }): Promise<ComparisonBuildIds> {
316
+ const result = ids(
317
+ options.country,
318
+ options.currentBuildArtifactId,
319
+ options.candidateBuildArtifactId,
320
+ );
321
+ const [reportedExists, sharedExists] = await Promise.all([
322
+ comparisonExists(options.country, result.reportedBuildArtifactId),
323
+ comparisonExists(options.country, result.sharedBuildArtifactId),
324
+ ]);
325
+ if (reportedExists && sharedExists) return result;
326
+
327
+ const [current, candidate] = await Promise.all([
328
+ loadCalibrationComparisonSource(options.country, options.currentBuildArtifactId),
329
+ loadCalibrationComparisonSource(options.country, options.candidateBuildArtifactId),
330
+ ]);
331
+ const bundles = buildCalibrationComparisonBundles(current, candidate);
332
+ if (bundles.pairArtifactId !== result.pairArtifactId) {
333
+ throw new Error("Calibration comparison identity is inconsistent.");
334
+ }
335
+ await Promise.all([
336
+ uploadCalibrationTreeBundle({ bundle: bundles.reported, token: options.token }),
337
+ uploadCalibrationTreeBundle({ bundle: bundles.shared, token: options.token }),
338
+ ]);
339
+ return result;
340
+ }
341
+
342
+ export function ensureCalibrationComparison(options: {
343
+ country: MicrocosmCountry;
344
+ currentBuildArtifactId: string;
345
+ candidateBuildArtifactId: string;
346
+ token: string;
347
+ }): Promise<ComparisonBuildIds> {
348
+ const key = [
349
+ options.country,
350
+ options.currentBuildArtifactId,
351
+ options.candidateBuildArtifactId,
352
+ ].join(":");
353
+ const existing = activeBuilds.get(key);
354
+ if (existing) return existing;
355
+ const promise = buildAndPublishComparison(options);
356
+ activeBuilds.set(key, promise);
357
+ while (activeBuilds.size > ACTIVE_BUILD_LIMIT) {
358
+ const oldest = activeBuilds.keys().next().value;
359
+ if (typeof oldest !== "string") break;
360
+ activeBuilds.delete(oldest);
361
+ }
362
+ void promise.then(
363
+ () => activeBuilds.delete(key),
364
+ () => activeBuilds.delete(key),
365
+ );
366
+ return promise;
367
+ }