@sanity/ailf 2.8.0 → 3.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/dist/_vendor/ailf-core/artifact-capture/association.d.ts +35 -0
- package/dist/_vendor/ailf-core/artifact-capture/association.js +28 -0
- package/dist/_vendor/ailf-core/artifact-registry.d.ts +124 -23
- package/dist/_vendor/ailf-core/artifact-registry.js +708 -64
- package/dist/_vendor/ailf-core/batch-signing.d.ts +64 -0
- package/dist/_vendor/ailf-core/batch-signing.js +23 -0
- package/dist/_vendor/ailf-core/index.d.ts +3 -2
- package/dist/_vendor/ailf-core/index.js +3 -2
- package/dist/_vendor/ailf-core/ports/artifact-writer.d.ts +59 -20
- package/dist/_vendor/ailf-core/ports/artifact-writer.js +33 -10
- package/dist/_vendor/ailf-core/ports/context.d.ts +20 -17
- package/dist/_vendor/ailf-core/ports/index.d.ts +0 -2
- package/dist/_vendor/ailf-core/schemas/pipeline.d.ts +6 -6
- package/dist/_vendor/ailf-core/services/index.d.ts +1 -0
- package/dist/_vendor/ailf-core/services/index.js +1 -0
- package/dist/_vendor/ailf-core/services/slim-report-summary.d.ts +31 -0
- package/dist/_vendor/ailf-core/services/slim-report-summary.js +217 -0
- package/dist/_vendor/ailf-core/types/branded-ids.d.ts +33 -0
- package/dist/_vendor/ailf-core/types/index.d.ts +202 -23
- package/dist/adapters/config-sources/file-config-adapter.js +0 -4
- package/dist/artifact-capture/accumulating-artifact-writer.d.ts +50 -0
- package/dist/artifact-capture/accumulating-artifact-writer.js +111 -0
- package/dist/artifact-capture/api-gateway-artifact-writer.d.ts +17 -4
- package/dist/artifact-capture/api-gateway-artifact-writer.js +58 -7
- package/dist/artifact-capture/emit-file.d.ts +28 -0
- package/dist/artifact-capture/emit-file.js +56 -0
- package/dist/artifact-capture/fanout-artifact-writer.d.ts +39 -0
- package/dist/artifact-capture/fanout-artifact-writer.js +76 -0
- package/dist/artifact-capture/gcs-artifact-writer.d.ts +40 -3
- package/dist/artifact-capture/gcs-artifact-writer.js +238 -14
- package/dist/artifact-capture/local-fs-artifact-writer.d.ts +71 -0
- package/dist/artifact-capture/local-fs-artifact-writer.js +273 -0
- package/dist/artifact-capture/redact-artifact.d.ts +3 -5
- package/dist/artifact-capture/redact-artifact.js +3 -5
- package/dist/cli.js +56 -2
- package/dist/commands/explain-handler.js +4 -4
- package/dist/commands/pipeline-action.d.ts +5 -4
- package/dist/commands/pipeline-action.js +33 -16
- package/dist/commands/pipeline.d.ts +4 -4
- package/dist/commands/pipeline.js +4 -4
- package/dist/commands/publish.js +4 -1
- package/dist/commands/runs.d.ts +18 -0
- package/dist/commands/runs.js +71 -0
- package/dist/composition-root.d.ts +13 -10
- package/dist/composition-root.js +74 -46
- package/dist/orchestration/build-app-context.js +4 -7
- package/dist/orchestration/pipeline-orchestrator.d.ts +1 -1
- package/dist/orchestration/pipeline-orchestrator.js +37 -46
- package/dist/orchestration/steps/calculate-scores-step.d.ts +1 -1
- package/dist/orchestration/steps/calculate-scores-step.js +19 -19
- package/dist/orchestration/steps/callback-step.d.ts +1 -1
- package/dist/orchestration/steps/callback-step.js +6 -4
- package/dist/orchestration/steps/compare-step.d.ts +1 -1
- package/dist/orchestration/steps/compare-step.js +4 -2
- package/dist/orchestration/steps/discovery-report-step.d.ts +1 -1
- package/dist/orchestration/steps/discovery-report-step.js +4 -1
- package/dist/orchestration/steps/fetch-docs-step.js +9 -15
- package/dist/orchestration/steps/finalize-run-step.js +21 -7
- package/dist/orchestration/steps/gap-analysis-step.js +34 -6
- package/dist/orchestration/steps/generate-configs-step.d.ts +1 -1
- package/dist/orchestration/steps/generate-configs-step.js +11 -11
- package/dist/orchestration/steps/publish-report-step.d.ts +1 -1
- package/dist/orchestration/steps/publish-report-step.js +24 -19
- package/dist/orchestration/steps/readiness-step.d.ts +1 -1
- package/dist/orchestration/steps/readiness-step.js +4 -1
- package/dist/orchestration/steps/report-step.d.ts +1 -1
- package/dist/orchestration/steps/report-step.js +6 -3
- package/dist/orchestration/steps/run-eval-step.js +14 -9
- package/dist/pipeline/compare.d.ts +2 -2
- package/dist/pipeline/emit-eval-results.d.ts +38 -0
- package/dist/pipeline/emit-eval-results.js +100 -0
- package/dist/pipeline/map-request-to-config.js +0 -4
- package/package.json +1 -1
- package/dist/_vendor/ailf-core/artifact-capture/noop-collector.d.ts +0 -14
- package/dist/_vendor/ailf-core/artifact-capture/noop-collector.js +0 -25
- package/dist/_vendor/ailf-core/ports/artifact-collector.d.ts +0 -94
- package/dist/_vendor/ailf-core/ports/artifact-collector.js +0 -13
- package/dist/_vendor/ailf-core/ports/capture-comparator.d.ts +0 -138
- package/dist/_vendor/ailf-core/ports/capture-comparator.js +0 -10
- package/dist/artifact-capture/comparator.d.ts +0 -22
- package/dist/artifact-capture/comparator.js +0 -493
- package/dist/artifact-capture/filesystem-collector.d.ts +0 -42
- package/dist/artifact-capture/filesystem-collector.js +0 -237
- package/dist/artifact-capture/gcs-collector.d.ts +0 -55
- package/dist/artifact-capture/gcs-collector.js +0 -117
- package/dist/commands/capture-compare.d.ts +0 -15
- package/dist/commands/capture-compare.js +0 -253
- package/dist/commands/capture-list.d.ts +0 -12
- package/dist/commands/capture-list.js +0 -150
- package/dist/commands/capture.d.ts +0 -9
- package/dist/commands/capture.js +0 -16
|
@@ -1,493 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CaptureComparator — compares two capture directories and produces a diff report.
|
|
3
|
-
*
|
|
4
|
-
* Reads manifest.json from both directories and computes:
|
|
5
|
-
* - Inventory diff (added/removed/common artifacts)
|
|
6
|
-
* - Content diff (structural or strict, for common artifacts)
|
|
7
|
-
* - Score comparison (from score-summary.json)
|
|
8
|
-
* - Timing comparison (from pipeline-context.json)
|
|
9
|
-
* - Metadata comparison (mode, variant, config keys)
|
|
10
|
-
* - Security scan (regex for leaked secrets)
|
|
11
|
-
*
|
|
12
|
-
* Implementation for the types defined in @sanity/ailf-core.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Defaults
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
const DEFAULT_OPTIONS = {
|
|
20
|
-
mode: "inventory",
|
|
21
|
-
scoreThresholds: { aggregate: 5, perTask: 10 },
|
|
22
|
-
timingThresholds: { multiplier: 2.0 },
|
|
23
|
-
jsonDiffDepth: 3,
|
|
24
|
-
};
|
|
25
|
-
const DEFAULT_EPHEMERAL_FIELDS = new Set([
|
|
26
|
-
"captureId",
|
|
27
|
-
"startedAt",
|
|
28
|
-
"completedAt",
|
|
29
|
-
"capturedAt",
|
|
30
|
-
"durationMs",
|
|
31
|
-
]);
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Public API
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
/**
|
|
36
|
-
* Compare two capture directories and produce a structured diff report.
|
|
37
|
-
*
|
|
38
|
-
* @param baselineDir - Path to the baseline capture directory (contains manifest.json)
|
|
39
|
-
* @param experimentDir - Path to the experiment capture directory
|
|
40
|
-
* @param opts - Comparison options (mode, thresholds, etc.)
|
|
41
|
-
*/
|
|
42
|
-
export function compareCaptures(baselineDir, experimentDir, opts) {
|
|
43
|
-
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
44
|
-
const ephemeral = new Set([
|
|
45
|
-
...DEFAULT_EPHEMERAL_FIELDS,
|
|
46
|
-
...(options.ephemeralFields ?? []),
|
|
47
|
-
]);
|
|
48
|
-
const baselineManifest = readManifest(baselineDir);
|
|
49
|
-
const experimentManifest = readManifest(experimentDir);
|
|
50
|
-
// Inventory diff
|
|
51
|
-
const inventory = computeInventoryDiff(baselineManifest, experimentManifest);
|
|
52
|
-
// Security scan (always runs)
|
|
53
|
-
const security = scanForSecrets(baselineDir, experimentDir, baselineManifest, experimentManifest);
|
|
54
|
-
// Content diff (structural/strict only)
|
|
55
|
-
let content;
|
|
56
|
-
if (options.mode !== "inventory" && inventory.common.length > 0) {
|
|
57
|
-
content = computeContentDiffs(baselineDir, experimentDir, baselineManifest, experimentManifest, inventory.common, options.mode, options.jsonDiffDepth ?? 3, ephemeral);
|
|
58
|
-
}
|
|
59
|
-
// Score comparison
|
|
60
|
-
const scores = computeScoreDiff(baselineDir, experimentDir, baselineManifest, experimentManifest, options.scoreThresholds ?? DEFAULT_OPTIONS.scoreThresholds);
|
|
61
|
-
// Timing comparison
|
|
62
|
-
const timing = computeTimingDiff(baselineDir, experimentDir, baselineManifest, experimentManifest, options.timingThresholds ?? DEFAULT_OPTIONS.timingThresholds);
|
|
63
|
-
// Metadata comparison
|
|
64
|
-
const metadata = computeMetadataDiff(baselineDir, experimentDir, baselineManifest, experimentManifest, ephemeral);
|
|
65
|
-
// Determine equivalence
|
|
66
|
-
const violations = [];
|
|
67
|
-
if (inventory.added.length > 0)
|
|
68
|
-
violations.push(`${inventory.added.length} artifact(s) added`);
|
|
69
|
-
if (inventory.removed.length > 0)
|
|
70
|
-
violations.push(`${inventory.removed.length} artifact(s) removed`);
|
|
71
|
-
if (content && content.length > 0)
|
|
72
|
-
violations.push(`${content.length} artifact(s) changed`);
|
|
73
|
-
if (scores?.breaches.length)
|
|
74
|
-
violations.push(`${scores.breaches.length} score breach(es)`);
|
|
75
|
-
if (timing?.breaches.length)
|
|
76
|
-
violations.push(`${timing.breaches.length} timing breach(es)`);
|
|
77
|
-
if (security.leaksFound)
|
|
78
|
-
violations.push(`${security.violations.length} secret leak(s)`);
|
|
79
|
-
const equivalent = violations.length === 0;
|
|
80
|
-
return {
|
|
81
|
-
equivalent,
|
|
82
|
-
summary: equivalent
|
|
83
|
-
? "Captures are equivalent."
|
|
84
|
-
: `Differences found: ${violations.join("; ")}.`,
|
|
85
|
-
mode: options.mode,
|
|
86
|
-
inventory,
|
|
87
|
-
...(content ? { content } : {}),
|
|
88
|
-
...(scores ? { scores } : {}),
|
|
89
|
-
...(timing ? { timing } : {}),
|
|
90
|
-
...(metadata ? { metadata } : {}),
|
|
91
|
-
security,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// Manifest reading
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
function readManifest(dir) {
|
|
98
|
-
const manifestPath = join(dir, "manifest.json");
|
|
99
|
-
if (!existsSync(manifestPath)) {
|
|
100
|
-
throw new Error(`No manifest.json found in ${dir}`);
|
|
101
|
-
}
|
|
102
|
-
return JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
103
|
-
}
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Inventory diff
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
function artifactKey(entry) {
|
|
108
|
-
return `${entry.step}/${entry.type}`;
|
|
109
|
-
}
|
|
110
|
-
function computeInventoryDiff(baseline, experiment) {
|
|
111
|
-
const baselineKeys = new Set(baseline.artifacts.map(artifactKey));
|
|
112
|
-
const experimentKeys = new Set(experiment.artifacts.map(artifactKey));
|
|
113
|
-
const added = [...experimentKeys].filter((k) => !baselineKeys.has(k));
|
|
114
|
-
const removed = [...baselineKeys].filter((k) => !experimentKeys.has(k));
|
|
115
|
-
const common = [...baselineKeys].filter((k) => experimentKeys.has(k));
|
|
116
|
-
return { added, removed, common };
|
|
117
|
-
}
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Content diff
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
function computeContentDiffs(baselineDir, experimentDir, baselineManifest, experimentManifest, commonKeys, mode, depth, ephemeral) {
|
|
122
|
-
const diffs = [];
|
|
123
|
-
const baselineByKey = new Map(baselineManifest.artifacts.map((a) => [artifactKey(a), a]));
|
|
124
|
-
const experimentByKey = new Map(experimentManifest.artifacts.map((a) => [artifactKey(a), a]));
|
|
125
|
-
for (const key of commonKeys) {
|
|
126
|
-
const baseEntry = baselineByKey.get(key);
|
|
127
|
-
const expEntry = experimentByKey.get(key);
|
|
128
|
-
const basePath = join(baselineDir, baseEntry.path);
|
|
129
|
-
const expPath = join(experimentDir, expEntry.path);
|
|
130
|
-
if (!existsSync(basePath) || !existsSync(expPath))
|
|
131
|
-
continue;
|
|
132
|
-
const baseContent = readFileSync(basePath, "utf-8");
|
|
133
|
-
const expContent = readFileSync(expPath, "utf-8");
|
|
134
|
-
if (baseEntry.format === "json") {
|
|
135
|
-
try {
|
|
136
|
-
const baseData = JSON.parse(baseContent);
|
|
137
|
-
const expData = JSON.parse(expContent);
|
|
138
|
-
const stripped1 = stripEphemeral(baseData, ephemeral);
|
|
139
|
-
const stripped2 = stripEphemeral(expData, ephemeral);
|
|
140
|
-
if (mode === "strict") {
|
|
141
|
-
if (JSON.stringify(stripped1) !== JSON.stringify(stripped2)) {
|
|
142
|
-
const changes = diffJson(stripped1, stripped2, "", depth);
|
|
143
|
-
if (changes.length > 0) {
|
|
144
|
-
diffs.push({ artifactKey: key, format: "json", changes });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
// structural — compare keys/types only
|
|
150
|
-
const changes = diffJsonStructural(stripped1, stripped2, "", depth);
|
|
151
|
-
if (changes.length > 0) {
|
|
152
|
-
diffs.push({ artifactKey: key, format: "json", changes });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// Can't parse JSON — fall through to text comparison
|
|
158
|
-
if (baseContent !== expContent) {
|
|
159
|
-
diffs.push({
|
|
160
|
-
artifactKey: key,
|
|
161
|
-
format: "text",
|
|
162
|
-
changes: computeLineDiff(baseContent, expContent),
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
// Text/markdown — line-level diff
|
|
169
|
-
if (baseContent !== expContent) {
|
|
170
|
-
diffs.push({
|
|
171
|
-
artifactKey: key,
|
|
172
|
-
format: baseEntry.format,
|
|
173
|
-
changes: computeLineDiff(baseContent, expContent),
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return diffs;
|
|
179
|
-
}
|
|
180
|
-
function computeLineDiff(a, b) {
|
|
181
|
-
const aLines = a.split("\n");
|
|
182
|
-
const bLines = b.split("\n");
|
|
183
|
-
const aFreq = new Map();
|
|
184
|
-
for (const l of aLines)
|
|
185
|
-
aFreq.set(l, (aFreq.get(l) ?? 0) + 1);
|
|
186
|
-
const bFreq = new Map();
|
|
187
|
-
for (const l of bLines)
|
|
188
|
-
bFreq.set(l, (bFreq.get(l) ?? 0) + 1);
|
|
189
|
-
let addedLines = 0;
|
|
190
|
-
let removedLines = 0;
|
|
191
|
-
for (const [line, count] of bFreq) {
|
|
192
|
-
addedLines += Math.max(0, count - (aFreq.get(line) ?? 0));
|
|
193
|
-
}
|
|
194
|
-
for (const [line, count] of aFreq) {
|
|
195
|
-
removedLines += Math.max(0, count - (bFreq.get(line) ?? 0));
|
|
196
|
-
}
|
|
197
|
-
return { addedLines, removedLines };
|
|
198
|
-
}
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
// JSON diffing
|
|
201
|
-
// ---------------------------------------------------------------------------
|
|
202
|
-
function stripEphemeral(data, ephemeral) {
|
|
203
|
-
if (typeof data !== "object" || data === null)
|
|
204
|
-
return data;
|
|
205
|
-
if (Array.isArray(data))
|
|
206
|
-
return data.map((item) => stripEphemeral(item, ephemeral));
|
|
207
|
-
const result = {};
|
|
208
|
-
for (const [key, value] of Object.entries(data)) {
|
|
209
|
-
if (ephemeral.has(key))
|
|
210
|
-
continue;
|
|
211
|
-
result[key] = stripEphemeral(value, ephemeral);
|
|
212
|
-
}
|
|
213
|
-
return result;
|
|
214
|
-
}
|
|
215
|
-
/** Strict diff — compares values at each key path. */
|
|
216
|
-
function diffJson(a, b, path, depth) {
|
|
217
|
-
if (depth <= 0)
|
|
218
|
-
return [];
|
|
219
|
-
if (a === b)
|
|
220
|
-
return [];
|
|
221
|
-
const entries = [];
|
|
222
|
-
if (typeof a !== "object" ||
|
|
223
|
-
typeof b !== "object" ||
|
|
224
|
-
a === null ||
|
|
225
|
-
b === null) {
|
|
226
|
-
return [{ path: path || "(root)", baseline: a, experiment: b }];
|
|
227
|
-
}
|
|
228
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
229
|
-
if (JSON.stringify(a) !== JSON.stringify(b)) {
|
|
230
|
-
entries.push({ path: path || "(root)", baseline: a, experiment: b });
|
|
231
|
-
}
|
|
232
|
-
return entries;
|
|
233
|
-
}
|
|
234
|
-
const aObj = a;
|
|
235
|
-
const bObj = b;
|
|
236
|
-
const allKeys = new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
|
|
237
|
-
for (const key of allKeys) {
|
|
238
|
-
const subPath = path ? `${path}.${key}` : key;
|
|
239
|
-
if (!(key in aObj)) {
|
|
240
|
-
entries.push({ path: subPath, experiment: bObj[key] });
|
|
241
|
-
}
|
|
242
|
-
else if (!(key in bObj)) {
|
|
243
|
-
entries.push({ path: subPath, baseline: aObj[key] });
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
entries.push(...diffJson(aObj[key], bObj[key], subPath, depth - 1));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return entries;
|
|
250
|
-
}
|
|
251
|
-
/** Structural diff — only checks that keys exist and types match. */
|
|
252
|
-
function diffJsonStructural(a, b, path, depth) {
|
|
253
|
-
if (depth <= 0)
|
|
254
|
-
return [];
|
|
255
|
-
const typeA = typeof a;
|
|
256
|
-
const typeB = typeof b;
|
|
257
|
-
if (typeA !== typeB) {
|
|
258
|
-
return [{ path: path || "(root)", baseline: typeA, experiment: typeB }];
|
|
259
|
-
}
|
|
260
|
-
if (typeA !== "object" || a === null || b === null)
|
|
261
|
-
return [];
|
|
262
|
-
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
263
|
-
return [
|
|
264
|
-
{
|
|
265
|
-
path: path || "(root)",
|
|
266
|
-
baseline: Array.isArray(a) ? "array" : "object",
|
|
267
|
-
experiment: Array.isArray(b) ? "array" : "object",
|
|
268
|
-
},
|
|
269
|
-
];
|
|
270
|
-
}
|
|
271
|
-
if (Array.isArray(a))
|
|
272
|
-
return []; // Arrays: structural match if both are arrays
|
|
273
|
-
const entries = [];
|
|
274
|
-
const aObj = a;
|
|
275
|
-
const bObj = b;
|
|
276
|
-
const allKeys = new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
|
|
277
|
-
for (const key of allKeys) {
|
|
278
|
-
const subPath = path ? `${path}.${key}` : key;
|
|
279
|
-
if (!(key in aObj)) {
|
|
280
|
-
entries.push({ path: subPath, experiment: typeof bObj[key] });
|
|
281
|
-
}
|
|
282
|
-
else if (!(key in bObj)) {
|
|
283
|
-
entries.push({ path: subPath, baseline: typeof aObj[key] });
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
entries.push(...diffJsonStructural(aObj[key], bObj[key], subPath, depth - 1));
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return entries;
|
|
290
|
-
}
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// Score comparison
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
function computeScoreDiff(baselineDir, experimentDir, baselineManifest, experimentManifest, thresholds) {
|
|
295
|
-
const baseScore = findAndReadArtifact(baselineDir, baselineManifest, "calculate-scores", "score-summary");
|
|
296
|
-
const expScore = findAndReadArtifact(experimentDir, experimentManifest, "calculate-scores", "score-summary");
|
|
297
|
-
if (!baseScore || !expScore)
|
|
298
|
-
return undefined;
|
|
299
|
-
const baselineMean = baseScore.aggregate ?? 0;
|
|
300
|
-
const currentMean = expScore.aggregate ?? 0;
|
|
301
|
-
const delta = currentMean - baselineMean;
|
|
302
|
-
// Per-task comparison
|
|
303
|
-
const baseScores = baseScore.scores ?? [];
|
|
304
|
-
const expScores = expScore.scores ?? [];
|
|
305
|
-
const expByTask = new Map(expScores.map((s) => [s.task ?? "", s.score ?? 0]));
|
|
306
|
-
const perTask = [];
|
|
307
|
-
const breaches = [];
|
|
308
|
-
for (const base of baseScores) {
|
|
309
|
-
const task = base.task ?? "";
|
|
310
|
-
const baseVal = base.score ?? 0;
|
|
311
|
-
const expVal = expByTask.get(task);
|
|
312
|
-
if (expVal !== undefined) {
|
|
313
|
-
const taskDelta = expVal - baseVal;
|
|
314
|
-
perTask.push({
|
|
315
|
-
task,
|
|
316
|
-
baseline: baseVal,
|
|
317
|
-
current: expVal,
|
|
318
|
-
delta: taskDelta,
|
|
319
|
-
});
|
|
320
|
-
if (taskDelta < -thresholds.perTask) {
|
|
321
|
-
breaches.push(`${task}: dropped ${Math.abs(taskDelta)} points`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (delta < -thresholds.aggregate) {
|
|
326
|
-
breaches.push(`Aggregate score dropped ${Math.abs(delta).toFixed(1)} points (threshold: ${thresholds.aggregate})`);
|
|
327
|
-
}
|
|
328
|
-
return { baselineMean, currentMean, delta, perTask, breaches };
|
|
329
|
-
}
|
|
330
|
-
// ---------------------------------------------------------------------------
|
|
331
|
-
// Timing comparison
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
function computeTimingDiff(baselineDir, experimentDir, baselineManifest, experimentManifest, thresholds) {
|
|
334
|
-
const baseCtx = findAndReadArtifact(baselineDir, baselineManifest, "pipeline", "pipeline-context");
|
|
335
|
-
const expCtx = findAndReadArtifact(experimentDir, experimentManifest, "pipeline", "pipeline-context");
|
|
336
|
-
if (!baseCtx || !expCtx)
|
|
337
|
-
return undefined;
|
|
338
|
-
const baseSteps = (baseCtx.steps ?? []).filter((s) => s.durationMs !== undefined);
|
|
339
|
-
const expSteps = (expCtx.steps ?? []).filter((s) => s.durationMs !== undefined);
|
|
340
|
-
const expByName = new Map(expSteps.map((s) => [s.name, s.durationMs]));
|
|
341
|
-
const perStep = [];
|
|
342
|
-
const breaches = [];
|
|
343
|
-
let totalBaseline = 0;
|
|
344
|
-
let totalExperiment = 0;
|
|
345
|
-
for (const base of baseSteps) {
|
|
346
|
-
const expMs = expByName.get(base.name);
|
|
347
|
-
if (expMs !== undefined) {
|
|
348
|
-
const baseMs = base.durationMs;
|
|
349
|
-
const ratio = baseMs > 0 ? expMs / baseMs : expMs > 0 ? Infinity : 1;
|
|
350
|
-
perStep.push({
|
|
351
|
-
step: base.name,
|
|
352
|
-
baselineMs: baseMs,
|
|
353
|
-
currentMs: expMs,
|
|
354
|
-
ratio,
|
|
355
|
-
});
|
|
356
|
-
const stepThreshold = thresholds.perStep?.[base.name] ?? thresholds.multiplier;
|
|
357
|
-
if (ratio > stepThreshold) {
|
|
358
|
-
breaches.push(`${base.name}: ${ratio.toFixed(1)}x slower (threshold: ${stepThreshold}x)`);
|
|
359
|
-
}
|
|
360
|
-
totalBaseline += baseMs;
|
|
361
|
-
totalExperiment += expMs;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
totalDeltaMs: totalExperiment - totalBaseline,
|
|
366
|
-
perStep,
|
|
367
|
-
breaches,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
// ---------------------------------------------------------------------------
|
|
371
|
-
// Metadata comparison
|
|
372
|
-
// ---------------------------------------------------------------------------
|
|
373
|
-
function computeMetadataDiff(baselineDir, experimentDir, baselineManifest, experimentManifest, ephemeral) {
|
|
374
|
-
const modeMatch = baselineManifest.pipeline.mode === experimentManifest.pipeline.mode;
|
|
375
|
-
const variantMatch = baselineManifest.pipeline.variant === experimentManifest.pipeline.variant;
|
|
376
|
-
// Compare config from pipeline-context
|
|
377
|
-
const baseCtx = findAndReadArtifact(baselineDir, baselineManifest, "pipeline", "pipeline-context");
|
|
378
|
-
const expCtx = findAndReadArtifact(experimentDir, experimentManifest, "pipeline", "pipeline-context");
|
|
379
|
-
let configDiffs = [];
|
|
380
|
-
if (baseCtx && expCtx) {
|
|
381
|
-
const baseConfig = baseCtx.config ?? {};
|
|
382
|
-
const expConfig = expCtx.config ?? {};
|
|
383
|
-
configDiffs = diffJsonStructural(stripEphemeral(baseConfig, ephemeral), stripEphemeral(expConfig, ephemeral), "config", 3);
|
|
384
|
-
}
|
|
385
|
-
return { modeMatch, variantMatch, configDiffs };
|
|
386
|
-
}
|
|
387
|
-
// ---------------------------------------------------------------------------
|
|
388
|
-
// Security scan
|
|
389
|
-
// ---------------------------------------------------------------------------
|
|
390
|
-
/** Patterns that indicate potential secret values (not just key names). */
|
|
391
|
-
const SECRET_VALUE_PATTERNS = [
|
|
392
|
-
/^sk-[a-zA-Z0-9_-]{20,}/, // OpenAI-style keys
|
|
393
|
-
/^sk[A-Z][a-zA-Z0-9]{20,}/, // Sanity-style tokens (e.g., skJ3rMwt…)
|
|
394
|
-
/^xoxb-/, // Slack bot tokens
|
|
395
|
-
/^ghp_/, // GitHub personal tokens
|
|
396
|
-
/^ghs_/, // GitHub server-to-server tokens
|
|
397
|
-
/^Bearer\s+\S{20,}/, // Authorization header values
|
|
398
|
-
];
|
|
399
|
-
/**
|
|
400
|
-
* Keys that should never appear with non-empty string values in captured artifacts.
|
|
401
|
-
*
|
|
402
|
-
* Uses case-insensitive matching without word boundaries to handle camelCase
|
|
403
|
-
* (e.g., "apiToken", "secretKey"). Intentionally broader than the orchestrator's
|
|
404
|
-
* sanitization pattern (/token|secret|key/i) — this also catches "password",
|
|
405
|
-
* "credential", and "authorization" in artifacts that bypass orchestrator
|
|
406
|
-
* sanitization (e.g., in-memory captures from PublishReportStep or CallbackStep).
|
|
407
|
-
*/
|
|
408
|
-
const SECRET_KEY_PATTERN = /(?:api[_-]?token|auth(?:orization|[_-]?token)|access[_-]?token)|secret|apiKey|api_key|password|credential|^set-cookie$|^cookie$/i;
|
|
409
|
-
function scanForSecrets(baselineDir, experimentDir, baselineManifest, experimentManifest) {
|
|
410
|
-
const violations = [];
|
|
411
|
-
for (const [dir, manifest] of [
|
|
412
|
-
[experimentDir, experimentManifest],
|
|
413
|
-
[baselineDir, baselineManifest],
|
|
414
|
-
]) {
|
|
415
|
-
for (const artifact of manifest.artifacts) {
|
|
416
|
-
const filePath = join(dir, artifact.path);
|
|
417
|
-
if (!existsSync(filePath))
|
|
418
|
-
continue;
|
|
419
|
-
const content = readFileSync(filePath, "utf-8");
|
|
420
|
-
if (artifact.format === "json") {
|
|
421
|
-
try {
|
|
422
|
-
const data = JSON.parse(content);
|
|
423
|
-
scanJsonForSecrets(data, artifact.path, "", violations);
|
|
424
|
-
}
|
|
425
|
-
catch {
|
|
426
|
-
// Non-parseable — skip
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
// Text/markdown: scan for secret-looking strings
|
|
431
|
-
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
432
|
-
if (pattern.test(content)) {
|
|
433
|
-
violations.push({
|
|
434
|
-
file: artifact.path,
|
|
435
|
-
detail: `Content matches secret pattern: ${pattern.source}`,
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return { leaksFound: violations.length > 0, violations };
|
|
443
|
-
}
|
|
444
|
-
function scanJsonForSecrets(data, file, path, violations, depth = 0) {
|
|
445
|
-
if (depth > 10)
|
|
446
|
-
return;
|
|
447
|
-
if (typeof data === "string") {
|
|
448
|
-
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
449
|
-
if (pattern.test(data)) {
|
|
450
|
-
violations.push({
|
|
451
|
-
file,
|
|
452
|
-
detail: `Value at "${path}" matches secret pattern: ${pattern.source}`,
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
else if (Array.isArray(data)) {
|
|
458
|
-
for (let i = 0; i < data.length; i++) {
|
|
459
|
-
scanJsonForSecrets(data[i], file, `${path}[${i}]`, violations, depth + 1);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
else if (typeof data === "object" && data !== null) {
|
|
463
|
-
for (const [key, value] of Object.entries(data)) {
|
|
464
|
-
// Check if a key name looks like it holds a secret AND has a string value
|
|
465
|
-
if (SECRET_KEY_PATTERN.test(key) &&
|
|
466
|
-
typeof value === "string" &&
|
|
467
|
-
value.length > 0) {
|
|
468
|
-
violations.push({
|
|
469
|
-
file,
|
|
470
|
-
detail: `Key "${path ? path + "." : ""}${key}" may contain a secret value`,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
scanJsonForSecrets(value, file, path ? `${path}.${key}` : key, violations, depth + 1);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// ---------------------------------------------------------------------------
|
|
478
|
-
// Helpers
|
|
479
|
-
// ---------------------------------------------------------------------------
|
|
480
|
-
function findAndReadArtifact(dir, manifest, step, type) {
|
|
481
|
-
const entry = manifest.artifacts.find((a) => a.step === step && a.type === type);
|
|
482
|
-
if (!entry)
|
|
483
|
-
return undefined;
|
|
484
|
-
const filePath = join(dir, entry.path);
|
|
485
|
-
if (!existsSync(filePath))
|
|
486
|
-
return undefined;
|
|
487
|
-
try {
|
|
488
|
-
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
489
|
-
}
|
|
490
|
-
catch {
|
|
491
|
-
return undefined;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FilesystemArtifactCollector — writes captured artifacts to a local directory.
|
|
3
|
-
*
|
|
4
|
-
* Accumulates artifact entries in memory during pipeline execution.
|
|
5
|
-
* On flush(), creates a structured directory with one subdirectory per
|
|
6
|
-
* step, writes all artifacts, and generates a manifest.json.
|
|
7
|
-
*
|
|
8
|
-
* Design principles:
|
|
9
|
-
* - capture() and captureFile() are synchronous (no I/O during step execution)
|
|
10
|
-
* - flush() does all I/O at pipeline end
|
|
11
|
-
* - Failures in capture/captureFile are swallowed (P5: non-blocking)
|
|
12
|
-
*/
|
|
13
|
-
import type { ArtifactCollector, CaptureFlushResult } from "../_vendor/ailf-core/index.d.ts";
|
|
14
|
-
export interface FilesystemCollectorOptions {
|
|
15
|
-
/** Base directory for capture output (e.g., results/captures/) */
|
|
16
|
-
captureDir: string;
|
|
17
|
-
/** Pipeline mode (for directory naming) */
|
|
18
|
-
mode: string;
|
|
19
|
-
/** Whether to compress on flush (Phase 5 — currently ignored) */
|
|
20
|
-
compress: boolean;
|
|
21
|
-
/** Whether mode-specific extras are enabled */
|
|
22
|
-
extras: boolean;
|
|
23
|
-
/** Pipeline metadata for the manifest */
|
|
24
|
-
pipeline?: {
|
|
25
|
-
variant?: string;
|
|
26
|
-
source?: string;
|
|
27
|
-
areas?: string[];
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export declare class FilesystemArtifactCollector implements ArtifactCollector {
|
|
31
|
-
readonly enabled = true;
|
|
32
|
-
readonly extrasEnabled: boolean;
|
|
33
|
-
private readonly entries;
|
|
34
|
-
private readonly captureId;
|
|
35
|
-
private readonly outputDir;
|
|
36
|
-
private readonly startedAt;
|
|
37
|
-
private readonly options;
|
|
38
|
-
constructor(options: FilesystemCollectorOptions);
|
|
39
|
-
capture(step: string, type: string, data: unknown, meta?: Record<string, unknown>): void;
|
|
40
|
-
captureFile(step: string, type: string, filePath: string, meta?: Record<string, unknown>): void;
|
|
41
|
-
flush(): Promise<CaptureFlushResult>;
|
|
42
|
-
}
|