@sanity/ailf 2.7.1 → 2.9.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 +173 -0
- package/dist/_vendor/ailf-core/artifact-registry.js +811 -0
- package/dist/_vendor/ailf-core/index.d.ts +3 -1
- package/dist/_vendor/ailf-core/index.js +3 -1
- package/dist/_vendor/ailf-core/ports/artifact-collector.d.ts +3 -3
- package/dist/_vendor/ailf-core/ports/artifact-writer.d.ts +95 -0
- package/dist/_vendor/ailf-core/ports/artifact-writer.js +51 -0
- package/dist/_vendor/ailf-core/ports/context.d.ts +32 -3
- package/dist/_vendor/ailf-core/ports/index.d.ts +3 -3
- package/dist/_vendor/ailf-core/ports/index.js +1 -1
- 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 +42 -0
- package/dist/_vendor/ailf-core/types/branded-ids.js +21 -0
- package/dist/_vendor/ailf-core/types/index.d.ts +298 -77
- package/dist/_vendor/ailf-core/types/index.js +1 -1
- package/dist/_vendor/ailf-shared/index.d.ts +2 -0
- package/dist/_vendor/ailf-shared/index.js +2 -0
- package/dist/_vendor/ailf-shared/run-context.d.ts +55 -0
- package/dist/_vendor/ailf-shared/run-context.js +17 -0
- package/dist/_vendor/ailf-shared/run-trigger.d.ts +30 -0
- package/dist/_vendor/ailf-shared/run-trigger.js +13 -0
- 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 +52 -0
- package/dist/artifact-capture/api-gateway-artifact-writer.js +199 -0
- 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/filesystem-collector.d.ts +22 -4
- package/dist/artifact-capture/filesystem-collector.js +48 -23
- package/dist/artifact-capture/gcs-artifact-writer.d.ts +67 -0
- package/dist/artifact-capture/gcs-artifact-writer.js +343 -0
- 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/commands/explain-handler.js +4 -0
- package/dist/commands/pipeline-action.d.ts +5 -0
- package/dist/commands/pipeline-action.js +56 -5
- package/dist/commands/pipeline.d.ts +4 -0
- package/dist/commands/pipeline.js +6 -2
- package/dist/commands/publish.js +7 -3
- package/dist/composition-root.d.ts +14 -11
- package/dist/composition-root.js +90 -31
- package/dist/orchestration/build-step-sequence.js +6 -1
- package/dist/orchestration/pipeline-orchestrator.d.ts +1 -1
- package/dist/orchestration/pipeline-orchestrator.js +41 -30
- package/dist/orchestration/steps/calculate-scores-step.d.ts +1 -1
- package/dist/orchestration/steps/calculate-scores-step.js +50 -10
- 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.d.ts +29 -0
- package/dist/orchestration/steps/finalize-run-step.js +117 -0
- 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 +40 -55
- 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/calculate-scores.js +13 -2
- 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/provenance.d.ts +24 -44
- package/dist/pipeline/provenance.js +17 -165
- package/dist/pipeline/report-title.d.ts +2 -2
- package/dist/pipeline/run-context.d.ts +57 -0
- package/dist/pipeline/run-context.js +156 -0
- package/dist/pipeline/upload-test-outputs.d.ts +26 -0
- package/dist/pipeline/upload-test-outputs.js +34 -0
- package/dist/report-store.js +4 -2
- package/package.json +3 -3
- package/dist/_vendor/ailf-core/ports/artifact-uploader.d.ts +0 -35
- package/dist/_vendor/ailf-core/ports/artifact-uploader.js +0 -18
- package/dist/artifact-capture/api-gateway-artifact-uploader.d.ts +0 -41
- package/dist/artifact-capture/api-gateway-artifact-uploader.js +0 -123
- package/dist/artifact-capture/gcs-report-artifact-uploader.d.ts +0 -31
- package/dist/artifact-capture/gcs-report-artifact-uploader.js +0 -66
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalFilesystemArtifactWriter — writes AILF run artifacts to the local
|
|
3
|
+
* filesystem under `{rootDir}/runs/{runId}/…`.
|
|
4
|
+
*
|
|
5
|
+
* D0033 M4 inverts D0032's "GCS default, local fallback" stance: the local
|
|
6
|
+
* writer is **always** attached, and GCS layers on top via a fanout writer
|
|
7
|
+
* when credentials are present. The result — every run produces a usable
|
|
8
|
+
* artifact bundle on disk even on airplanes, laptops, and CI without GCS
|
|
9
|
+
* creds. Studio retrieval works uniformly; only `ArtifactRef.store`
|
|
10
|
+
* differentiates.
|
|
11
|
+
*
|
|
12
|
+
* ## Path layout
|
|
13
|
+
*
|
|
14
|
+
* Paths mirror the GCS tree exactly, so the same descriptor's `objectPath`
|
|
15
|
+
* is used verbatim:
|
|
16
|
+
* - bulk: `{rootDir}/runs/{runId}/{slug}.{ext}`
|
|
17
|
+
* - per-entry: `{rootDir}/runs/{runId}/{slug}/{sanitizedKey}.{ext}`
|
|
18
|
+
* - manifest: `{rootDir}/runs/{runId}/manifest.json`
|
|
19
|
+
*
|
|
20
|
+
* This keeps the L6 cross-reader contract test simple — a byte-compare of
|
|
21
|
+
* every object at every path, modulo timestamped manifest fields.
|
|
22
|
+
*
|
|
23
|
+
* ## Design choices
|
|
24
|
+
*
|
|
25
|
+
* - **Redaction at emit boundary (AC10).** `redactArtifactData` runs per
|
|
26
|
+
* write, not post-hoc on a tarball. Same rules as the legacy collector.
|
|
27
|
+
* - **Exclude list gating (Q3).** `--capture-exclude=LIST` passes through
|
|
28
|
+
* to the constructor; `emit()` returns null for excluded types before
|
|
29
|
+
* touching disk.
|
|
30
|
+
* - **NDJSON uses plain `fs.appendFile`.** No compose rollover needed —
|
|
31
|
+
* unlike GCS, local fs appends are atomic and unbounded. A crash mid-
|
|
32
|
+
* append leaves a partial row visible; acceptable for dev runs.
|
|
33
|
+
* - **Non-blocking per P5.** Any fs error returns null + warns, never
|
|
34
|
+
* throws. The pipeline must not fail because local disk is full.
|
|
35
|
+
*
|
|
36
|
+
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M4)
|
|
37
|
+
* @see packages/eval/src/artifact-capture/gcs-artifact-writer.ts (mirror)
|
|
38
|
+
*/
|
|
39
|
+
import { promises as fs } from "node:fs";
|
|
40
|
+
import path from "node:path";
|
|
41
|
+
import { ARTIFACT_REGISTRY, buildManifestPreview, } from "../_vendor/ailf-core/index.js";
|
|
42
|
+
import { redactArtifactData } from "./redact-artifact.js";
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Implementation
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export class LocalFilesystemArtifactWriter {
|
|
47
|
+
options;
|
|
48
|
+
excludeSet;
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this.options = options;
|
|
51
|
+
this.excludeSet = new Set(options.exclude ?? []);
|
|
52
|
+
}
|
|
53
|
+
// ---- Canonical W0049 API ------------------------------------------------
|
|
54
|
+
async emit(type, association, payload) {
|
|
55
|
+
if (this.excludeSet.has(type))
|
|
56
|
+
return null;
|
|
57
|
+
const descriptor = ARTIFACT_REGISTRY[type];
|
|
58
|
+
const runId = association.run;
|
|
59
|
+
if (!runId) {
|
|
60
|
+
console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const redacted = redactArtifactData(payload);
|
|
64
|
+
const body = serializeForMime(redacted, descriptor.mime);
|
|
65
|
+
const bytes = Buffer.byteLength(body, "utf-8");
|
|
66
|
+
// Preview is built from the pre-redaction payload so the extract sees
|
|
67
|
+
// the same shape the producer handed us. The full entry is still redacted
|
|
68
|
+
// on disk; the preview lives only on the manifest and is bounded by the
|
|
69
|
+
// descriptor's capBytes.
|
|
70
|
+
const preview = buildManifestPreview(descriptor, payload);
|
|
71
|
+
if (descriptor.layout === "bulk") {
|
|
72
|
+
const relPath = descriptor.objectPath(runId);
|
|
73
|
+
const absPath = this.resolve(relPath);
|
|
74
|
+
const wrote = await this.writeAtomic(absPath, body);
|
|
75
|
+
if (!wrote)
|
|
76
|
+
return null;
|
|
77
|
+
return {
|
|
78
|
+
store: "local",
|
|
79
|
+
bucket: this.options.rootDir,
|
|
80
|
+
path: relPath,
|
|
81
|
+
bytes,
|
|
82
|
+
entryCount: entryCountOf(redacted),
|
|
83
|
+
layout: "bulk",
|
|
84
|
+
...(preview === undefined ? {} : { preview }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// per-entry
|
|
88
|
+
const entryKey = descriptor.formatEntryKey(association);
|
|
89
|
+
const relPath = descriptor.objectPath(runId, entryKey);
|
|
90
|
+
const absPath = this.resolve(relPath);
|
|
91
|
+
const wrote = await this.writeAtomic(absPath, body);
|
|
92
|
+
if (!wrote)
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
store: "local",
|
|
96
|
+
bucket: this.options.rootDir,
|
|
97
|
+
path: `runs/${runId}/${descriptor.slug}`,
|
|
98
|
+
bytes,
|
|
99
|
+
entryCount: 1,
|
|
100
|
+
layout: "per-entry",
|
|
101
|
+
entries: [
|
|
102
|
+
{
|
|
103
|
+
key: entryKey,
|
|
104
|
+
bytes,
|
|
105
|
+
association,
|
|
106
|
+
...(preview === undefined ? {} : { preview }),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async appendNdjson(type, association, rows) {
|
|
112
|
+
if (this.excludeSet.has(type))
|
|
113
|
+
return null;
|
|
114
|
+
const descriptor = ARTIFACT_REGISTRY[type];
|
|
115
|
+
if (descriptor.mime !== "application/x-ndjson") {
|
|
116
|
+
console.warn(` ⚠️ appendNdjson("${type}"): descriptor mime is ${descriptor.mime}, not application/x-ndjson — skipping`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const runId = association.run;
|
|
120
|
+
if (!runId) {
|
|
121
|
+
console.warn(` ⚠️ appendNdjson("${type}"): association.run is required, skipping`);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (rows.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const entryKey = descriptor.formatEntryKey(association);
|
|
127
|
+
const relPath = descriptor.objectPath(runId, entryKey);
|
|
128
|
+
const absPath = this.resolve(relPath);
|
|
129
|
+
const redactedRows = rows.map((r) => redactArtifactData(r));
|
|
130
|
+
const body = redactedRows.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
131
|
+
const bytes = Buffer.byteLength(body, "utf-8");
|
|
132
|
+
try {
|
|
133
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
134
|
+
await fs.appendFile(absPath, body, "utf-8");
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
+
console.warn(` ⚠️ NDJSON append failed (non-blocking): ${absPath} — ${message}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
// Local fs appendFile keeps no part state, so the returned ref always
|
|
142
|
+
// reports the *cumulative* size on disk — same semantic as the GCS
|
|
143
|
+
// writer's `state.totalBytes`, just computed differently.
|
|
144
|
+
let cumulative = bytes;
|
|
145
|
+
try {
|
|
146
|
+
const stat = await fs.stat(absPath);
|
|
147
|
+
cumulative = stat.size;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// If stat fails we still have the current batch's bytes — acceptable.
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
store: "local",
|
|
154
|
+
bucket: this.options.rootDir,
|
|
155
|
+
path: `runs/${runId}/${descriptor.slug}`,
|
|
156
|
+
bytes: cumulative,
|
|
157
|
+
entryCount: 1,
|
|
158
|
+
layout: "per-entry",
|
|
159
|
+
entries: [{ key: entryKey, bytes: cumulative, association }],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async writeManifest(runId, manifest) {
|
|
163
|
+
const relPath = `runs/${runId}/manifest.json`;
|
|
164
|
+
const absPath = this.resolve(relPath);
|
|
165
|
+
const body = JSON.stringify(manifest);
|
|
166
|
+
const wrote = await this.writeAtomic(absPath, body);
|
|
167
|
+
if (!wrote)
|
|
168
|
+
return null;
|
|
169
|
+
return {
|
|
170
|
+
store: "local",
|
|
171
|
+
bucket: this.options.rootDir,
|
|
172
|
+
path: relPath,
|
|
173
|
+
bytes: Buffer.byteLength(body, "utf-8"),
|
|
174
|
+
layout: "bulk",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// ---- Deprecated legacy surface (W0052) ----------------------------------
|
|
178
|
+
/** @deprecated Use `emit()` instead. */
|
|
179
|
+
async writeBulk(type, runId, data) {
|
|
180
|
+
const descriptor = ARTIFACT_REGISTRY[type];
|
|
181
|
+
if (descriptor.layout !== "bulk") {
|
|
182
|
+
console.warn(` ⚠️ writeBulk("${type}"): descriptor layout is "${descriptor.layout}", not "bulk" — skipping`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return this.emit(type, { run: runId }, data);
|
|
186
|
+
}
|
|
187
|
+
/** @deprecated Use `emit()` per entry instead. */
|
|
188
|
+
async writePerEntry(type, runId, entries) {
|
|
189
|
+
const descriptor = ARTIFACT_REGISTRY[type];
|
|
190
|
+
if (descriptor.layout !== "per-entry") {
|
|
191
|
+
console.warn(` ⚠️ writePerEntry("${type}"): descriptor layout is "${descriptor.layout}", not "per-entry" — skipping`);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!descriptor.parseEntryKey) {
|
|
195
|
+
console.warn(` ⚠️ writePerEntry("${type}"): descriptor has no parseEntryKey`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
// Legacy shim: write each entry directly by its raw key, bypassing
|
|
199
|
+
// formatEntryKey. Preserves byte-equivalence with pre-W0050 paths
|
|
200
|
+
// for producers still on the legacy call surface.
|
|
201
|
+
const uploaded = [];
|
|
202
|
+
let totalBytes = 0;
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
const parsed = descriptor.parseEntryKey(entry.key);
|
|
205
|
+
if (!parsed.ok) {
|
|
206
|
+
console.warn(` ⚠️ Skipping entry with invalid key "${entry.key}": ${parsed.reason}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const redacted = redactArtifactData(entry.data);
|
|
210
|
+
const body = serializeForMime(redacted, descriptor.mime);
|
|
211
|
+
const bytes = Buffer.byteLength(body, "utf-8");
|
|
212
|
+
const relPath = descriptor.objectPath(runId, entry.key);
|
|
213
|
+
const absPath = this.resolve(relPath);
|
|
214
|
+
const wrote = await this.writeAtomic(absPath, body);
|
|
215
|
+
if (!wrote)
|
|
216
|
+
continue;
|
|
217
|
+
uploaded.push({ key: entry.key, bytes });
|
|
218
|
+
totalBytes += bytes;
|
|
219
|
+
}
|
|
220
|
+
if (uploaded.length === 0)
|
|
221
|
+
return null;
|
|
222
|
+
return {
|
|
223
|
+
store: "local",
|
|
224
|
+
bucket: this.options.rootDir,
|
|
225
|
+
path: `runs/${runId}/${descriptor.slug}`,
|
|
226
|
+
bytes: totalBytes,
|
|
227
|
+
entryCount: uploaded.length,
|
|
228
|
+
layout: "per-entry",
|
|
229
|
+
entries: uploaded,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// ---- Internals ----------------------------------------------------------
|
|
233
|
+
resolve(relPath) {
|
|
234
|
+
return path.resolve(this.options.rootDir, relPath);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Write the body to `absPath`, creating parent dirs as needed. Returns
|
|
238
|
+
* false + warns on any fs error (P5 non-blocking).
|
|
239
|
+
*/
|
|
240
|
+
async writeAtomic(absPath, body) {
|
|
241
|
+
try {
|
|
242
|
+
await fs.mkdir(path.dirname(absPath), { recursive: true });
|
|
243
|
+
await fs.writeFile(absPath, body, "utf-8");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
248
|
+
console.warn(` ⚠️ Artifact write failed (non-blocking): ${absPath} — ${message}`);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Helpers (shared shape with GcsArtifactWriter)
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
function serializeForMime(payload, mime) {
|
|
257
|
+
if (mime === "text/markdown" || mime === "application/yaml") {
|
|
258
|
+
if (typeof payload === "string")
|
|
259
|
+
return payload;
|
|
260
|
+
return String(payload ?? "");
|
|
261
|
+
}
|
|
262
|
+
return JSON.stringify(payload);
|
|
263
|
+
}
|
|
264
|
+
function entryCountOf(data) {
|
|
265
|
+
if (typeof data === "object" &&
|
|
266
|
+
data !== null &&
|
|
267
|
+
"entries" in data &&
|
|
268
|
+
typeof data.entries === "object") {
|
|
269
|
+
return Object.keys(data.entries)
|
|
270
|
+
.length;
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
@@ -727,6 +727,10 @@ async function buildPipelineExplainPlan(actionCommand, rootDir) {
|
|
|
727
727
|
captureCompress: raw.captureCompress ?? true,
|
|
728
728
|
captureExtras: raw.captureExtras ?? true,
|
|
729
729
|
captureDir: raw.captureDir,
|
|
730
|
+
artifacts: raw.artifacts ?? true,
|
|
731
|
+
artifactsDir: raw.artifactsDir,
|
|
732
|
+
artifactsDryRun: raw.artifactsDryRun ?? false,
|
|
733
|
+
captureExclude: raw.captureExclude,
|
|
730
734
|
};
|
|
731
735
|
const resolved = computeResolvedOptions(withDefaults);
|
|
732
736
|
const planOpts = {
|
|
@@ -67,6 +67,11 @@ export interface ResolvedOptions {
|
|
|
67
67
|
captureDir?: string;
|
|
68
68
|
captureCompress: boolean;
|
|
69
69
|
captureExtras: boolean;
|
|
70
|
+
/** D0033 / W0049 — unified artifact surface (W0050 wires it into writer). */
|
|
71
|
+
artifactsDisabled: boolean;
|
|
72
|
+
artifactsDir?: string;
|
|
73
|
+
artifactsDryRun: boolean;
|
|
74
|
+
artifactsExclude?: readonly string[];
|
|
70
75
|
}
|
|
71
76
|
/**
|
|
72
77
|
* Pure option resolution — computes ResolvedOptions from CLI flags without
|
|
@@ -263,13 +263,40 @@ export function computeResolvedOptions(opts) {
|
|
|
263
263
|
tagOption,
|
|
264
264
|
taskSourceType: resolvedTaskSourceType,
|
|
265
265
|
urlArgs,
|
|
266
|
-
captureEnabled: opts.capture
|
|
267
|
-
captureDir: opts
|
|
266
|
+
captureEnabled: opts.capture,
|
|
267
|
+
captureDir: resolveArtifactsDir(opts),
|
|
268
268
|
captureCompress: opts.captureCompress !== false &&
|
|
269
269
|
process.env.AILF_CAPTURE_COMPRESS !== "0",
|
|
270
270
|
captureExtras: opts.captureExtras !== false && process.env.AILF_CAPTURE_EXTRAS !== "0",
|
|
271
|
+
artifactsDisabled: opts.artifacts === false,
|
|
272
|
+
artifactsDir: resolveArtifactsDir(opts),
|
|
273
|
+
artifactsDryRun: opts.artifactsDryRun,
|
|
274
|
+
artifactsExclude: parseCaptureExcludeList(opts.captureExclude),
|
|
271
275
|
};
|
|
272
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Resolve the artifacts / capture output directory from CLI flags and env
|
|
279
|
+
* vars. Precedence (highest first):
|
|
280
|
+
* 1. `--artifacts-dir` flag
|
|
281
|
+
* 2. `--capture-dir` flag (deprecated alias; no warning — silent rewrite)
|
|
282
|
+
* 3. `AILF_ARTIFACTS_DIR` env var
|
|
283
|
+
* 4. `AILF_CAPTURE_DIR` env var (deprecated alias; silent)
|
|
284
|
+
*/
|
|
285
|
+
function resolveArtifactsDir(opts) {
|
|
286
|
+
return (opts.artifactsDir ??
|
|
287
|
+
opts.captureDir ??
|
|
288
|
+
process.env.AILF_ARTIFACTS_DIR ??
|
|
289
|
+
process.env.AILF_CAPTURE_DIR);
|
|
290
|
+
}
|
|
291
|
+
function parseCaptureExcludeList(raw) {
|
|
292
|
+
if (!raw)
|
|
293
|
+
return undefined;
|
|
294
|
+
const list = raw
|
|
295
|
+
.split(",")
|
|
296
|
+
.map((s) => s.trim())
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
return list.length > 0 ? list : undefined;
|
|
299
|
+
}
|
|
273
300
|
/** Resolve and validate the --task-source flag value. */
|
|
274
301
|
function resolveTaskSourceType(raw) {
|
|
275
302
|
if (!raw || raw === "content-lake")
|
|
@@ -282,6 +309,20 @@ function resolveTaskSourceType(raw) {
|
|
|
282
309
|
// ---------------------------------------------------------------------------
|
|
283
310
|
// Pipeline entry point
|
|
284
311
|
// ---------------------------------------------------------------------------
|
|
312
|
+
/**
|
|
313
|
+
* Module-level flag so the `--capture` deprecation warning fires exactly
|
|
314
|
+
* once per process even when `executePipeline` is invoked multiple times
|
|
315
|
+
* (e.g. tests, long-lived dev loops).
|
|
316
|
+
*/
|
|
317
|
+
let warnedCaptureDeprecation = false;
|
|
318
|
+
function warnCaptureDeprecationIfNeeded(cliOpts) {
|
|
319
|
+
if (!cliOpts.capture)
|
|
320
|
+
return;
|
|
321
|
+
if (warnedCaptureDeprecation)
|
|
322
|
+
return;
|
|
323
|
+
warnedCaptureDeprecation = true;
|
|
324
|
+
console.warn("--capture is deprecated and will be removed in a future release; use --artifacts-dir or --no-artifacts instead");
|
|
325
|
+
}
|
|
285
326
|
/**
|
|
286
327
|
* Execute the evaluation pipeline.
|
|
287
328
|
*
|
|
@@ -291,6 +332,7 @@ function resolveTaskSourceType(raw) {
|
|
|
291
332
|
* 4. Delegate to the PipelineOrchestrator
|
|
292
333
|
*/
|
|
293
334
|
export async function executePipeline(cliOpts) {
|
|
335
|
+
warnCaptureDeprecationIfNeeded(cliOpts);
|
|
294
336
|
// When --config is provided, resolve config from file instead of CLI flags
|
|
295
337
|
if (cliOpts.config) {
|
|
296
338
|
const { existsSync } = await import("fs");
|
|
@@ -317,9 +359,11 @@ export async function executePipeline(cliOpts) {
|
|
|
317
359
|
config.outputDir = resolveOutputDir(cliOpts.outputDir);
|
|
318
360
|
// Capture options — CLI flags and env vars aren't in the config file,
|
|
319
361
|
// so merge them here (same logic as resolveOptions).
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
362
|
+
// AILF_CAPTURE is a no-op post-W0049; only the flag toggles captureEnabled.
|
|
363
|
+
config.captureEnabled = cliOpts.capture;
|
|
364
|
+
const resolvedArtifactsDir = resolveArtifactsDir(cliOpts);
|
|
365
|
+
if (resolvedArtifactsDir) {
|
|
366
|
+
config.captureDir = resolvedArtifactsDir;
|
|
323
367
|
}
|
|
324
368
|
config.captureCompress =
|
|
325
369
|
cliOpts.captureCompress !== false &&
|
|
@@ -328,6 +372,13 @@ export async function executePipeline(cliOpts) {
|
|
|
328
372
|
cliOpts.captureExtras !== false && process.env.AILF_CAPTURE_EXTRAS !== "0";
|
|
329
373
|
config.captureGcsBucket ??= process.env.AILF_CAPTURE_GCS_BUCKET;
|
|
330
374
|
config.captureGcsPrefix ??= process.env.AILF_CAPTURE_GCS_PREFIX;
|
|
375
|
+
config.artifactsDisabled ??= cliOpts.artifacts === false;
|
|
376
|
+
config.artifactsDir ??= resolvedArtifactsDir;
|
|
377
|
+
config.artifactsDryRun ??= cliOpts.artifactsDryRun;
|
|
378
|
+
const excludeList = parseCaptureExcludeList(cliOpts.captureExclude);
|
|
379
|
+
if (excludeList) {
|
|
380
|
+
config.artifactsExclude = excludeList;
|
|
381
|
+
}
|
|
331
382
|
config.artifactGcsBucket ??= process.env.AILF_GCS_ARTIFACT_BUCKET;
|
|
332
383
|
config.artifactUpload ??= parseArtifactUploadEnv(process.env.AILF_ARTIFACT_UPLOAD);
|
|
333
384
|
// Create AppContext directly from the merged config so adapters
|
|
@@ -68,5 +68,9 @@ export interface PipelineCliOptions {
|
|
|
68
68
|
captureDir?: string;
|
|
69
69
|
captureCompress: boolean;
|
|
70
70
|
captureExtras: boolean;
|
|
71
|
+
artifacts: boolean;
|
|
72
|
+
artifactsDir?: string;
|
|
73
|
+
artifactsDryRun: boolean;
|
|
74
|
+
captureExclude?: string;
|
|
71
75
|
}
|
|
72
76
|
export declare function createPipelineCommand(): Command;
|
|
@@ -54,10 +54,14 @@ export function createPipelineCommand() {
|
|
|
54
54
|
.option("--repo-tasks-path <path>", "Path to repo-based task definitions (.ailf/tasks/ directory)")
|
|
55
55
|
.option("--remote", "Submit evaluation to the AILF API instead of running locally", false)
|
|
56
56
|
.option("--api-url <url>", "AILF API base URL (default: https://ailf-api.sanity.build)")
|
|
57
|
-
.option("--capture", "Enable artifact capture
|
|
58
|
-
.option("--capture-dir <path>", "
|
|
57
|
+
.option("--capture", "[DEPRECATED] Enable legacy artifact capture. Use --artifacts-dir / --no-artifacts instead.", false)
|
|
58
|
+
.option("--capture-dir <path>", "[DEPRECATED] Alias for --artifacts-dir.")
|
|
59
59
|
.option("--no-capture-compress", "Disable tar.gz compression of captures")
|
|
60
60
|
.option("--no-capture-extras", "Exclude mode-specific artifacts from captures")
|
|
61
|
+
.option("--no-artifacts", "Disable all artifact writers (D0033). Overrides --artifacts-dir.")
|
|
62
|
+
.option("--artifacts-dir <path>", "Root directory for local artifact output (D0033; default: .ailf/results/captures/)")
|
|
63
|
+
.option("--artifacts-dry-run", "Run artifact writers in dry-run mode — log intended writes, touch no storage", false)
|
|
64
|
+
.option("--capture-exclude <types>", "Comma-separated artifact types to skip (e.g. traces,graderPrompts)")
|
|
61
65
|
.action(async (opts) => {
|
|
62
66
|
const { executePipeline } = await import("./pipeline-action.js");
|
|
63
67
|
await executePipeline(opts);
|
package/dist/commands/publish.js
CHANGED
|
@@ -27,6 +27,7 @@ import { addOutputDirOption } from "./shared/options.js";
|
|
|
27
27
|
import { getCallerCwd, resolveOutputDir } from "./shared/resolve-output-dir.js";
|
|
28
28
|
import { buildProvenance, } from "../pipeline/provenance.js";
|
|
29
29
|
import { generateReportTitle } from "../pipeline/report-title.js";
|
|
30
|
+
import { buildSlimReportSummary } from "../_vendor/ailf-core/index.js";
|
|
30
31
|
import { generateReportId, } from "../report-store.js";
|
|
31
32
|
import { withRetry } from "../sinks/retry.js";
|
|
32
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -55,7 +56,7 @@ export function createPublishCommand() {
|
|
|
55
56
|
* the summary metadata and environment. Some fields (contextHash,
|
|
56
57
|
* promptfooUrl) are not available for manual publishes.
|
|
57
58
|
*/
|
|
58
|
-
function buildProvenanceFromSummary(summary) {
|
|
59
|
+
function buildProvenanceFromSummary(summary, runId) {
|
|
59
60
|
const areas = summary.scores.map((s) => s.feature);
|
|
60
61
|
const mode = (process.env.EVAL_MODE ?? "literacy");
|
|
61
62
|
const source = {
|
|
@@ -76,6 +77,7 @@ function buildProvenanceFromSummary(summary) {
|
|
|
76
77
|
areas,
|
|
77
78
|
mode,
|
|
78
79
|
rootDir: ROOT,
|
|
80
|
+
runId,
|
|
79
81
|
source,
|
|
80
82
|
};
|
|
81
83
|
}
|
|
@@ -145,7 +147,7 @@ async function runPublishCommand(summaryPath, outputDir, opts) {
|
|
|
145
147
|
// -----------------------------------------------------------------------
|
|
146
148
|
// 2. Build provenance
|
|
147
149
|
// -----------------------------------------------------------------------
|
|
148
|
-
const provenanceInput = buildProvenanceFromSummary(summary);
|
|
150
|
+
const provenanceInput = buildProvenanceFromSummary(summary, ctx.runId);
|
|
149
151
|
const provenance = buildProvenance(provenanceInput);
|
|
150
152
|
// -----------------------------------------------------------------------
|
|
151
153
|
// 3. Create report
|
|
@@ -173,13 +175,15 @@ async function runPublishCommand(summaryPath, outputDir, opts) {
|
|
|
173
175
|
}
|
|
174
176
|
const reportId = generateReportId();
|
|
175
177
|
const title = generateReportTitle({ provenance });
|
|
178
|
+
// W0051 Slice 3: slim the summary at publish time.
|
|
179
|
+
const slimSummary = buildSlimReportSummary(summary, provenance.mode);
|
|
176
180
|
const report = {
|
|
177
181
|
comparison: comparison ?? undefined,
|
|
178
182
|
completedAt: now,
|
|
179
183
|
durationMs: 0, // manual publish — no pipeline duration
|
|
180
184
|
id: reportId,
|
|
181
185
|
provenance,
|
|
182
|
-
summary,
|
|
186
|
+
summary: slimSummary,
|
|
183
187
|
tag: opts.tag,
|
|
184
188
|
title,
|
|
185
189
|
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @see packages/core/src/ports/context.ts — AppContext interface
|
|
16
16
|
* @see docs/archive/exec-plans/ports-and-adapters/phase-7-composition-root.md
|
|
17
17
|
*/
|
|
18
|
-
import { type AppContext, type
|
|
18
|
+
import { type AppContext, type ArtifactWriter, type AssertionRegistration, type Logger, type ResolvedConfig } from "./_vendor/ailf-core/index.d.ts";
|
|
19
19
|
/**
|
|
20
20
|
* Create a fully wired AppContext from resolved configuration.
|
|
21
21
|
*
|
|
@@ -24,21 +24,24 @@ import { type AppContext, type ArtifactUploader, type AssertionRegistration, typ
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function createAppContext(config: ResolvedConfig): AppContext;
|
|
26
26
|
/**
|
|
27
|
-
* Selects
|
|
27
|
+
* Selects the `ArtifactWriter` wiring per D0033 M4:
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 2.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
29
|
+
* 1. `--no-artifacts` (`config.artifactsDisabled === true`, or legacy
|
|
30
|
+
* `config.artifactUpload === false`) → `NoOpArtifactWriter`.
|
|
31
|
+
* 2. Otherwise: always attach `LocalFilesystemArtifactWriter` under
|
|
32
|
+
* `--artifacts-dir` (default `.ailf/results/captures`).
|
|
33
|
+
* 3. When a remote backend is reachable (ADC, GCLOUD_PROJECT, or an
|
|
34
|
+
* AILF API key + URL), layer it via `FanoutArtifactWriter([local, gcs])`.
|
|
35
|
+
* Local is listed first so a local success + remote failure still
|
|
36
|
+
* produces a non-null ref.
|
|
34
37
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* Always returns a writer — pipeline code can assume `ctx.artifactWriter`
|
|
39
|
+
* is present. Producers post-W0050 drop their `if (ctx.artifactWriter)`
|
|
40
|
+
* guards in Slice 6.
|
|
38
41
|
*
|
|
39
42
|
* Exported for unit-test access; not part of the public package API.
|
|
40
43
|
*/
|
|
41
|
-
export declare function
|
|
44
|
+
export declare function createArtifactWriter(config: ResolvedConfig, logger: Logger): ArtifactWriter;
|
|
42
45
|
/**
|
|
43
46
|
* Generic Promptfoo assertion types available to all evaluation modes.
|
|
44
47
|
*
|