@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.
Files changed (92) hide show
  1. package/dist/_vendor/ailf-core/artifact-capture/association.d.ts +35 -0
  2. package/dist/_vendor/ailf-core/artifact-capture/association.js +28 -0
  3. package/dist/_vendor/ailf-core/artifact-registry.d.ts +173 -0
  4. package/dist/_vendor/ailf-core/artifact-registry.js +811 -0
  5. package/dist/_vendor/ailf-core/index.d.ts +3 -1
  6. package/dist/_vendor/ailf-core/index.js +3 -1
  7. package/dist/_vendor/ailf-core/ports/artifact-collector.d.ts +3 -3
  8. package/dist/_vendor/ailf-core/ports/artifact-writer.d.ts +95 -0
  9. package/dist/_vendor/ailf-core/ports/artifact-writer.js +51 -0
  10. package/dist/_vendor/ailf-core/ports/context.d.ts +32 -3
  11. package/dist/_vendor/ailf-core/ports/index.d.ts +3 -3
  12. package/dist/_vendor/ailf-core/ports/index.js +1 -1
  13. package/dist/_vendor/ailf-core/schemas/pipeline.d.ts +6 -6
  14. package/dist/_vendor/ailf-core/services/index.d.ts +1 -0
  15. package/dist/_vendor/ailf-core/services/index.js +1 -0
  16. package/dist/_vendor/ailf-core/services/slim-report-summary.d.ts +31 -0
  17. package/dist/_vendor/ailf-core/services/slim-report-summary.js +217 -0
  18. package/dist/_vendor/ailf-core/types/branded-ids.d.ts +42 -0
  19. package/dist/_vendor/ailf-core/types/branded-ids.js +21 -0
  20. package/dist/_vendor/ailf-core/types/index.d.ts +298 -77
  21. package/dist/_vendor/ailf-core/types/index.js +1 -1
  22. package/dist/_vendor/ailf-shared/index.d.ts +2 -0
  23. package/dist/_vendor/ailf-shared/index.js +2 -0
  24. package/dist/_vendor/ailf-shared/run-context.d.ts +55 -0
  25. package/dist/_vendor/ailf-shared/run-context.js +17 -0
  26. package/dist/_vendor/ailf-shared/run-trigger.d.ts +30 -0
  27. package/dist/_vendor/ailf-shared/run-trigger.js +13 -0
  28. package/dist/artifact-capture/accumulating-artifact-writer.d.ts +50 -0
  29. package/dist/artifact-capture/accumulating-artifact-writer.js +111 -0
  30. package/dist/artifact-capture/api-gateway-artifact-writer.d.ts +52 -0
  31. package/dist/artifact-capture/api-gateway-artifact-writer.js +199 -0
  32. package/dist/artifact-capture/emit-file.d.ts +28 -0
  33. package/dist/artifact-capture/emit-file.js +56 -0
  34. package/dist/artifact-capture/fanout-artifact-writer.d.ts +39 -0
  35. package/dist/artifact-capture/fanout-artifact-writer.js +76 -0
  36. package/dist/artifact-capture/filesystem-collector.d.ts +22 -4
  37. package/dist/artifact-capture/filesystem-collector.js +48 -23
  38. package/dist/artifact-capture/gcs-artifact-writer.d.ts +67 -0
  39. package/dist/artifact-capture/gcs-artifact-writer.js +343 -0
  40. package/dist/artifact-capture/local-fs-artifact-writer.d.ts +71 -0
  41. package/dist/artifact-capture/local-fs-artifact-writer.js +273 -0
  42. package/dist/commands/explain-handler.js +4 -0
  43. package/dist/commands/pipeline-action.d.ts +5 -0
  44. package/dist/commands/pipeline-action.js +56 -5
  45. package/dist/commands/pipeline.d.ts +4 -0
  46. package/dist/commands/pipeline.js +6 -2
  47. package/dist/commands/publish.js +7 -3
  48. package/dist/composition-root.d.ts +14 -11
  49. package/dist/composition-root.js +90 -31
  50. package/dist/orchestration/build-step-sequence.js +6 -1
  51. package/dist/orchestration/pipeline-orchestrator.d.ts +1 -1
  52. package/dist/orchestration/pipeline-orchestrator.js +41 -30
  53. package/dist/orchestration/steps/calculate-scores-step.d.ts +1 -1
  54. package/dist/orchestration/steps/calculate-scores-step.js +50 -10
  55. package/dist/orchestration/steps/callback-step.d.ts +1 -1
  56. package/dist/orchestration/steps/callback-step.js +6 -4
  57. package/dist/orchestration/steps/compare-step.d.ts +1 -1
  58. package/dist/orchestration/steps/compare-step.js +4 -2
  59. package/dist/orchestration/steps/discovery-report-step.d.ts +1 -1
  60. package/dist/orchestration/steps/discovery-report-step.js +4 -1
  61. package/dist/orchestration/steps/fetch-docs-step.js +9 -15
  62. package/dist/orchestration/steps/finalize-run-step.d.ts +29 -0
  63. package/dist/orchestration/steps/finalize-run-step.js +117 -0
  64. package/dist/orchestration/steps/gap-analysis-step.js +34 -6
  65. package/dist/orchestration/steps/generate-configs-step.d.ts +1 -1
  66. package/dist/orchestration/steps/generate-configs-step.js +11 -11
  67. package/dist/orchestration/steps/publish-report-step.d.ts +1 -1
  68. package/dist/orchestration/steps/publish-report-step.js +40 -55
  69. package/dist/orchestration/steps/readiness-step.d.ts +1 -1
  70. package/dist/orchestration/steps/readiness-step.js +4 -1
  71. package/dist/orchestration/steps/report-step.d.ts +1 -1
  72. package/dist/orchestration/steps/report-step.js +6 -3
  73. package/dist/orchestration/steps/run-eval-step.js +14 -9
  74. package/dist/pipeline/calculate-scores.js +13 -2
  75. package/dist/pipeline/compare.d.ts +2 -2
  76. package/dist/pipeline/emit-eval-results.d.ts +38 -0
  77. package/dist/pipeline/emit-eval-results.js +100 -0
  78. package/dist/pipeline/provenance.d.ts +24 -44
  79. package/dist/pipeline/provenance.js +17 -165
  80. package/dist/pipeline/report-title.d.ts +2 -2
  81. package/dist/pipeline/run-context.d.ts +57 -0
  82. package/dist/pipeline/run-context.js +156 -0
  83. package/dist/pipeline/upload-test-outputs.d.ts +26 -0
  84. package/dist/pipeline/upload-test-outputs.js +34 -0
  85. package/dist/report-store.js +4 -2
  86. package/package.json +3 -3
  87. package/dist/_vendor/ailf-core/ports/artifact-uploader.d.ts +0 -35
  88. package/dist/_vendor/ailf-core/ports/artifact-uploader.js +0 -18
  89. package/dist/artifact-capture/api-gateway-artifact-uploader.d.ts +0 -41
  90. package/dist/artifact-capture/api-gateway-artifact-uploader.js +0 -123
  91. package/dist/artifact-capture/gcs-report-artifact-uploader.d.ts +0 -31
  92. 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 || process.env.AILF_CAPTURE === "1",
