@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,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { buildTargetChangeDatasetFromSummaries } from "./calibration-build-comparison";
|
|
4
|
+
import {
|
|
5
|
+
CALIBRATION_TREE_SCHEMA_VERSION,
|
|
6
|
+
calibrationTreePartPath,
|
|
7
|
+
type CalibrationTreeIndexArtifact,
|
|
8
|
+
type CalibrationTreeTargetSummary,
|
|
9
|
+
} from "./calibration-tree-artifact";
|
|
10
|
+
import type { CalibrationTreeTarget } from "./calibration-tree";
|
|
11
|
+
import {
|
|
12
|
+
buildCalibrationTreeBundle,
|
|
13
|
+
type CalibrationTreeBundle,
|
|
14
|
+
} from "./calibration-tree-bundle";
|
|
15
|
+
import { targetChangeForMode, type TargetChangeMode } from "./target-change";
|
|
16
|
+
|
|
17
|
+
export interface ComparisonBundleSource {
|
|
18
|
+
index: CalibrationTreeIndexArtifact;
|
|
19
|
+
indexSha256: string;
|
|
20
|
+
targetSummaries: CalibrationTreeTargetSummary[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CalibrationComparisonBundles {
|
|
24
|
+
pairArtifactId: string;
|
|
25
|
+
reported: CalibrationTreeBundle;
|
|
26
|
+
shared: CalibrationTreeBundle;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sha256(value: string): string {
|
|
30
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function calibrationComparisonPairArtifactId(
|
|
34
|
+
country: string,
|
|
35
|
+
currentBuildArtifactId: string,
|
|
36
|
+
candidateBuildArtifactId: string,
|
|
37
|
+
): string {
|
|
38
|
+
return sha256(JSON.stringify({
|
|
39
|
+
schemaVersion: CALIBRATION_TREE_SCHEMA_VERSION,
|
|
40
|
+
country,
|
|
41
|
+
currentBuildArtifactId,
|
|
42
|
+
candidateBuildArtifactId,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function calibrationComparisonBuildArtifactId(
|
|
47
|
+
pairArtifactId: string,
|
|
48
|
+
mode: TargetChangeMode,
|
|
49
|
+
): string {
|
|
50
|
+
return sha256(JSON.stringify({
|
|
51
|
+
schemaVersion: CALIBRATION_TREE_SCHEMA_VERSION,
|
|
52
|
+
pairArtifactId,
|
|
53
|
+
mode,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rowsForMode(
|
|
58
|
+
rows: ReturnType<typeof buildTargetChangeDatasetFromSummaries>["rows"],
|
|
59
|
+
mode: TargetChangeMode,
|
|
60
|
+
): CalibrationTreeTarget[] {
|
|
61
|
+
return rows.flatMap((row) => {
|
|
62
|
+
if (mode === "shared" && row.comparison_status !== "shared") return [];
|
|
63
|
+
const change = targetChangeForMode(row, mode);
|
|
64
|
+
return change == null ? [] : [{ ...row, target_change: change }];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildCalibrationComparisonBundles(
|
|
69
|
+
current: ComparisonBundleSource,
|
|
70
|
+
candidate: ComparisonBundleSource,
|
|
71
|
+
): CalibrationComparisonBundles {
|
|
72
|
+
if (current.index.country !== candidate.index.country) {
|
|
73
|
+
throw new Error("Calibration builds from different countries cannot be compared.");
|
|
74
|
+
}
|
|
75
|
+
const country = current.index.country;
|
|
76
|
+
const currentBuildArtifactId = current.index.buildArtifactId;
|
|
77
|
+
const candidateBuildArtifactId = candidate.index.buildArtifactId;
|
|
78
|
+
const pairArtifactId = calibrationComparisonPairArtifactId(
|
|
79
|
+
country,
|
|
80
|
+
currentBuildArtifactId,
|
|
81
|
+
candidateBuildArtifactId,
|
|
82
|
+
);
|
|
83
|
+
const dataset = buildTargetChangeDatasetFromSummaries(
|
|
84
|
+
{
|
|
85
|
+
country,
|
|
86
|
+
comparison: current.index.targetComparison,
|
|
87
|
+
targets: current.targetSummaries,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
country,
|
|
91
|
+
comparison: candidate.index.targetComparison,
|
|
92
|
+
targets: candidate.targetSummaries,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
const buildForMode = (mode: TargetChangeMode): CalibrationTreeBundle => {
|
|
96
|
+
const buildArtifactId = calibrationComparisonBuildArtifactId(
|
|
97
|
+
pairArtifactId,
|
|
98
|
+
mode,
|
|
99
|
+
);
|
|
100
|
+
return buildCalibrationTreeBundle({
|
|
101
|
+
country,
|
|
102
|
+
buildArtifactId,
|
|
103
|
+
buildKind: "comparison",
|
|
104
|
+
sourceId: `${currentBuildArtifactId}:${candidateBuildArtifactId}:${mode}`,
|
|
105
|
+
label: `${dataset.current.releaseId} → ${dataset.candidate.releaseId}`,
|
|
106
|
+
createdAt: candidate.index.build.createdAt,
|
|
107
|
+
releaseId: dataset.candidate.releaseId,
|
|
108
|
+
hfRepo: null,
|
|
109
|
+
hfCommitSha: null,
|
|
110
|
+
sourceArtifacts: {
|
|
111
|
+
calibrationDiagnostics: null,
|
|
112
|
+
buildManifest: null,
|
|
113
|
+
releaseManifest: null,
|
|
114
|
+
demographics: null,
|
|
115
|
+
comparisonCurrentIndex: {
|
|
116
|
+
path: calibrationTreePartPath(country, currentBuildArtifactId, "index"),
|
|
117
|
+
sha256: current.indexSha256,
|
|
118
|
+
},
|
|
119
|
+
comparisonCandidateIndex: {
|
|
120
|
+
path: calibrationTreePartPath(country, candidateBuildArtifactId, "index"),
|
|
121
|
+
sha256: candidate.indexSha256,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
rows: rowsForMode(dataset.rows, mode),
|
|
125
|
+
calibrationProvenance: dataset.candidate.calibrationProvenance,
|
|
126
|
+
lossAttributionAvailable: dataset.available,
|
|
127
|
+
comparison: candidate.index.targetComparison,
|
|
128
|
+
comparisonResult: {
|
|
129
|
+
pairArtifactId,
|
|
130
|
+
currentBuildArtifactId,
|
|
131
|
+
candidateBuildArtifactId,
|
|
132
|
+
mode,
|
|
133
|
+
available: dataset.available && dataset.summaries[mode] != null,
|
|
134
|
+
reason: dataset.reason ?? dataset.modeReasons[mode],
|
|
135
|
+
current: dataset.current,
|
|
136
|
+
candidate: dataset.candidate,
|
|
137
|
+
methodology: dataset.methodology,
|
|
138
|
+
matching: dataset.matching,
|
|
139
|
+
summary: dataset.summaries[mode],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
pairArtifactId,
|
|
146
|
+
reported: buildForMode("reported"),
|
|
147
|
+
shared: buildForMode("shared"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export const FIT_BANDS = [
|
|
2
|
+
"0_5",
|
|
3
|
+
"5_10",
|
|
4
|
+
"10_20",
|
|
5
|
+
"20_40",
|
|
6
|
+
"40_plus",
|
|
7
|
+
"unscored",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type FitBand = (typeof FIT_BANDS)[number];
|
|
11
|
+
export const COMPARISON_FITS = [
|
|
12
|
+
"improved",
|
|
13
|
+
"regressed",
|
|
14
|
+
"unchanged",
|
|
15
|
+
"not_applicable",
|
|
16
|
+
] as const;
|
|
17
|
+
export type ComparisonFit = (typeof COMPARISON_FITS)[number];
|
|
18
|
+
export type CalibrationStatus = "included" | "skipped";
|
|
19
|
+
export type ExplorerBreakdown = "program" | "geography";
|
|
20
|
+
|
|
21
|
+
export interface ExplorerDimensionSelection {
|
|
22
|
+
key: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExplorerPath {
|
|
28
|
+
source?: string;
|
|
29
|
+
program?: string;
|
|
30
|
+
geography?: string;
|
|
31
|
+
dimensions: ExplorerDimensionSelection[];
|
|
32
|
+
target?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ExplorerFilters {
|
|
36
|
+
geographyLevels: string[];
|
|
37
|
+
geographies: string[];
|
|
38
|
+
fitBands: FitBand[];
|
|
39
|
+
comparisonFits: ComparisonFit[];
|
|
40
|
+
calibrationStatuses: CalibrationStatus[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ExplorerState {
|
|
44
|
+
breakdown: ExplorerBreakdown;
|
|
45
|
+
path: ExplorerPath;
|
|
46
|
+
filters: ExplorerFilters;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ExplorerNodeSelection =
|
|
50
|
+
| { kind: "program"; source: string; value: string }
|
|
51
|
+
| { kind: "geography"; value: string }
|
|
52
|
+
| { kind: "dimension_value"; key: string; label: string; value: string }
|
|
53
|
+
| { kind: "target"; value: string };
|
|
54
|
+
|
|
55
|
+
export type ExplorerAction =
|
|
56
|
+
| { type: "select"; selection: ExplorerNodeSelection }
|
|
57
|
+
| { type: "navigate"; path: ExplorerPath }
|
|
58
|
+
| { type: "up" }
|
|
59
|
+
| { type: "breakdown"; breakdown: ExplorerBreakdown }
|
|
60
|
+
| { type: "filters"; filters: ExplorerFilters }
|
|
61
|
+
| { type: "clear_target" };
|
|
62
|
+
|
|
63
|
+
export function calibrationExplorerSourceIdentity({
|
|
64
|
+
country,
|
|
65
|
+
release,
|
|
66
|
+
stagingRunId,
|
|
67
|
+
}: {
|
|
68
|
+
country: string;
|
|
69
|
+
release?: string;
|
|
70
|
+
stagingRunId?: string;
|
|
71
|
+
}): string {
|
|
72
|
+
return JSON.stringify(
|
|
73
|
+
stagingRunId
|
|
74
|
+
? [country, "staging", stagingRunId]
|
|
75
|
+
: [country, "release", release ?? null],
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createExplorerFilters(): ExplorerFilters {
|
|
80
|
+
return {
|
|
81
|
+
geographyLevels: [],
|
|
82
|
+
geographies: [],
|
|
83
|
+
fitBands: [],
|
|
84
|
+
comparisonFits: [],
|
|
85
|
+
calibrationStatuses: [],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createExplorerState(): ExplorerState {
|
|
90
|
+
return {
|
|
91
|
+
breakdown: "program",
|
|
92
|
+
path: { dimensions: [] },
|
|
93
|
+
filters: createExplorerFilters(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function selectExplorerNode(
|
|
98
|
+
state: ExplorerState,
|
|
99
|
+
selection: ExplorerNodeSelection,
|
|
100
|
+
): ExplorerState {
|
|
101
|
+
const path = { ...state.path, dimensions: [...state.path.dimensions] };
|
|
102
|
+
delete path.target;
|
|
103
|
+
if (selection.kind === "program") {
|
|
104
|
+
if (state.breakdown === "geography" && !path.geography) return state;
|
|
105
|
+
path.source = selection.source;
|
|
106
|
+
path.program = selection.value;
|
|
107
|
+
if (state.breakdown === "program") delete path.geography;
|
|
108
|
+
path.dimensions = [];
|
|
109
|
+
} else if (selection.kind === "geography") {
|
|
110
|
+
if (state.breakdown === "program" && (!path.source || !path.program)) {
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
if (state.breakdown === "geography") {
|
|
114
|
+
delete path.source;
|
|
115
|
+
delete path.program;
|
|
116
|
+
}
|
|
117
|
+
path.geography = selection.value;
|
|
118
|
+
path.dimensions = [];
|
|
119
|
+
} else if (selection.kind === "dimension_value") {
|
|
120
|
+
if (!path.geography && (!path.source || !path.program)) return state;
|
|
121
|
+
path.dimensions = [
|
|
122
|
+
...path.dimensions.filter((dimension) => dimension.key !== selection.key),
|
|
123
|
+
{
|
|
124
|
+
key: selection.key,
|
|
125
|
+
label: selection.label,
|
|
126
|
+
value: selection.value,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
} else {
|
|
130
|
+
if (!path.geography && (!path.source || !path.program)) return state;
|
|
131
|
+
path.target = selection.value;
|
|
132
|
+
}
|
|
133
|
+
return { ...state, path };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function nextLevelExplorerStates(
|
|
137
|
+
state: ExplorerState,
|
|
138
|
+
selections: ExplorerNodeSelection[],
|
|
139
|
+
): ExplorerState[] {
|
|
140
|
+
return selections.flatMap((selection) =>
|
|
141
|
+
selection.kind === "target"
|
|
142
|
+
? []
|
|
143
|
+
: [selectExplorerNode(state, selection)],
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function parentExplorerState(state: ExplorerState): ExplorerState {
|
|
148
|
+
const path = { ...state.path, dimensions: [...state.path.dimensions] };
|
|
149
|
+
delete path.target;
|
|
150
|
+
if (path.dimensions.length) {
|
|
151
|
+
path.dimensions.pop();
|
|
152
|
+
} else if (path.geography && path.source && path.program) {
|
|
153
|
+
if (state.breakdown === "program") {
|
|
154
|
+
delete path.geography;
|
|
155
|
+
} else {
|
|
156
|
+
delete path.program;
|
|
157
|
+
delete path.source;
|
|
158
|
+
}
|
|
159
|
+
} else if (path.geography) {
|
|
160
|
+
delete path.geography;
|
|
161
|
+
} else if (path.program || path.source) {
|
|
162
|
+
delete path.program;
|
|
163
|
+
delete path.source;
|
|
164
|
+
}
|
|
165
|
+
return { ...state, path };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function explorerReducer(
|
|
169
|
+
state: ExplorerState,
|
|
170
|
+
action: ExplorerAction,
|
|
171
|
+
): ExplorerState {
|
|
172
|
+
if (action.type === "select") {
|
|
173
|
+
return selectExplorerNode(state, action.selection);
|
|
174
|
+
}
|
|
175
|
+
if (action.type === "navigate") {
|
|
176
|
+
const path = {
|
|
177
|
+
...action.path,
|
|
178
|
+
dimensions: [...action.path.dimensions],
|
|
179
|
+
};
|
|
180
|
+
delete path.target;
|
|
181
|
+
return { ...state, path };
|
|
182
|
+
}
|
|
183
|
+
if (action.type === "up") return parentExplorerState(state);
|
|
184
|
+
if (action.type === "breakdown") {
|
|
185
|
+
return {
|
|
186
|
+
...state,
|
|
187
|
+
breakdown: action.breakdown,
|
|
188
|
+
path: { dimensions: [] },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (action.type === "filters") {
|
|
192
|
+
return {
|
|
193
|
+
...state,
|
|
194
|
+
filters: action.filters,
|
|
195
|
+
path: { ...state.path, dimensions: [...state.path.dimensions], target: undefined },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
...state,
|
|
200
|
+
path: { ...state.path, dimensions: [...state.path.dimensions], target: undefined },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { readCalibrationTreeManifest } from "./calibration-tree-blob";
|
|
2
|
+
import {
|
|
3
|
+
calibrationTreeManifestEntry,
|
|
4
|
+
releaseDirectoryOf,
|
|
5
|
+
} from "./calibration-tree-manifest";
|
|
6
|
+
import type { CalibrationTreeManifest } from "./calibration-tree-manifest";
|
|
7
|
+
import {
|
|
8
|
+
countryRegistration,
|
|
9
|
+
type MicrocosmCountry,
|
|
10
|
+
} from "./countries";
|
|
11
|
+
import { isBuildFolderId, parseBuildFolderId } from "./release-record";
|
|
12
|
+
|
|
13
|
+
export interface CalibrationReleaseLocation {
|
|
14
|
+
buildArtifactId: string;
|
|
15
|
+
releaseId: string;
|
|
16
|
+
/**
|
|
17
|
+
* The `releases/` folder holding the files: releaseId, which for a build
|
|
18
|
+
* in its own folder is `<release>/<build>`, unless releaseId is a tag read
|
|
19
|
+
* from its release's folder.
|
|
20
|
+
*/
|
|
21
|
+
releaseDirectory: string;
|
|
22
|
+
hfCommitSha: string;
|
|
23
|
+
updatedAt: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CalibrationReleaseNotFoundError extends Error {}
|
|
27
|
+
|
|
28
|
+
function hfHeaders(): HeadersInit | undefined {
|
|
29
|
+
const token = process.env.HF_TOKEN ?? process.env.HUGGINGFACE_TOKEN;
|
|
30
|
+
return token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether a value may name a release or Hugging Face revision: safe to put
|
|
35
|
+
* in a Hugging Face URL, and never `latest`, which names no fixed release.
|
|
36
|
+
*/
|
|
37
|
+
export function isReleaseId(value: unknown): value is string {
|
|
38
|
+
return typeof value === "string" &&
|
|
39
|
+
/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) &&
|
|
40
|
+
value !== "latest";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safeReleaseId(value: string, label = "release"): string {
|
|
44
|
+
if (!isReleaseId(value)) {
|
|
45
|
+
throw new Error(`Invalid ${label} id.`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function repository(country: MicrocosmCountry): string {
|
|
51
|
+
const registration = countryRegistration(country);
|
|
52
|
+
return (
|
|
53
|
+
(registration.repo_env ? process.env[registration.repo_env] : undefined) ??
|
|
54
|
+
registration.repo
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function repositoryRevision(country: MicrocosmCountry): string {
|
|
59
|
+
const registration = countryRegistration(country);
|
|
60
|
+
return (
|
|
61
|
+
(registration.revision_env
|
|
62
|
+
? process.env[registration.revision_env]
|
|
63
|
+
: undefined) ?? registration.revision
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolveHfRevisionSha(
|
|
68
|
+
country: MicrocosmCountry,
|
|
69
|
+
revision: string,
|
|
70
|
+
revalidate = 3600,
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const safeRevision = safeReleaseId(revision, "Hugging Face revision");
|
|
73
|
+
const repo = repository(country);
|
|
74
|
+
const response = await fetch(
|
|
75
|
+
`https://huggingface.co/api/datasets/${repo}/revision/${encodeURIComponent(safeRevision)}`,
|
|
76
|
+
{
|
|
77
|
+
headers: hfHeaders(),
|
|
78
|
+
// Each Hugging Face lookup in this module stops after 30 seconds. The
|
|
79
|
+
// publisher reads one revision per release tag, so no slow response
|
|
80
|
+
// may hold its run indefinitely.
|
|
81
|
+
signal: AbortSignal.timeout(30_000),
|
|
82
|
+
...(revalidate <= 0
|
|
83
|
+
? { cache: "no-store" as const }
|
|
84
|
+
: { next: { revalidate } }),
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
if (response.status === 404) {
|
|
88
|
+
throw new CalibrationReleaseNotFoundError(
|
|
89
|
+
`Hugging Face release ${revision} was not found for ${country}.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Hugging Face revision lookup failed with ${response.status}.`);
|
|
94
|
+
}
|
|
95
|
+
const value = (await response.json()) as { sha?: unknown };
|
|
96
|
+
const sha = typeof value.sha === "string" ? value.sha.toLowerCase() : "";
|
|
97
|
+
if (!/^[0-9a-f]{40,64}$/.test(sha)) {
|
|
98
|
+
throw new Error(`Hugging Face returned an invalid commit SHA for ${revision}.`);
|
|
99
|
+
}
|
|
100
|
+
return sha;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a release directory that was published without a matching HF tag.
|
|
105
|
+
*
|
|
106
|
+
* The expanded tree response records the commit which last changed each file.
|
|
107
|
+
* The newest of those commits is the first immutable snapshot that contains the
|
|
108
|
+
* current versions of every release artifact in a linear repository history.
|
|
109
|
+
* Callers still fetch every required artifact at the returned commit, so an
|
|
110
|
+
* incomplete or inconsistent snapshot fails publication.
|
|
111
|
+
*/
|
|
112
|
+
export async function resolveHfReleaseDirectorySha(
|
|
113
|
+
country: MicrocosmCountry,
|
|
114
|
+
release: string,
|
|
115
|
+
revalidate = 0,
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
const revision = repositoryRevision(country);
|
|
118
|
+
const safeRelease = safeReleaseId(release);
|
|
119
|
+
const repo = repository(country);
|
|
120
|
+
const prefix = `releases/${safeRelease}/`;
|
|
121
|
+
const response = await fetch(
|
|
122
|
+
`https://huggingface.co/api/datasets/${repo}/tree/` +
|
|
123
|
+
`${encodeURIComponent(revision)}/releases/${encodeURIComponent(safeRelease)}` +
|
|
124
|
+
"?recursive=false&expand=true",
|
|
125
|
+
{
|
|
126
|
+
headers: hfHeaders(),
|
|
127
|
+
signal: AbortSignal.timeout(30_000),
|
|
128
|
+
...(revalidate <= 0
|
|
129
|
+
? { cache: "no-store" as const }
|
|
130
|
+
: { next: { revalidate } }),
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
if (response.status === 404) {
|
|
134
|
+
throw new CalibrationReleaseNotFoundError(
|
|
135
|
+
`Hugging Face release directory ${release} was not found for ${country}.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Hugging Face release directory lookup failed with ${response.status}.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return newestCommitOfCalibratedFolder(
|
|
145
|
+
await response.json(),
|
|
146
|
+
prefix,
|
|
147
|
+
`release ${release}`,
|
|
148
|
+
country,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The commit that wrote a build folder, `releases/<release>/<build>/`, as
|
|
154
|
+
* the folder is at `revision` (an exact commit, so the folder and the
|
|
155
|
+
* `releases.json` read beside it come from one snapshot). A build folder is
|
|
156
|
+
* never changed after publishing, so the newest commit behind its files is
|
|
157
|
+
* the one that wrote it, and every file is read there.
|
|
158
|
+
*/
|
|
159
|
+
export async function resolveHfBuildFolderSha(
|
|
160
|
+
country: MicrocosmCountry,
|
|
161
|
+
folderId: string,
|
|
162
|
+
revision: string,
|
|
163
|
+
revalidate = 0,
|
|
164
|
+
): Promise<string> {
|
|
165
|
+
const folder = parseBuildFolderId(folderId);
|
|
166
|
+
if (!folder) throw new Error("Invalid build folder.");
|
|
167
|
+
const safeRevision = safeReleaseId(revision, "Hugging Face revision");
|
|
168
|
+
const repo = repository(country);
|
|
169
|
+
const response = await fetch(
|
|
170
|
+
`https://huggingface.co/api/datasets/${repo}/tree/${encodeURIComponent(safeRevision)}` +
|
|
171
|
+
`/releases/${encodeURIComponent(folder.release)}/${encodeURIComponent(folder.build)}` +
|
|
172
|
+
"?recursive=false&expand=true",
|
|
173
|
+
{
|
|
174
|
+
headers: hfHeaders(),
|
|
175
|
+
signal: AbortSignal.timeout(30_000),
|
|
176
|
+
...(revalidate <= 0
|
|
177
|
+
? { cache: "no-store" as const }
|
|
178
|
+
: { next: { revalidate } }),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
if (response.status === 404) {
|
|
182
|
+
throw new CalibrationReleaseNotFoundError(
|
|
183
|
+
`Hugging Face build folder releases/${folderId}/ was not found for ${country}.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(`Hugging Face build folder lookup failed with ${response.status}.`);
|
|
188
|
+
}
|
|
189
|
+
return newestCommitOfCalibratedFolder(
|
|
190
|
+
await response.json(),
|
|
191
|
+
`releases/${folderId}/`,
|
|
192
|
+
`build folder releases/${folderId}/`,
|
|
193
|
+
country,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The newest commit behind a folder listing's files (an expanded Hugging
|
|
198
|
+
// Face tree response), refusing a folder without calibration diagnostics.
|
|
199
|
+
function newestCommitOfCalibratedFolder(
|
|
200
|
+
listing: unknown,
|
|
201
|
+
prefix: string,
|
|
202
|
+
subject: string,
|
|
203
|
+
country: MicrocosmCountry,
|
|
204
|
+
): string {
|
|
205
|
+
const entries = listing as Array<{
|
|
206
|
+
path?: unknown;
|
|
207
|
+
type?: unknown;
|
|
208
|
+
lastCommit?: { id?: unknown; date?: unknown };
|
|
209
|
+
}>;
|
|
210
|
+
if (!Array.isArray(entries)) {
|
|
211
|
+
throw new Error("Hugging Face returned an invalid release directory listing.");
|
|
212
|
+
}
|
|
213
|
+
const files = entries.filter(
|
|
214
|
+
(entry) =>
|
|
215
|
+
entry.type === "file" &&
|
|
216
|
+
typeof entry.path === "string" &&
|
|
217
|
+
entry.path.startsWith(prefix),
|
|
218
|
+
);
|
|
219
|
+
if (!files.some(
|
|
220
|
+
(entry) => entry.path === `${prefix}calibration_diagnostics.json`,
|
|
221
|
+
)) {
|
|
222
|
+
throw new CalibrationReleaseNotFoundError(
|
|
223
|
+
`Hugging Face ${subject} has no calibration diagnostics for ${country}.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const commits = files.map((entry) => {
|
|
228
|
+
const id = typeof entry.lastCommit?.id === "string"
|
|
229
|
+
? entry.lastCommit.id.toLowerCase()
|
|
230
|
+
: "";
|
|
231
|
+
const date = typeof entry.lastCommit?.date === "string"
|
|
232
|
+
? Date.parse(entry.lastCommit.date)
|
|
233
|
+
: Number.NaN;
|
|
234
|
+
if (!/^[0-9a-f]{40,64}$/.test(id) || !Number.isFinite(date)) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Hugging Face returned invalid commit metadata for ${subject}.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return { id, date };
|
|
240
|
+
});
|
|
241
|
+
commits.sort((left, right) => right.date - left.date);
|
|
242
|
+
if (!commits[0]) {
|
|
243
|
+
throw new CalibrationReleaseNotFoundError(
|
|
244
|
+
`Hugging Face ${subject} has no source files for ${country}.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return commits[0].id;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* When a Hugging Face commit was made: the one publication timestamp.
|
|
252
|
+
*
|
|
253
|
+
* Every calibration build pins the exact repository commit its bytes come
|
|
254
|
+
* from (`hfRepo` and `hfCommitSha`), so its publication time is that
|
|
255
|
+
* commit's date, read from that repository's commit log, whether the build
|
|
256
|
+
* is being published, migrated or loaded. A commit is identified by
|
|
257
|
+
* repository and SHA together, so callers holding recorded provenance pass
|
|
258
|
+
* the recorded repository rather than the one the current deployment
|
|
259
|
+
* configures. Release ids and tag names are not a source: microcosm's
|
|
260
|
+
* national releases are named by line (`microcosm-uk-2024-25-national`)
|
|
261
|
+
* and their immutable cut tags end in an attempt suffix. When the commit
|
|
262
|
+
* cannot be read this fails rather than substituting a timestamp with
|
|
263
|
+
* different semantics.
|
|
264
|
+
*/
|
|
265
|
+
export async function resolveHfRepositoryCommitPublishedAt(
|
|
266
|
+
repo: string,
|
|
267
|
+
sha: string,
|
|
268
|
+
revalidate = 0,
|
|
269
|
+
): Promise<string> {
|
|
270
|
+
if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
|
|
271
|
+
throw new Error(`Hugging Face repository ${repo} is not an owner/name id.`);
|
|
272
|
+
}
|
|
273
|
+
const commitSha = sha.toLowerCase();
|
|
274
|
+
if (!/^[0-9a-f]{40,64}$/.test(commitSha)) {
|
|
275
|
+
throw new Error(`Hugging Face commit ${sha} is not a commit SHA.`);
|
|
276
|
+
}
|
|
277
|
+
const response = await fetch(
|
|
278
|
+
`https://huggingface.co/api/datasets/${repo}/commits/${commitSha}?limit=1`,
|
|
279
|
+
{
|
|
280
|
+
headers: hfHeaders(),
|
|
281
|
+
signal: AbortSignal.timeout(30_000),
|
|
282
|
+
...(revalidate <= 0
|
|
283
|
+
? { cache: "no-store" as const }
|
|
284
|
+
: { next: { revalidate } }),
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
if (response.status === 404) {
|
|
288
|
+
throw new CalibrationReleaseNotFoundError(
|
|
289
|
+
`Hugging Face commit ${sha} was not found in ${repo}.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
throw new Error(`Hugging Face commit lookup failed with ${response.status}.`);
|
|
294
|
+
}
|
|
295
|
+
const commits = (await response.json()) as Array<{ id?: unknown; date?: unknown }>;
|
|
296
|
+
const first = Array.isArray(commits) ? commits[0] : undefined;
|
|
297
|
+
const id = typeof first?.id === "string" ? first.id.toLowerCase() : "";
|
|
298
|
+
const date = typeof first?.date === "string" ? Date.parse(first.date) : Number.NaN;
|
|
299
|
+
if (id !== commitSha || !Number.isFinite(date)) {
|
|
300
|
+
throw new Error(`Hugging Face returned invalid commit metadata for ${sha}.`);
|
|
301
|
+
}
|
|
302
|
+
return new Date(date).toISOString();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The publication timestamp of a commit in the country's configured repository. */
|
|
306
|
+
export async function resolveHfCommitPublishedAt(
|
|
307
|
+
country: MicrocosmCountry,
|
|
308
|
+
sha: string,
|
|
309
|
+
revalidate = 0,
|
|
310
|
+
): Promise<string> {
|
|
311
|
+
return resolveHfRepositoryCommitPublishedAt(repository(country), sha, revalidate);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* A requested release: "latest" (also for an empty request), a release id
|
|
316
|
+
* (a release folder or a tag), or a build folder, `<release>/<build>`.
|
|
317
|
+
*/
|
|
318
|
+
export function requestedReleaseId(requestedRelease: string): string {
|
|
319
|
+
if (!requestedRelease || requestedRelease === "latest") return "latest";
|
|
320
|
+
if (isBuildFolderId(requestedRelease)) return requestedRelease;
|
|
321
|
+
return safeReleaseId(requestedRelease);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function resolveCalibrationRelease(
|
|
325
|
+
country: MicrocosmCountry,
|
|
326
|
+
requestedRelease: string,
|
|
327
|
+
_revalidate = 3600,
|
|
328
|
+
): Promise<CalibrationReleaseLocation> {
|
|
329
|
+
const releaseId = requestedReleaseId(requestedRelease);
|
|
330
|
+
const { manifest } = await readCalibrationTreeManifest();
|
|
331
|
+
return calibrationReleaseFromManifest(manifest, country, releaseId);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function calibrationReleaseFromManifest(
|
|
335
|
+
manifest: CalibrationTreeManifest,
|
|
336
|
+
country: MicrocosmCountry,
|
|
337
|
+
releaseId = "latest",
|
|
338
|
+
): CalibrationReleaseLocation {
|
|
339
|
+
const entry = calibrationTreeManifestEntry(manifest, country, { releaseId });
|
|
340
|
+
if (!entry.releaseId) {
|
|
341
|
+
throw new Error(`Calibration build ${entry.buildArtifactId} is not a release.`);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
buildArtifactId: entry.buildArtifactId,
|
|
345
|
+
releaseId: entry.releaseId,
|
|
346
|
+
releaseDirectory: releaseDirectoryOf(entry) ?? entry.releaseId,
|
|
347
|
+
hfCommitSha: entry.hfCommitSha,
|
|
348
|
+
// Null only on an entry written before the publication timestamp was
|
|
349
|
+
// read from the pinned commit; the release loader then reads it from
|
|
350
|
+
// that same commit.
|
|
351
|
+
updatedAt: entry.updatedAt,
|
|
352
|
+
};
|
|
353
|
+
}
|