@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,111 @@
1
+ /**
2
+ * accumulating-artifact-writer.ts
3
+ *
4
+ * Decorator that wraps any `ArtifactWriter` and accumulates every
5
+ * successful `emit()` / `appendNdjson()` return into a run-scoped
6
+ * manifest slice. FinalizeRunStep reads the accumulator at the end of
7
+ * a pipeline and writes a `runs/{runId}/manifest.json` populated with
8
+ * one entry per produced artifact type.
9
+ *
10
+ * Before W0051 revisit: only `calculate-scores-step` registered its
11
+ * ref (for `testOutputs`) into `state.artifactRefs`; every other
12
+ * producer discarded the returned ref. The result was empty
13
+ * `Report.artifactManifest` fields and no per-entry preview lookup
14
+ * for Studio hooks. Wrapping the writer at the composition-root level
15
+ * closes that gap without per-producer bookkeeping.
16
+ *
17
+ * Merging rules (per type):
18
+ * - `bulk`: last-writer-wins. A pipeline that emits the same bulk
19
+ * artifact twice overwrites — matches GCS semantics.
20
+ * - `per-entry`: entries accumulate into a keyed map. A later emit
21
+ * at the same `entries[].key` replaces the earlier one.
22
+ *
23
+ * The decorator holds no disk state; the `_resetAccumulated()` hook is
24
+ * for unit tests that rerun emit sequences within a single writer.
25
+ *
26
+ * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M5)
27
+ */
28
+ export class AccumulatingArtifactWriter {
29
+ /**
30
+ * Exposed so composition-root tests can assert on the underlying backend
31
+ * (LocalFilesystemArtifactWriter, FanoutArtifactWriter, etc.) without
32
+ * plumbing a separate accessor. Treat as read-only.
33
+ */
34
+ inner;
35
+ accumulated = {};
36
+ constructor(inner) {
37
+ this.inner = inner;
38
+ }
39
+ /** Snapshot of every ref produced this far, keyed by artifact type. */
40
+ getAccumulatedArtifactRefs() {
41
+ return { ...this.accumulated };
42
+ }
43
+ /** Test-only. Clears accumulated refs without touching the inner writer. */
44
+ _resetAccumulated() {
45
+ for (const k of Object.keys(this.accumulated)) {
46
+ delete this.accumulated[k];
47
+ }
48
+ }
49
+ // ---- ArtifactWriter surface --------------------------------------------
50
+ async emit(type, association, payload) {
51
+ const ref = await this.inner.emit(type, association, payload);
52
+ if (ref)
53
+ this.mergeRef(type, ref);
54
+ return ref;
55
+ }
56
+ async appendNdjson(type, association, rows) {
57
+ const ref = await this.inner.appendNdjson(type, association, rows);
58
+ if (ref)
59
+ this.mergeRef(type, ref);
60
+ return ref;
61
+ }
62
+ async writeManifest(runId, manifest) {
63
+ return this.inner.writeManifest(runId, manifest);
64
+ }
65
+ /** @deprecated — forwarded to the inner writer without accumulation. */
66
+ async writeBulk(type, runId, data) {
67
+ const ref = await this.inner.writeBulk(type, runId, data);
68
+ if (ref)
69
+ this.mergeRef(type, ref);
70
+ return ref;
71
+ }
72
+ /** @deprecated — forwarded to the inner writer without accumulation. */
73
+ async writePerEntry(type, runId, entries) {
74
+ const ref = await this.inner.writePerEntry(type, runId, entries);
75
+ if (ref)
76
+ this.mergeRef(type, ref);
77
+ return ref;
78
+ }
79
+ // ---- Merge rules --------------------------------------------------------
80
+ mergeRef(type, ref) {
81
+ const existing = this.accumulated[type];
82
+ if (!existing) {
83
+ this.accumulated[type] = ref;
84
+ return;
85
+ }
86
+ // Bulk: last-writer-wins. A step that re-emits a bulk artifact (e.g.
87
+ // a rerun of calculate-scores) overwrites the earlier body on disk,
88
+ // so the manifest reflects the latest.
89
+ if (ref.layout === "bulk") {
90
+ this.accumulated[type] = ref;
91
+ return;
92
+ }
93
+ // Per-entry: merge entries by key. Duplicate keys replace (last write
94
+ // wins at that key — matches local/gcs overwrite semantics).
95
+ const merged = new Map();
96
+ for (const e of existing.entries ?? [])
97
+ merged.set(e.key, e);
98
+ for (const e of ref.entries ?? [])
99
+ merged.set(e.key, e);
100
+ const entries = Array.from(merged.values());
101
+ this.accumulated[type] = {
102
+ ...existing,
103
+ layout: "per-entry",
104
+ entries,
105
+ entryCount: entries.length,
106
+ bytes: entries.reduce((sum, e) => sum + (e.bytes ?? 0), 0),
107
+ // `store`, `bucket`, `path` stay from the first ref — per-entry
108
+ // paths are descriptor-derived and stable across calls.
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ApiGatewayArtifactWriter — uploads AILF run artifacts via the API Gateway.
3
+ *
4
+ * Used when the CLI runs locally without GCS credentials. The Gateway signs a
5
+ * PUT URL (scoped to a single GCS object) and the writer PUTs JSON directly
6
+ * to GCS so Vercel stays out of the data path.
7
+ *
8
+ * Endpoints:
9
+ * - Bulk: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/upload-url
10
+ * - Per-entry: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/{entryKey}/upload-url
11
+ * - Manifest: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/manifest/upload-url
12
+ *
13
+ * ## W0049 API surface
14
+ *
15
+ * - `emit(type, association, payload)` — canonical single-shot write. Uses
16
+ * the registry to resolve `layout` and the signing endpoint.
17
+ * - `appendNdjson` — NOT IMPLEMENTED. The API Gateway has no batch-signing
18
+ * endpoint yet (W0052), and per-object signing would issue one sign call
19
+ * per row, which blows the Vercel Function budget. Throws
20
+ * `NotImplementedError` so the gap is explicit rather than a silent no-op.
21
+ * - `writeBulk` / `writePerEntry` — @deprecated legacy surface; removal in W0052.
22
+ *
23
+ * Design principles:
24
+ * - P5: Non-blocking — any non-structural failure returns null and warns.
25
+ * - Stateless — no client state between calls.
26
+ *
27
+ * @see docs/decisions/D0032-run-anchored-artifact-store.md
28
+ * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
29
+ */
30
+ import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
31
+ export interface ApiGatewayArtifactWriterOptions {
32
+ /** Base URL of the API gateway (e.g., "https://ailf-api.sanity.build"). */
33
+ apiBaseUrl: string;
34
+ /** AILF API key with the `artifact:write` scope. */
35
+ apiKey: string;
36
+ /** GCS bucket name — included in the returned ArtifactRef. */
37
+ bucket: string;
38
+ }
39
+ export declare class ApiGatewayArtifactWriter implements ArtifactWriter {
40
+ private readonly options;
41
+ constructor(options: ApiGatewayArtifactWriterOptions);
42
+ emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
43
+ appendNdjson(): Promise<ArtifactRef | null>;
44
+ writeManifest(runId: RunId, manifest: RunManifest): Promise<ArtifactRef | null>;
45
+ /** @deprecated Use `emit()` instead. */
46
+ writeBulk(type: ArtifactType, runId: RunId, data: unknown): Promise<ArtifactRef | null>;
47
+ /** @deprecated Use `emit()` per entry instead. */
48
+ writePerEntry(type: ArtifactType, runId: RunId, entries: readonly ArtifactEntry[]): Promise<ArtifactRef | null>;
49
+ private putJson;
50
+ private putJsonRaw;
51
+ private fetchSignedUrl;
52
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * ApiGatewayArtifactWriter — uploads AILF run artifacts via the API Gateway.
3
+ *
4
+ * Used when the CLI runs locally without GCS credentials. The Gateway signs a
5
+ * PUT URL (scoped to a single GCS object) and the writer PUTs JSON directly
6
+ * to GCS so Vercel stays out of the data path.
7
+ *
8
+ * Endpoints:
9
+ * - Bulk: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/upload-url
10
+ * - Per-entry: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/{entryKey}/upload-url
11
+ * - Manifest: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/manifest/upload-url
12
+ *
13
+ * ## W0049 API surface
14
+ *
15
+ * - `emit(type, association, payload)` — canonical single-shot write. Uses
16
+ * the registry to resolve `layout` and the signing endpoint.
17
+ * - `appendNdjson` — NOT IMPLEMENTED. The API Gateway has no batch-signing
18
+ * endpoint yet (W0052), and per-object signing would issue one sign call
19
+ * per row, which blows the Vercel Function budget. Throws
20
+ * `NotImplementedError` so the gap is explicit rather than a silent no-op.
21
+ * - `writeBulk` / `writePerEntry` — @deprecated legacy surface; removal in W0052.
22
+ *
23
+ * Design principles:
24
+ * - P5: Non-blocking — any non-structural failure returns null and warns.
25
+ * - Stateless — no client state between calls.
26
+ *
27
+ * @see docs/decisions/D0032-run-anchored-artifact-store.md
28
+ * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
29
+ */
30
+ import { ARTIFACT_REGISTRY, NotImplementedError, } from "../_vendor/ailf-core/index.js";
31
+ export class ApiGatewayArtifactWriter {
32
+ options;
33
+ constructor(options) {
34
+ this.options = options;
35
+ }
36
+ // ---- Canonical W0049 API ------------------------------------------------
37
+ async emit(type, association, payload) {
38
+ const descriptor = ARTIFACT_REGISTRY[type];
39
+ const runId = association.run;
40
+ if (!runId) {
41
+ console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
42
+ return null;
43
+ }
44
+ if (descriptor.layout === "bulk") {
45
+ const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/upload-url`;
46
+ return this.putJson(uploadUrlPath, payload, {
47
+ layout: "bulk",
48
+ entryCount: entryCountOf(payload),
49
+ });
50
+ }
51
+ // per-entry
52
+ const entryKey = descriptor.formatEntryKey(association);
53
+ const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/${encodeURIComponent(entryKey)}/upload-url`;
54
+ const result = await this.putJsonRaw(uploadUrlPath, payload);
55
+ if (!result)
56
+ return null;
57
+ return {
58
+ store: "gcs",
59
+ bucket: result.bucket,
60
+ path: `runs/${runId}/${descriptor.slug}`,
61
+ bytes: result.bytes,
62
+ entryCount: 1,
63
+ layout: "per-entry",
64
+ entries: [{ key: entryKey, bytes: result.bytes, association }],
65
+ };
66
+ }
67
+ async appendNdjson() {
68
+ throw new NotImplementedError("ApiGatewayArtifactWriter.appendNdjson is not supported. " +
69
+ "NDJSON streaming for traces requires the batch signing endpoint " +
70
+ "(W0052). Producers should use GcsArtifactWriter directly when " +
71
+ "running locally, or defer traces emission until the gateway lands " +
72
+ "the batch route.");
73
+ }
74
+ async writeManifest(runId, manifest) {
75
+ const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/manifest/upload-url`;
76
+ return this.putJson(uploadUrlPath, manifest, { layout: "bulk" });
77
+ }
78
+ // ---- Deprecated legacy surface (W0052) ----------------------------------
79
+ /** @deprecated Use `emit()` instead. */
80
+ async writeBulk(type, runId, data) {
81
+ const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/upload-url`;
82
+ return this.putJson(uploadUrlPath, data, {
83
+ layout: "bulk",
84
+ entryCount: entryCountOf(data),
85
+ });
86
+ }
87
+ /** @deprecated Use `emit()` per entry instead. */
88
+ async writePerEntry(type, runId, entries) {
89
+ const descriptor = ARTIFACT_REGISTRY[type];
90
+ if (!descriptor.parseEntryKey) {
91
+ console.warn(` ⚠️ writePerEntry called for "${type}" but the registry has no parseEntryKey`);
92
+ return null;
93
+ }
94
+ const uploaded = [];
95
+ let totalBytes = 0;
96
+ let bucket = this.options.bucket;
97
+ for (const entry of entries) {
98
+ const parsed = descriptor.parseEntryKey(entry.key);
99
+ if (!parsed.ok) {
100
+ console.warn(` ⚠️ Skipping entry with invalid key "${entry.key}": ${parsed.reason}`);
101
+ continue;
102
+ }
103
+ const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/${encodeURIComponent(entry.key)}/upload-url`;
104
+ const result = await this.putJsonRaw(uploadUrlPath, entry.data);
105
+ if (!result)
106
+ continue;
107
+ bucket = result.bucket;
108
+ uploaded.push({ key: entry.key, bytes: result.bytes });
109
+ totalBytes += result.bytes;
110
+ }
111
+ if (uploaded.length === 0)
112
+ return null;
113
+ return {
114
+ store: "gcs",
115
+ bucket,
116
+ path: `runs/${runId}/${descriptor.slug}`,
117
+ bytes: totalBytes,
118
+ entryCount: uploaded.length,
119
+ layout: "per-entry",
120
+ entries: uploaded,
121
+ };
122
+ }
123
+ // ---- Internals ----------------------------------------------------------
124
+ async putJson(uploadUrlPath, data, meta) {
125
+ const result = await this.putJsonRaw(uploadUrlPath, data);
126
+ if (!result)
127
+ return null;
128
+ return {
129
+ store: "gcs",
130
+ bucket: result.bucket,
131
+ path: result.path,
132
+ bytes: result.bytes,
133
+ entryCount: meta.entryCount,
134
+ layout: meta.layout,
135
+ };
136
+ }
137
+ async putJsonRaw(uploadUrlPath, data) {
138
+ const json = JSON.stringify(data);
139
+ const bytes = Buffer.byteLength(json, "utf-8");
140
+ try {
141
+ const signed = await this.fetchSignedUrl(uploadUrlPath);
142
+ if (!signed)
143
+ return null;
144
+ const putRes = await fetch(signed.url, {
145
+ body: json,
146
+ headers: signed.requiredHeaders,
147
+ method: "PUT",
148
+ });
149
+ if (!putRes.ok) {
150
+ console.warn(` ⚠️ Artifact upload failed (non-blocking): ${signed.path} — GCS PUT ${putRes.status} ${putRes.statusText}`);
151
+ return null;
152
+ }
153
+ return { bucket: signed.bucket, path: signed.path, bytes };
154
+ }
155
+ catch (err) {
156
+ const message = err instanceof Error ? err.message : String(err);
157
+ console.warn(` ⚠️ Artifact upload failed (non-blocking): ${uploadUrlPath} — ${message}`);
158
+ return null;
159
+ }
160
+ }
161
+ async fetchSignedUrl(uploadUrlPath) {
162
+ const url = `${this.options.apiBaseUrl.replace(/\/$/, "")}${uploadUrlPath}`;
163
+ const res = await fetch(url, {
164
+ headers: { Authorization: `Bearer ${this.options.apiKey}` },
165
+ method: "GET",
166
+ });
167
+ if (!res.ok) {
168
+ console.warn(` ⚠️ Signed-URL request failed: ${res.status} ${res.statusText}`);
169
+ return null;
170
+ }
171
+ const body = (await res.json());
172
+ if (body.object !== "signed_upload_url" ||
173
+ typeof body.url !== "string" ||
174
+ typeof body.path !== "string" ||
175
+ typeof body.bucket !== "string" ||
176
+ !body.requiredHeaders) {
177
+ console.warn(` ⚠️ Signed-URL response was malformed`);
178
+ return null;
179
+ }
180
+ return {
181
+ bucket: body.bucket,
182
+ method: "PUT",
183
+ object: "signed_upload_url",
184
+ path: body.path,
185
+ requiredHeaders: body.requiredHeaders,
186
+ url: body.url,
187
+ };
188
+ }
189
+ }
190
+ function entryCountOf(data) {
191
+ if (typeof data === "object" &&
192
+ data !== null &&
193
+ "entries" in data &&
194
+ typeof data.entries === "object") {
195
+ return Object.keys(data.entries)
196
+ .length;
197
+ }
198
+ return undefined;
199
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * emitFileContents() — reads a file from disk and hands its contents to
3
+ * the writer's `emit()`. Shim for the legacy `captureFile(path)` pattern
4
+ * now that the port takes in-memory payloads.
5
+ *
6
+ * Covers the ~13 producer sites that write a file for user-facing output
7
+ * (e.g. promptfoo writes YAML configs and JSON results) and then need to
8
+ * also capture them as artifacts. Reading at emit-time is uniform, keeps
9
+ * the port narrow, and costs nothing at the sizes in play (all descriptors
10
+ * cap ≤10 MB; most are ≤256 KB).
11
+ *
12
+ * Failures are non-blocking per P5 — a missing file or unparseable JSON
13
+ * returns null + warns rather than throwing, so the pipeline keeps moving
14
+ * even if the user-facing file wasn't produced.
15
+ *
16
+ * See `tasks/plan.md § Q2` for the design rationale.
17
+ */
18
+ import { type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues } from "../_vendor/ailf-core/index.d.ts";
19
+ /**
20
+ * Read a file from disk, parse it per the descriptor's mime, and emit it.
21
+ *
22
+ * - JSON mime: file contents are `JSON.parse`d into an object before `emit()`.
23
+ * - Markdown / YAML mime: file contents are passed to `emit()` as a string.
24
+ * - NDJSON: not supported by `emit()` — use `appendNdjson()` directly instead.
25
+ *
26
+ * Returns null (with a warn) on any error. Never throws.
27
+ */
28
+ export declare function emitFileContents<T extends ArtifactType>(writer: ArtifactWriter, type: T, association: AssociationValues, filePath: string): Promise<ArtifactRef | null>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * emitFileContents() — reads a file from disk and hands its contents to
3
+ * the writer's `emit()`. Shim for the legacy `captureFile(path)` pattern
4
+ * now that the port takes in-memory payloads.
5
+ *
6
+ * Covers the ~13 producer sites that write a file for user-facing output
7
+ * (e.g. promptfoo writes YAML configs and JSON results) and then need to
8
+ * also capture them as artifacts. Reading at emit-time is uniform, keeps
9
+ * the port narrow, and costs nothing at the sizes in play (all descriptors
10
+ * cap ≤10 MB; most are ≤256 KB).
11
+ *
12
+ * Failures are non-blocking per P5 — a missing file or unparseable JSON
13
+ * returns null + warns rather than throwing, so the pipeline keeps moving
14
+ * even if the user-facing file wasn't produced.
15
+ *
16
+ * See `tasks/plan.md § Q2` for the design rationale.
17
+ */
18
+ import { promises as fs } from "node:fs";
19
+ import { ARTIFACT_REGISTRY, } from "../_vendor/ailf-core/index.js";
20
+ /**
21
+ * Read a file from disk, parse it per the descriptor's mime, and emit it.
22
+ *
23
+ * - JSON mime: file contents are `JSON.parse`d into an object before `emit()`.
24
+ * - Markdown / YAML mime: file contents are passed to `emit()` as a string.
25
+ * - NDJSON: not supported by `emit()` — use `appendNdjson()` directly instead.
26
+ *
27
+ * Returns null (with a warn) on any error. Never throws.
28
+ */
29
+ export async function emitFileContents(writer, type, association, filePath) {
30
+ const descriptor = ARTIFACT_REGISTRY[type];
31
+ if (descriptor.mime === "application/x-ndjson") {
32
+ console.warn(` ⚠️ emitFileContents("${type}", ${filePath}): NDJSON types require appendNdjson(), not emit() — skipping`);
33
+ return null;
34
+ }
35
+ let contents;
36
+ try {
37
+ contents = await fs.readFile(filePath, "utf-8");
38
+ }
39
+ catch (err) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ console.warn(` ⚠️ emitFileContents read failed (non-blocking): ${filePath} — ${message}`);
42
+ return null;
43
+ }
44
+ let payload = contents;
45
+ if (descriptor.mime === "application/json") {
46
+ try {
47
+ payload = JSON.parse(contents);
48
+ }
49
+ catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ console.warn(` ⚠️ emitFileContents parse failed (non-blocking): ${filePath} — ${message}`);
52
+ return null;
53
+ }
54
+ }
55
+ return writer.emit(type, association, payload);
56
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * FanoutArtifactWriter — layers multiple writers so each `emit()` fans out
3
+ * to every configured backend.
4
+ *
5
+ * D0033 M4 default wiring:
6
+ * `FanoutArtifactWriter([ LocalFilesystemArtifactWriter, GcsArtifactWriter ])`
7
+ *
8
+ * Semantics:
9
+ * - Fan out in declaration order. Every writer runs, even if earlier ones fail.
10
+ * - Return the **first non-null ArtifactRef**. Local is listed first, so a
11
+ * local success + GCS failure still produces a non-null ref pointing at
12
+ * local — the pipeline succeeds and Studio retrieval works against the
13
+ * local tree with a warning logged for the GCS leg.
14
+ * - Failures on individual writers warn (via their own P5 paths) but do
15
+ * not propagate. The fanout never throws.
16
+ *
17
+ * This writer is a composition primitive — it adds no I/O of its own.
18
+ * Tests verify the fanout semantics against `NoOpArtifactWriter` instances
19
+ * plus a recording test double.
20
+ *
21
+ * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M4)
22
+ */
23
+ import type { ArtifactEntry, ArtifactRef, ArtifactType, ArtifactWriter, AssociationValues, RunId, RunManifest } from "../_vendor/ailf-core/index.d.ts";
24
+ export declare class FanoutArtifactWriter implements ArtifactWriter {
25
+ private readonly writers;
26
+ constructor(writers: readonly ArtifactWriter[]);
27
+ emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
28
+ appendNdjson<T extends ArtifactType>(type: T, association: AssociationValues, rows: readonly unknown[]): Promise<ArtifactRef | null>;
29
+ writeManifest(runId: RunId, manifest: RunManifest): Promise<ArtifactRef | null>;
30
+ /** @deprecated Use `emit()` instead. */
31
+ writeBulk(type: ArtifactType, runId: RunId, data: unknown): Promise<ArtifactRef | null>;
32
+ /** @deprecated Use `emit()` per entry instead. */
33
+ writePerEntry(type: ArtifactType, runId: RunId, entries: readonly ArtifactEntry[]): Promise<ArtifactRef | null>;
34
+ /**
35
+ * Run `op` against every delegate writer. Any individual writer's rejected
36
+ * promise is caught and logged; the fanout itself never rejects.
37
+ */
38
+ private runAll;
39
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * FanoutArtifactWriter — layers multiple writers so each `emit()` fans out
3
+ * to every configured backend.
4
+ *
5
+ * D0033 M4 default wiring:
6
+ * `FanoutArtifactWriter([ LocalFilesystemArtifactWriter, GcsArtifactWriter ])`
7
+ *
8
+ * Semantics:
9
+ * - Fan out in declaration order. Every writer runs, even if earlier ones fail.
10
+ * - Return the **first non-null ArtifactRef**. Local is listed first, so a
11
+ * local success + GCS failure still produces a non-null ref pointing at
12
+ * local — the pipeline succeeds and Studio retrieval works against the
13
+ * local tree with a warning logged for the GCS leg.
14
+ * - Failures on individual writers warn (via their own P5 paths) but do
15
+ * not propagate. The fanout never throws.
16
+ *
17
+ * This writer is a composition primitive — it adds no I/O of its own.
18
+ * Tests verify the fanout semantics against `NoOpArtifactWriter` instances
19
+ * plus a recording test double.
20
+ *
21
+ * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M4)
22
+ */
23
+ export class FanoutArtifactWriter {
24
+ writers;
25
+ constructor(writers) {
26
+ if (writers.length === 0) {
27
+ throw new Error("FanoutArtifactWriter requires at least one delegate writer");
28
+ }
29
+ this.writers = writers;
30
+ }
31
+ async emit(type, association, payload) {
32
+ const refs = await this.runAll((w) => w.emit(type, association, payload));
33
+ return firstNonNull(refs);
34
+ }
35
+ async appendNdjson(type, association, rows) {
36
+ const refs = await this.runAll((w) => w.appendNdjson(type, association, rows));
37
+ return firstNonNull(refs);
38
+ }
39
+ async writeManifest(runId, manifest) {
40
+ const refs = await this.runAll((w) => w.writeManifest(runId, manifest));
41
+ return firstNonNull(refs);
42
+ }
43
+ /** @deprecated Use `emit()` instead. */
44
+ async writeBulk(type, runId, data) {
45
+ const refs = await this.runAll((w) => w.writeBulk(type, runId, data));
46
+ return firstNonNull(refs);
47
+ }
48
+ /** @deprecated Use `emit()` per entry instead. */
49
+ async writePerEntry(type, runId, entries) {
50
+ const refs = await this.runAll((w) => w.writePerEntry(type, runId, entries));
51
+ return firstNonNull(refs);
52
+ }
53
+ /**
54
+ * Run `op` against every delegate writer. Any individual writer's rejected
55
+ * promise is caught and logged; the fanout itself never rejects.
56
+ */
57
+ async runAll(op) {
58
+ return Promise.all(this.writers.map(async (writer) => {
59
+ try {
60
+ return await op(writer);
61
+ }
62
+ catch (err) {
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ console.warn(` ⚠️ Fanout delegate threw (non-blocking): ${writer.constructor.name} — ${message}`);
65
+ return null;
66
+ }
67
+ }));
68
+ }
69
+ }
70
+ function firstNonNull(refs) {
71
+ for (const ref of refs) {
72
+ if (ref !== null)
73
+ return ref;
74
+ }
75
+ return null;
76
+ }
@@ -1,14 +1,32 @@
1
1
  /**
2
- * FilesystemArtifactCollector — writes captured artifacts to a local directory.
2
+ * FilesystemArtifactCollector — DEPRECATED legacy capture path.
3
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.
4
+ * W0050 Slice 5: the unified artifact writer (`ctx.artifactWriter.emit()`)
5
+ * owns artifact production starting this release. This class is retained
6
+ * for one release cycle so producer steps that haven't migrated yet still
7
+ * have a surface to call against; W0052 deletes the class + port.
8
+ *
9
+ * Behavioral changes vs. pre-W0050:
10
+ *
11
+ * - **No tarball.** The `compress` option is ignored; flush() always writes
12
+ * a loose directory tree. The legacy `.tar.gz` artifact archive is no
13
+ * longer produced (D0033 AC8 — "no new capture tarballs").
14
+ * - **First-call deprecation warning.** The first invocation of capture()
15
+ * or captureFile() on any instance emits a one-time-per-process warning
16
+ * pointing callers at `ctx.artifactWriter.emit()`.
17
+ *
18
+ * Everything else (in-memory accumulation, redaction, loose-file output at
19
+ * flush time) is unchanged — existing tests continue to pass. This is the
20
+ * **minimum** pass-through posture. Full delegation to
21
+ * `LocalFilesystemArtifactWriter` is W0052 scope, at which point the port
22
+ * and this class are both deleted.
7
23
  *
8
24
  * Design principles:
9
25
  * - capture() and captureFile() are synchronous (no I/O during step execution)
10
26
  * - flush() does all I/O at pipeline end
11
27
  * - Failures in capture/captureFile are swallowed (P5: non-blocking)
28
+ *
29
+ * @deprecated Use `ctx.artifactWriter.emit()` instead; removal scheduled for W0052.
12
30
  */
13
31
  import type { ArtifactCollector, CaptureFlushResult } from "../_vendor/ailf-core/index.d.ts";
14
32
  export interface FilesystemCollectorOptions {