267
- captureDir: opts.captureDir ?? process.env.AILF_CAPTURE_DIR,
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
- config.captureEnabled = cliOpts.capture || process.env.AILF_CAPTURE === "1";
321
- if (cliOpts.captureDir ?? process.env.AILF_CAPTURE_DIR) {
322
- config.captureDir = cliOpts.captureDir ?? process.env.AILF_CAPTURE_DIR;
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 for this run", false)
58
- .option("--capture-dir <path>", "Base directory for capture output (default: .ailf/results/captures/)")
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);
@@ -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 ArtifactUploader, type AssertionRegistration, type Logger, type ResolvedConfig } from "./_vendor/ailf-core/index.d.ts";
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 an ArtifactUploader implementation based on available credentials.
27
+ * Selects the `ArtifactWriter` wiring per D0033 M4:
28
28
  *
29
- * Selection order:
30
- * 1. config.artifactUpload === false → always skip (explicit opt-out)
31
- * 2. GOOGLE_APPLICATION_CREDENTIALS or GCLOUD_PROJECT present → direct GCS
32
- * 3. apiKey + apiUrl present → gateway-signed PUT URL
33
- * 4. Neither skip silently (P5)
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
- * The bucket defaults to DEFAULT_ARTIFACT_BUCKET when not explicitly set
36
- * users only need to override for self-hosted deployments with a different
37
- * bucket (and matching gateway signing credentials).
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 createArtifactUploader(config: ResolvedConfig, logger: Logger): ArtifactUploader | undefined;
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
  *