@sanity/ailf 4.5.0 → 5.0.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.
- package/canonical/grader-references/agent-harness-tools.yaml +42 -0
- package/canonical/grader-references/knowledge-probe-recall.yaml +36 -0
- package/canonical/grader-references/mcp-server-spec.yaml +51 -0
- package/canonical/grader-references/portable-text.yaml +48 -0
- package/config/rubrics.ts +38 -2
- package/dist/_vendor/ailf-core/artifact-registry.d.ts +197 -2
- package/dist/_vendor/ailf-core/artifact-registry.js +419 -5
- package/dist/_vendor/ailf-core/examples/index.d.ts +125 -26
- package/dist/_vendor/ailf-core/examples/index.js +146 -47
- package/dist/_vendor/ailf-core/ports/context.d.ts +26 -0
- package/dist/_vendor/ailf-core/ports/index.d.ts +2 -0
- package/dist/_vendor/ailf-core/ports/index.js +1 -0
- package/dist/_vendor/ailf-core/ports/llm-client.d.ts +112 -0
- package/dist/_vendor/ailf-core/ports/llm-client.js +68 -0
- package/dist/_vendor/ailf-core/ports/mode-handler.d.ts +15 -0
- package/dist/_vendor/ailf-core/schemas/branded-string.d.ts +40 -0
- package/dist/_vendor/ailf-core/schemas/branded-string.js +45 -0
- package/dist/_vendor/ailf-core/schemas/confidence-schema.d.ts +36 -0
- package/dist/_vendor/ailf-core/schemas/confidence-schema.js +32 -0
- package/dist/_vendor/ailf-core/schemas/eval-config.d.ts +1 -0
- package/dist/_vendor/ailf-core/schemas/eval-config.js +8 -4
- package/dist/_vendor/ailf-core/schemas/index.d.ts +2 -0
- package/dist/_vendor/ailf-core/schemas/index.js +9 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +1 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.js +1 -0
- package/dist/_vendor/ailf-core/schemas/pipeline.d.ts +34 -8
- package/dist/_vendor/ailf-core/schemas/pipeline.js +23 -1
- package/dist/_vendor/ailf-core/services/diagnosis/registry.d.ts +40 -0
- package/dist/_vendor/ailf-core/services/diagnosis/registry.js +25 -0
- package/dist/_vendor/ailf-core/services/diagnosis-runner.d.ts +19 -0
- package/dist/_vendor/ailf-core/services/diagnosis-runner.js +19 -0
- package/dist/_vendor/ailf-core/services/index.d.ts +2 -0
- package/dist/_vendor/ailf-core/services/index.js +5 -0
- package/dist/_vendor/ailf-core/services/report-to-markdown.js +3 -2
- package/dist/_vendor/ailf-core/types/attribution.d.ts +82 -0
- package/dist/_vendor/ailf-core/types/attribution.js +18 -0
- package/dist/_vendor/ailf-core/types/branded-ids.d.ts +26 -1
- package/dist/_vendor/ailf-core/types/branded-ids.js +80 -4
- package/dist/_vendor/ailf-core/types/confidence.d.ts +68 -0
- package/dist/_vendor/ailf-core/types/confidence.js +56 -0
- package/dist/_vendor/ailf-core/types/diagnosis.d.ts +169 -0
- package/dist/_vendor/ailf-core/types/diagnosis.js +17 -0
- package/dist/_vendor/ailf-core/types/generalized-task.d.ts +16 -1
- package/dist/_vendor/ailf-core/types/grader-judgment.d.ts +125 -0
- package/dist/_vendor/ailf-core/types/grader-judgment.js +30 -0
- package/dist/_vendor/ailf-core/types/index.d.ts +82 -29
- package/dist/_vendor/ailf-core/types/index.js +16 -1
- package/dist/_vendor/ailf-core/types/legacy-grader-judgment.d.ts +55 -0
- package/dist/_vendor/ailf-core/types/legacy-grader-judgment.js +30 -0
- package/dist/_vendor/ailf-core/types/pipeline-request.d.ts +1 -0
- package/dist/_vendor/ailf-core/types/repo-config.d.ts +8 -0
- package/dist/_vendor/ailf-shared/document-ref.d.ts +1 -1
- package/dist/adapters/api-client/build-request.d.ts +1 -0
- package/dist/adapters/api-client/build-request.js +3 -0
- package/dist/adapters/attribution/attribution-meta-writer.d.ts +35 -0
- package/dist/adapters/attribution/attribution-meta-writer.js +34 -0
- package/dist/adapters/attribution/index.d.ts +9 -0
- package/dist/adapters/attribution/index.js +8 -0
- package/dist/adapters/attribution/per-entry-attribution-writer.d.ts +56 -0
- package/dist/adapters/attribution/per-entry-attribution-writer.js +49 -0
- package/dist/adapters/config-sources/file-config-adapter.js +1 -0
- package/dist/adapters/grader-outputs/index.d.ts +10 -0
- package/dist/adapters/grader-outputs/index.js +8 -0
- package/dist/adapters/grader-outputs/legacy/index.d.ts +11 -0
- package/dist/adapters/grader-outputs/legacy/index.js +10 -0
- package/dist/adapters/grader-outputs/legacy/promptfoo-grader-output-legacy.d.ts +49 -0
- package/dist/adapters/grader-outputs/legacy/promptfoo-grader-output-legacy.js +48 -0
- package/dist/adapters/grader-outputs/promptfoo-grader-output.d.ts +102 -0
- package/dist/adapters/grader-outputs/promptfoo-grader-output.js +93 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/llm/anthropic-llm-client.d.ts +48 -0
- package/dist/adapters/llm/anthropic-llm-client.js +205 -0
- package/dist/adapters/llm/fake-llm-client.d.ts +49 -0
- package/dist/adapters/llm/fake-llm-client.js +63 -0
- package/dist/adapters/llm/index.d.ts +9 -0
- package/dist/adapters/llm/index.js +4 -0
- package/dist/adapters/llm/openai-llm-client.d.ts +44 -0
- package/dist/adapters/llm/openai-llm-client.js +168 -0
- package/dist/adapters/llm/pricing.d.ts +12 -0
- package/dist/adapters/llm/pricing.js +8 -0
- package/dist/adapters/llm/retry.d.ts +56 -0
- package/dist/adapters/llm/retry.js +66 -0
- package/dist/adapters/task-sources/content-lake-task-source.d.ts +5 -1
- package/dist/adapters/task-sources/content-lake-task-source.js +28 -2
- package/dist/adapters/task-sources/repo-schemas.d.ts +90 -22
- package/dist/adapters/task-sources/repo-schemas.js +19 -2
- package/dist/artifact-capture/api-gateway-artifact-writer.js +2 -1
- package/dist/artifact-capture/batching-api-gateway-artifact-writer.js +2 -1
- package/dist/artifact-capture/gcs-artifact-writer.js +3 -1
- package/dist/artifact-capture/local-fs-artifact-writer.js +3 -1
- package/dist/commands/calculate-scores.js +1 -1
- package/dist/commands/explain-handler.js +1 -1
- package/dist/commands/lookup-doc.d.ts +1 -1
- package/dist/commands/lookup-doc.js +3 -3
- package/dist/commands/pipeline-action.d.ts +6 -0
- package/dist/commands/pipeline-action.js +2 -0
- package/dist/commands/remote-pipeline.js +1 -0
- package/dist/composition-root.d.ts +59 -1
- package/dist/composition-root.js +95 -0
- package/dist/config/rubrics.ts +38 -2
- package/dist/grader/agent-harness.d.ts +14 -0
- package/dist/grader/agent-harness.js +17 -0
- package/dist/grader/common.d.ts +17 -0
- package/dist/grader/common.js +21 -0
- package/dist/grader/index.d.ts +38 -0
- package/dist/grader/index.js +75 -0
- package/dist/grader/knowledge-probe.d.ts +14 -0
- package/dist/grader/knowledge-probe.js +18 -0
- package/dist/grader/literacy.d.ts +13 -0
- package/dist/grader/literacy.js +17 -0
- package/dist/grader/mcp.d.ts +14 -0
- package/dist/grader/mcp.js +18 -0
- package/dist/orchestration/build-app-context.js +1 -0
- package/dist/orchestration/build-step-sequence.js +5 -0
- package/dist/orchestration/steps/calculate-scores-step.js +23 -1
- package/dist/orchestration/steps/compute-attribution-step.d.ts +44 -0
- package/dist/orchestration/steps/compute-attribution-step.js +279 -0
- package/dist/orchestration/steps/gap-analysis-step.js +35 -7
- package/dist/orchestration/steps/index.d.ts +1 -0
- package/dist/orchestration/steps/index.js +1 -0
- package/dist/pipeline/attribution.d.ts +15 -0
- package/dist/pipeline/attribution.js +18 -9
- package/dist/pipeline/borderline-consensus-runner.d.ts +63 -0
- package/dist/pipeline/borderline-consensus-runner.js +124 -0
- package/dist/pipeline/borderline-detector.d.ts +24 -0
- package/dist/pipeline/borderline-detector.js +26 -0
- package/dist/pipeline/calculate-scores.d.ts +114 -3
- package/dist/pipeline/calculate-scores.js +426 -24
- package/dist/pipeline/compiler/literacy-bridge.d.ts +1 -1
- package/dist/pipeline/compiler/literacy-bridge.js +35 -17
- package/dist/pipeline/compiler/rubric-resolution.d.ts +15 -0
- package/dist/pipeline/compiler/rubric-resolution.js +9 -1
- package/dist/pipeline/compute-attribution.d.ts +80 -0
- package/dist/pipeline/compute-attribution.js +196 -0
- package/dist/pipeline/failure-modes.d.ts +52 -17
- package/dist/pipeline/failure-modes.js +178 -117
- package/dist/pipeline/map-request-to-config.js +1 -0
- package/package.json +6 -4
|
@@ -24,6 +24,26 @@
|
|
|
24
24
|
* @see docs/design-docs/unified-run-artifacts.md (§ M1, § M5)
|
|
25
25
|
*/
|
|
26
26
|
import { z } from "zod";
|
|
27
|
+
/**
|
|
28
|
+
* Opt-in marker for the bulk-versioned-with-report-axis carve-out
|
|
29
|
+
* (Plan 01-03 / ITER2-BLOCKER-01). Any descriptor that needs to combine
|
|
30
|
+
* `layout: "bulk"` with a `report` axis (e.g. the post-hoc diagnosis
|
|
31
|
+
* artifact) must set
|
|
32
|
+
* `pathSafetyMarker: BULK_VERSIONED_WITH_REPORT_AXIS` to opt in to the
|
|
33
|
+
* carve-out in `assertValidArtifactDescriptor`.
|
|
34
|
+
*
|
|
35
|
+
* `Symbol.for(...)` is registry-keyed: it survives minification (the
|
|
36
|
+
* key is a string literal that minifiers cannot rename) and yields
|
|
37
|
+
* cross-module-instance equality by spec. This makes it bundler-stable
|
|
38
|
+
* — a deliberate replacement for any source-text reflection on the
|
|
39
|
+
* descriptor's `objectPath` closure body.
|
|
40
|
+
*
|
|
41
|
+
* The marker alone does NOT trigger the carve-out — the four structural
|
|
42
|
+
* conditions (bulk + versionedBy + run+report axes) must also hold.
|
|
43
|
+
* Conversely, the structural conditions alone do NOT trigger the
|
|
44
|
+
* carve-out without the marker. This is an explicit opt-in.
|
|
45
|
+
*/
|
|
46
|
+
export const BULK_VERSIONED_WITH_REPORT_AXIS = Symbol.for("ailf.artifactRegistry.bulkVersionedWithReportAxis");
|
|
27
47
|
// ---------------------------------------------------------------------------
|
|
28
48
|
// Path + key helpers
|
|
29
49
|
// ---------------------------------------------------------------------------
|
|
@@ -59,6 +79,221 @@ function perEntryPathBuilder(slug, mime) {
|
|
|
59
79
|
return `runs/${runId}/${slug}/${sanitized}.${ext}`;
|
|
60
80
|
};
|
|
61
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Bulk-shaped path builder with a `{version}` segment appended to the
|
|
84
|
+
* filename stem (D0050). Used by descriptors that opt into the versioning
|
|
85
|
+
* axis via `versionedBy`. Multiple versions coexist under the same run
|
|
86
|
+
* prefix as siblings:
|
|
87
|
+
*
|
|
88
|
+
* `runs/{runId}/{slug}-v1.{ext}`
|
|
89
|
+
* `runs/{runId}/{slug}-v2.{ext}`
|
|
90
|
+
*
|
|
91
|
+
* The version segment is sanitized via `sanitizeEntryKey` so versions
|
|
92
|
+
* containing `/` or wire separators don't accidentally nest into
|
|
93
|
+
* subdirectories. Empty / whitespace-only versions are rejected — a
|
|
94
|
+
* versioned descriptor with no version is a programmer error, not silently
|
|
95
|
+
* collapsed to an unversioned path.
|
|
96
|
+
*/
|
|
97
|
+
export function versionedPathBuilder(slug, mime) {
|
|
98
|
+
const ext = mimeExtension(mime);
|
|
99
|
+
return (runId, _entryKey, version) => {
|
|
100
|
+
if (version === undefined || version.trim() === "") {
|
|
101
|
+
throw new Error(`Artifact "${slug}" uses versioned layout; a non-empty version is required`);
|
|
102
|
+
}
|
|
103
|
+
if (hasControlChars(version)) {
|
|
104
|
+
throw new Error(`Artifact "${slug}" version must not contain control characters`);
|
|
105
|
+
}
|
|
106
|
+
const sanitized = sanitizeEntryKey(version);
|
|
107
|
+
return `runs/${runId}/${slug}-${sanitized}.${ext}`;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Synchronous SHA-256 over a UTF-8-encoded string returning a lowercase
|
|
112
|
+
* hex digest. Implemented inline because `@sanity/ailf-core` is the
|
|
113
|
+
* domain kernel — it intentionally avoids a `@types/node` dependency
|
|
114
|
+
* (matching the existing `byteLengthUtf8` rationale below) and the
|
|
115
|
+
* Web Crypto `crypto.subtle.digest` API is async-only, which would
|
|
116
|
+
* force `ArtifactObjectPath` to become async.
|
|
117
|
+
*
|
|
118
|
+
* Used only for the diagnosis descriptor's `cardSetShortHash` (the
|
|
119
|
+
* filename suffix) — collision resistance, not authentication, is the
|
|
120
|
+
* relevant property. FIPS 180-4 reference implementation.
|
|
121
|
+
*/
|
|
122
|
+
function sha256Hex(input) {
|
|
123
|
+
const K = [
|
|
124
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
125
|
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
126
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
127
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
128
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
129
|
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
130
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
131
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
132
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
133
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
134
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
135
|
+
];
|
|
136
|
+
// UTF-8 encode (without TextEncoder reliance — kernel runs in both Node
|
|
137
|
+
// and the browser; both have TextEncoder by spec, but staying explicit
|
|
138
|
+
// mirrors the byteLengthUtf8 helper below).
|
|
139
|
+
const bytes = [];
|
|
140
|
+
for (let i = 0; i < input.length; i++) {
|
|
141
|
+
let c = input.charCodeAt(i);
|
|
142
|
+
if (c < 0x80) {
|
|
143
|
+
bytes.push(c);
|
|
144
|
+
}
|
|
145
|
+
else if (c < 0x800) {
|
|
146
|
+
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
147
|
+
}
|
|
148
|
+
else if (c >= 0xd800 && c < 0xdc00 && i + 1 < input.length) {
|
|
149
|
+
// surrogate pair
|
|
150
|
+
const c2 = input.charCodeAt(i + 1);
|
|
151
|
+
const cp = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff));
|
|
152
|
+
bytes.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Pre-processing: padding
|
|
160
|
+
const bitLen = bytes.length * 8;
|
|
161
|
+
bytes.push(0x80);
|
|
162
|
+
while (bytes.length % 64 !== 56)
|
|
163
|
+
bytes.push(0);
|
|
164
|
+
// 64-bit big-endian length (high 32 bits zero — input < 2^32 bits is the
|
|
165
|
+
// realistic ceiling here).
|
|
166
|
+
bytes.push(0, 0, 0, 0);
|
|
167
|
+
bytes.push((bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 8) & 0xff, bitLen & 0xff);
|
|
168
|
+
let h0 = 0x6a09e667;
|
|
169
|
+
let h1 = 0xbb67ae85;
|
|
170
|
+
let h2 = 0x3c6ef372;
|
|
171
|
+
let h3 = 0xa54ff53a;
|
|
172
|
+
let h4 = 0x510e527f;
|
|
173
|
+
let h5 = 0x9b05688c;
|
|
174
|
+
let h6 = 0x1f83d9ab;
|
|
175
|
+
let h7 = 0x5be0cd19;
|
|
176
|
+
const W = new Array(64);
|
|
177
|
+
for (let chunk = 0; chunk < bytes.length; chunk += 64) {
|
|
178
|
+
for (let i = 0; i < 16; i++) {
|
|
179
|
+
const j = chunk + i * 4;
|
|
180
|
+
W[i] =
|
|
181
|
+
((bytes[j] << 24) |
|
|
182
|
+
(bytes[j + 1] << 16) |
|
|
183
|
+
(bytes[j + 2] << 8) |
|
|
184
|
+
bytes[j + 3]) >>>
|
|
185
|
+
0;
|
|
186
|
+
}
|
|
187
|
+
for (let i = 16; i < 64; i++) {
|
|
188
|
+
const a = W[i - 15];
|
|
189
|
+
const b = W[i - 2];
|
|
190
|
+
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
|
191
|
+
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
|
192
|
+
W[i] = (W[i - 16] + s0 + W[i - 7] + s1) >>> 0;
|
|
193
|
+
}
|
|
194
|
+
let a = h0;
|
|
195
|
+
let b = h1;
|
|
196
|
+
let c = h2;
|
|
197
|
+
let d = h3;
|
|
198
|
+
let e = h4;
|
|
199
|
+
let f = h5;
|
|
200
|
+
let g = h6;
|
|
201
|
+
let h = h7;
|
|
202
|
+
for (let i = 0; i < 64; i++) {
|
|
203
|
+
const S1 = ((e >>> 6) | (e << 26)) ^
|
|
204
|
+
((e >>> 11) | (e << 21)) ^
|
|
205
|
+
((e >>> 25) | (e << 7));
|
|
206
|
+
const ch = (e & f) ^ (~e & g);
|
|
207
|
+
const t1 = (h + S1 + ch + K[i] + W[i]) >>> 0;
|
|
208
|
+
const S0 = ((a >>> 2) | (a << 30)) ^
|
|
209
|
+
((a >>> 13) | (a << 19)) ^
|
|
210
|
+
((a >>> 22) | (a << 10));
|
|
211
|
+
const mj = (a & b) ^ (a & c) ^ (b & c);
|
|
212
|
+
const t2 = (S0 + mj) >>> 0;
|
|
213
|
+
h = g;
|
|
214
|
+
g = f;
|
|
215
|
+
f = e;
|
|
216
|
+
e = (d + t1) >>> 0;
|
|
217
|
+
d = c;
|
|
218
|
+
c = b;
|
|
219
|
+
b = a;
|
|
220
|
+
a = (t1 + t2) >>> 0;
|
|
221
|
+
}
|
|
222
|
+
h0 = (h0 + a) >>> 0;
|
|
223
|
+
h1 = (h1 + b) >>> 0;
|
|
224
|
+
h2 = (h2 + c) >>> 0;
|
|
225
|
+
h3 = (h3 + d) >>> 0;
|
|
226
|
+
h4 = (h4 + e) >>> 0;
|
|
227
|
+
h5 = (h5 + f) >>> 0;
|
|
228
|
+
h6 = (h6 + g) >>> 0;
|
|
229
|
+
h7 = (h7 + h) >>> 0;
|
|
230
|
+
}
|
|
231
|
+
const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
|
|
232
|
+
return (toHex(h0) +
|
|
233
|
+
toHex(h1) +
|
|
234
|
+
toHex(h2) +
|
|
235
|
+
toHex(h3) +
|
|
236
|
+
toHex(h4) +
|
|
237
|
+
toHex(h5) +
|
|
238
|
+
toHex(h6) +
|
|
239
|
+
toHex(h7));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Two-axis post-hoc path builder for the diagnosis descriptor (Plan 01-03,
|
|
243
|
+
* D-09). Filename: `diagnosis-{diagnosisVersion}-{cardSetShortHash}.json`
|
|
244
|
+
* where `cardSetShortHash = sha256(cardVersion).slice(0, 12)`.
|
|
245
|
+
*
|
|
246
|
+
* The compound version argument is `${diagnosisVersion}|${cardVersion}`
|
|
247
|
+
* to fit the existing 3-arg `objectPath` signature without growing the
|
|
248
|
+
* shared `ArtifactObjectPath` shape. Callers should use
|
|
249
|
+
* `encodeDiagnosisPathVersion()` rather than building the compound
|
|
250
|
+
* string by hand.
|
|
251
|
+
*
|
|
252
|
+
* The diagnosis descriptor opts into the bulk-versioned-with-report-axis
|
|
253
|
+
* carve-out via `pathSafetyMarker: BULK_VERSIONED_WITH_REPORT_AXIS`
|
|
254
|
+
* (ITER2-BLOCKER-01). The builder itself does not need to know about
|
|
255
|
+
* the marker — it just builds the path; the carve-out is a registration-
|
|
256
|
+
* time concern in `assertValidArtifactDescriptor`.
|
|
257
|
+
*/
|
|
258
|
+
export function diagnosisPathBuilder() {
|
|
259
|
+
return (runId, reportId, version) => {
|
|
260
|
+
if (!reportId)
|
|
261
|
+
throw new Error("diagnosisPathBuilder requires reportId axis");
|
|
262
|
+
if (!version)
|
|
263
|
+
throw new Error("diagnosisPathBuilder requires compound version");
|
|
264
|
+
if (hasControlChars(reportId))
|
|
265
|
+
throw new Error("diagnosisPathBuilder reportId must not contain control characters");
|
|
266
|
+
const sep = version.indexOf("|");
|
|
267
|
+
if (sep <= 0)
|
|
268
|
+
throw new Error("diagnosisPathBuilder version must be ${diagnosisVersion}|${cardVersion}");
|
|
269
|
+
const dv = version.slice(0, sep);
|
|
270
|
+
const cv = version.slice(sep + 1);
|
|
271
|
+
if (hasControlChars(dv))
|
|
272
|
+
throw new Error("diagnosisPathBuilder diagnosisVersion must not contain control characters");
|
|
273
|
+
// Sanitize reportId and diagnosisVersion segments — `/` would otherwise
|
|
274
|
+
// create unintended GCS subdirectories and break the
|
|
275
|
+
// `runs/{runId}/{reportId}/diagnosis-…` path-prefix containment
|
|
276
|
+
// guarantee. Mirrors `versionedPathBuilder` (above).
|
|
277
|
+
const sanitizedReportId = sanitizeEntryKey(reportId);
|
|
278
|
+
const sanitizedDv = sanitizeEntryKey(dv);
|
|
279
|
+
const cardSetShortHash = sha256Hex(cv).slice(0, 12);
|
|
280
|
+
return `runs/${runId}/${sanitizedReportId}/diagnosis-${sanitizedDv}-${cardSetShortHash}.json`;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Pack the diagnosis descriptor's two version segments into the existing
|
|
285
|
+
* single-string `version` slot of `ArtifactObjectPath`. Separator is `|`;
|
|
286
|
+
* `diagnosisVersion` MUST NOT contain `|` (the function rejects that case).
|
|
287
|
+
*/
|
|
288
|
+
export function encodeDiagnosisPathVersion(diagnosisVersion, cardVersion) {
|
|
289
|
+
if (diagnosisVersion === "")
|
|
290
|
+
throw new Error("encodeDiagnosisPathVersion: diagnosisVersion must be a non-empty string");
|
|
291
|
+
if (cardVersion === "")
|
|
292
|
+
throw new Error("encodeDiagnosisPathVersion: cardVersion must be a non-empty string");
|
|
293
|
+
if (diagnosisVersion.includes("|"))
|
|
294
|
+
throw new Error("diagnosisVersion must not contain '|'");
|
|
295
|
+
return `${diagnosisVersion}|${cardVersion}`;
|
|
296
|
+
}
|
|
62
297
|
/**
|
|
63
298
|
* Convert an entry key (wire format, e.g. `{taskId}::{modelId}`) to a
|
|
64
299
|
* filename-safe component.
|
|
@@ -415,9 +650,12 @@ function titleCaseCategory(id) {
|
|
|
415
650
|
.join(" ");
|
|
416
651
|
}
|
|
417
652
|
function buildDescriptor(input) {
|
|
418
|
-
const objectPath = input.
|
|
419
|
-
|
|
420
|
-
|
|
653
|
+
const objectPath = input.objectPath ??
|
|
654
|
+
(input.versionedBy
|
|
655
|
+
? versionedPathBuilder(input.slug, input.mime)
|
|
656
|
+
: input.layout === "bulk"
|
|
657
|
+
? bulkPathBuilder(input.slug, input.mime)
|
|
658
|
+
: perEntryPathBuilder(input.slug, input.mime));
|
|
421
659
|
const formatEntryKey = input.layout === "per-entry" ? formatKeyFromAxes(input.axes) : undefined;
|
|
422
660
|
const parseEntryKey = input.layout === "per-entry"
|
|
423
661
|
? (input.parseEntryKey ?? parseKeyByAxes(input.type, input.axes))
|
|
@@ -432,6 +670,9 @@ function buildDescriptor(input) {
|
|
|
432
670
|
capBytes: input.capBytes,
|
|
433
671
|
truncation: input.truncation,
|
|
434
672
|
optional: input.optional,
|
|
673
|
+
writePolicy: input.writePolicy,
|
|
674
|
+
versionedBy: input.versionedBy,
|
|
675
|
+
pathSafetyMarker: input.pathSafetyMarker,
|
|
435
676
|
objectPath,
|
|
436
677
|
formatEntryKey,
|
|
437
678
|
parseEntryKey,
|
|
@@ -932,6 +1173,49 @@ export const ARTIFACT_REGISTRY = {
|
|
|
932
1173
|
capBytes: 10_000_000,
|
|
933
1174
|
truncation: "trial-oversize",
|
|
934
1175
|
}),
|
|
1176
|
+
// -- Plan 01-03 — Actionability ladder Phase 1 ---------------------------
|
|
1177
|
+
diagnosis: buildDescriptor({
|
|
1178
|
+
type: "diagnosis",
|
|
1179
|
+
slug: "diagnosis",
|
|
1180
|
+
layout: "bulk",
|
|
1181
|
+
axes: ["run", "report"],
|
|
1182
|
+
entrySchema: unknownEntry,
|
|
1183
|
+
mime: "application/json",
|
|
1184
|
+
capBytes: 5 * 1024 * 1024,
|
|
1185
|
+
writePolicy: "post-hoc",
|
|
1186
|
+
versionedBy: "diagnosisVersion",
|
|
1187
|
+
objectPath: diagnosisPathBuilder(),
|
|
1188
|
+
// Defense-in-depth: this descriptor's axes (`run`, `report`) are both
|
|
1189
|
+
// bounded, so the `assertValidArtifactDescriptor` unbounded-axis rule
|
|
1190
|
+
// does not fire and the carve-out is never consulted at module load
|
|
1191
|
+
// for THIS descriptor. The marker is set so any future additions to
|
|
1192
|
+
// this descriptor (or a new diagnosis-shaped descriptor that gains an
|
|
1193
|
+
// unbounded axis like `task`) opt into the carve-out by default rather
|
|
1194
|
+
// than tripping the bounded-axis rule. The marker also documents
|
|
1195
|
+
// intent: this descriptor is the canonical reference for the
|
|
1196
|
+
// bulk-versioned-with-report-axis pattern (ITER2-BLOCKER-01).
|
|
1197
|
+
pathSafetyMarker: BULK_VERSIONED_WITH_REPORT_AXIS, // ITER2-BLOCKER-01 — explicit opt-in
|
|
1198
|
+
}),
|
|
1199
|
+
perEntryAttribution: buildDescriptor({
|
|
1200
|
+
type: "perEntryAttribution",
|
|
1201
|
+
slug: "attribution",
|
|
1202
|
+
layout: "per-entry",
|
|
1203
|
+
axes: ["run"],
|
|
1204
|
+
entrySchema: unknownEntry,
|
|
1205
|
+
mime: "application/json",
|
|
1206
|
+
capBytes: 1 * 1024 * 1024,
|
|
1207
|
+
writePolicy: "pipeline",
|
|
1208
|
+
}),
|
|
1209
|
+
attributionMeta: buildDescriptor({
|
|
1210
|
+
type: "attributionMeta",
|
|
1211
|
+
slug: "attribution-meta",
|
|
1212
|
+
layout: "bulk",
|
|
1213
|
+
axes: ["run"],
|
|
1214
|
+
entrySchema: unknownEntry,
|
|
1215
|
+
mime: "application/json",
|
|
1216
|
+
capBytes: 1 * 1024 * 1024,
|
|
1217
|
+
writePolicy: "pipeline",
|
|
1218
|
+
}),
|
|
935
1219
|
};
|
|
936
1220
|
/** All artifact types in declaration order. */
|
|
937
1221
|
export const ARTIFACT_TYPES = Object.keys(ARTIFACT_REGISTRY);
|
|
@@ -943,25 +1227,61 @@ export function isArtifactType(value) {
|
|
|
943
1227
|
return value in ARTIFACT_REGISTRY;
|
|
944
1228
|
}
|
|
945
1229
|
// ---------------------------------------------------------------------------
|
|
946
|
-
// Module-load invariant (D0033 / W0049)
|
|
1230
|
+
// Module-load invariant (D0033 / W0049 / D0050)
|
|
947
1231
|
// ---------------------------------------------------------------------------
|
|
948
1232
|
/**
|
|
949
1233
|
* Unbounded axes — dimensions whose cardinality grows with a run. A bulk
|
|
950
1234
|
* artifact fanning across these cannot bound its payload; the registry
|
|
951
1235
|
* forbids that shape at import time.
|
|
1236
|
+
*
|
|
1237
|
+
* **Layout rule (D0050).** Bulk descriptors must declare *only* bounded
|
|
1238
|
+
* axes — fanning a single JSON across an unbounded axis (`task`, `model`,
|
|
1239
|
+
* `trial`) violates the size cap at scale. Per-entry descriptors *may*
|
|
1240
|
+
* declare unbounded axes; the per-entry layout naturally produces one
|
|
1241
|
+
* object per axis tuple, so unboundedness becomes the file count, not
|
|
1242
|
+
* the file size. The existing `testOutputs` per-entry descriptor has
|
|
1243
|
+
* carried unbounded `task`+`model` axes since W0048 and is the precedent
|
|
1244
|
+
* D0050 formalizes for attribution.
|
|
952
1245
|
*/
|
|
953
1246
|
const UNBOUNDED_AXES = [
|
|
954
1247
|
"task",
|
|
955
1248
|
"model",
|
|
956
1249
|
"trial",
|
|
957
1250
|
];
|
|
1251
|
+
/**
|
|
1252
|
+
* The bulk-versioned-with-report-axis carve-out (Plan 01-03 /
|
|
1253
|
+
* ITER2-BLOCKER-01) is an explicit opt-in: a descriptor must set
|
|
1254
|
+
* `pathSafetyMarker === BULK_VERSIONED_WITH_REPORT_AXIS` AND meet the
|
|
1255
|
+
* four structural conditions:
|
|
1256
|
+
*
|
|
1257
|
+
* 1. desc.pathSafetyMarker === BULK_VERSIONED_WITH_REPORT_AXIS (gate)
|
|
1258
|
+
* 2. desc.layout === "bulk" (sanity)
|
|
1259
|
+
* 3. Boolean(desc.versionedBy) (sanity)
|
|
1260
|
+
* 4. desc.association.axes.includes("run") (sanity)
|
|
1261
|
+
* 5. desc.association.axes.includes("report") (sanity)
|
|
1262
|
+
*
|
|
1263
|
+
* Marker without structure → fails (the descriptor misapplied the marker).
|
|
1264
|
+
* Structure without marker → fails (the descriptor must opt in).
|
|
1265
|
+
*
|
|
1266
|
+
* The marker is the SOLE detection mechanism. There is no source-text
|
|
1267
|
+
* reflection on the descriptor's path-builder closure body — `Symbol.for(...)`
|
|
1268
|
+
* is bundler-stable by spec and survives minification.
|
|
1269
|
+
*/
|
|
1270
|
+
function isBulkVersionedWithReportAxisCarveOut(desc) {
|
|
1271
|
+
return (desc.pathSafetyMarker === BULK_VERSIONED_WITH_REPORT_AXIS &&
|
|
1272
|
+
desc.layout === "bulk" &&
|
|
1273
|
+
Boolean(desc.versionedBy) &&
|
|
1274
|
+
desc.association.axes.includes("run") &&
|
|
1275
|
+
desc.association.axes.includes("report"));
|
|
1276
|
+
}
|
|
958
1277
|
/**
|
|
959
1278
|
* Structural check run against a single descriptor. Exported so L1 contract
|
|
960
1279
|
* tests can construct an invalid descriptor inline and assert the throw.
|
|
961
1280
|
*/
|
|
962
1281
|
export function assertValidArtifactDescriptor(desc) {
|
|
1282
|
+
const carveOutApplies = isBulkVersionedWithReportAxisCarveOut(desc);
|
|
963
1283
|
const hasUnboundedAxis = desc.association.axes.some((a) => UNBOUNDED_AXES.includes(a));
|
|
964
|
-
if (hasUnboundedAxis && desc.layout !== "per-entry") {
|
|
1284
|
+
if (hasUnboundedAxis && desc.layout !== "per-entry" && !carveOutApplies) {
|
|
965
1285
|
throw new Error(`Artifact ${desc.type}: association contains unbounded axis (${desc.association.axes
|
|
966
1286
|
.filter((a) => UNBOUNDED_AXES.includes(a))
|
|
967
1287
|
.join(", ")}) but layout is "${desc.layout}". Unbounded axes require layout "per-entry".`);
|
|
@@ -972,6 +1292,14 @@ export function assertValidArtifactDescriptor(desc) {
|
|
|
972
1292
|
if (desc.layout === "per-entry" && !desc.formatEntryKey) {
|
|
973
1293
|
throw new Error(`Artifact ${desc.type}: per-entry descriptors must declare formatEntryKey`);
|
|
974
1294
|
}
|
|
1295
|
+
// D0050 — versioned descriptors are bulk-shaped only in v0; per-entry +
|
|
1296
|
+
// versioned is a future extension and rejected at module load so a
|
|
1297
|
+
// half-wired descriptor doesn't ship by accident. Bulk-versioned-with-
|
|
1298
|
+
// report-axis (the diagnosis case) is permitted only via the explicit
|
|
1299
|
+
// pathSafetyMarker opt-in checked above.
|
|
1300
|
+
if (desc.versionedBy && desc.layout !== "bulk") {
|
|
1301
|
+
throw new Error(`Artifact ${desc.type}: versionedBy is only supported on bulk descriptors (got layout "${desc.layout}")`);
|
|
1302
|
+
}
|
|
975
1303
|
}
|
|
976
1304
|
// Fire the invariant at import time — a bad descriptor kills the process
|
|
977
1305
|
// before any producer can silently serialize an oversized JSON array.
|
|
@@ -979,6 +1307,92 @@ for (const desc of Object.values(ARTIFACT_REGISTRY)) {
|
|
|
979
1307
|
assertValidArtifactDescriptor(desc);
|
|
980
1308
|
}
|
|
981
1309
|
// ---------------------------------------------------------------------------
|
|
1310
|
+
// Write-policy guard (D0050)
|
|
1311
|
+
// ---------------------------------------------------------------------------
|
|
1312
|
+
/**
|
|
1313
|
+
* Thrown when a writer's identity (`writeSource`) doesn't match a
|
|
1314
|
+
* descriptor's `writePolicy`. Pipeline writers can't emit `"post-hoc"`
|
|
1315
|
+
* descriptors (the post-hoc artifact would land mid-run, before the run
|
|
1316
|
+
* finalizes); post-hoc writers can't emit `"pipeline"` descriptors
|
|
1317
|
+
* (those should have been written by the pipeline itself).
|
|
1318
|
+
*
|
|
1319
|
+
* The error type is intentionally distinct from `Error` so CI can match
|
|
1320
|
+
* on the class and surface mismatches clearly in failure logs.
|
|
1321
|
+
*/
|
|
1322
|
+
export class WritePolicyMismatchError extends Error {
|
|
1323
|
+
code = "WRITE_POLICY_MISMATCH";
|
|
1324
|
+
artifactType;
|
|
1325
|
+
descriptorPolicy;
|
|
1326
|
+
writerSource;
|
|
1327
|
+
constructor(opts) {
|
|
1328
|
+
super(`Artifact "${opts.artifactType}" has writePolicy="${opts.descriptorPolicy}" but writer is "${opts.writerSource}". ` +
|
|
1329
|
+
`Pipeline writers cannot emit post-hoc descriptors and post-hoc writers cannot emit pipeline descriptors.`);
|
|
1330
|
+
this.name = "WritePolicyMismatchError";
|
|
1331
|
+
this.artifactType = opts.artifactType;
|
|
1332
|
+
this.descriptorPolicy = opts.descriptorPolicy;
|
|
1333
|
+
this.writerSource = opts.writerSource;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Resolve a descriptor's effective write policy. Defaults to `"pipeline"`
|
|
1338
|
+
* when unset — preserves backward compatibility with every pre-D0050
|
|
1339
|
+
* descriptor that doesn't declare the field.
|
|
1340
|
+
*/
|
|
1341
|
+
export function resolveWritePolicy(desc) {
|
|
1342
|
+
return desc.writePolicy ?? "pipeline";
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Writer-side guard. Call at the top of `emit()` / `appendNdjson()` in
|
|
1346
|
+
* every artifact writer that physically writes bytes (the in-memory test
|
|
1347
|
+
* doubles don't need it). Throws `WritePolicyMismatchError` on
|
|
1348
|
+
* mismatch; returns silently on a match. Pure function — no I/O, safe
|
|
1349
|
+
* to invoke from any layer.
|
|
1350
|
+
*/
|
|
1351
|
+
export function assertWritePolicyMatches(writerSource, descriptor) {
|
|
1352
|
+
const descriptorPolicy = resolveWritePolicy(descriptor);
|
|
1353
|
+
if (descriptorPolicy !== writerSource) {
|
|
1354
|
+
throw new WritePolicyMismatchError({
|
|
1355
|
+
artifactType: descriptor.type,
|
|
1356
|
+
descriptorPolicy,
|
|
1357
|
+
writerSource,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// ---------------------------------------------------------------------------
|
|
1362
|
+
// Slim-shape preview for post-hoc artifacts (D0050)
|
|
1363
|
+
// ---------------------------------------------------------------------------
|
|
1364
|
+
/**
|
|
1365
|
+
* Slim-shape preview for `"post-hoc"` descriptors. Replaces the
|
|
1366
|
+
* fixed-path semantics that pipeline-written artifacts use (a single
|
|
1367
|
+
* known path per descriptor) with `present: boolean` plus an optional
|
|
1368
|
+
* `latestVersion: string` — necessary because:
|
|
1369
|
+
*
|
|
1370
|
+
* - A post-hoc artifact may have zero versions (never written) or
|
|
1371
|
+
* multiple versions (regenerated). A fixed `path` cannot encode
|
|
1372
|
+
* either.
|
|
1373
|
+
* - Slim-shape consumers (Studio rollups) want to know "is there a
|
|
1374
|
+
* diagnosis for this run?" without enumerating versioned siblings.
|
|
1375
|
+
*
|
|
1376
|
+
* Post-hoc writers populate `latestVersion` after a successful write
|
|
1377
|
+
* (last-write-wins per-version semantics, per D0050 open-question
|
|
1378
|
+
* resolution). The full versioned payload is fetched via the
|
|
1379
|
+
* descriptor's `objectPath(runId, undefined, latestVersion)`.
|
|
1380
|
+
*/
|
|
1381
|
+
export const postHocSlimPreviewSchema = z.object({
|
|
1382
|
+
present: z.boolean(),
|
|
1383
|
+
latestVersion: z.string().optional(),
|
|
1384
|
+
});
|
|
1385
|
+
/**
|
|
1386
|
+
* Build a post-hoc slim-shape preview. `latestVersion` is omitted when
|
|
1387
|
+
* absent rather than emitted as `undefined`, matching the optional-field
|
|
1388
|
+
* convention used elsewhere in the registry.
|
|
1389
|
+
*/
|
|
1390
|
+
export function buildPostHocSlimPreview(opts) {
|
|
1391
|
+
return opts.latestVersion === undefined
|
|
1392
|
+
? { present: opts.present }
|
|
1393
|
+
: { present: opts.present, latestVersion: opts.latestVersion };
|
|
1394
|
+
}
|
|
1395
|
+
// ---------------------------------------------------------------------------
|
|
982
1396
|
// Manifest preview helper (W0051 / D0033 M7)
|
|
983
1397
|
// ---------------------------------------------------------------------------
|
|
984
1398
|
/**
|