@sanity/ailf 3.0.0 → 3.1.1
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 +37 -0
- package/dist/_vendor/ailf-core/artifact-capture/association.js +19 -0
- package/dist/_vendor/ailf-core/index.d.ts +1 -1
- package/dist/_vendor/ailf-core/index.js +1 -1
- package/dist/_vendor/ailf-core/ports/context.d.ts +8 -0
- package/dist/_vendor/ailf-core/ports/index.d.ts +2 -0
- package/dist/_vendor/ailf-core/ports/index.js +1 -0
- package/dist/_vendor/ailf-core/ports/progress-reporter.d.ts +74 -0
- package/dist/_vendor/ailf-core/ports/progress-reporter.js +26 -0
- package/dist/_vendor/ailf-core/services/slim-report-summary.js +1 -16
- package/dist/adapters/progress/console-progress-reporter.d.ts +35 -0
- package/dist/adapters/progress/console-progress-reporter.js +110 -0
- package/dist/artifact-capture/api-gateway-artifact-writer.d.ts +8 -1
- package/dist/artifact-capture/api-gateway-artifact-writer.js +79 -42
- package/dist/artifact-capture/batching-api-gateway-artifact-writer.d.ts +108 -0
- package/dist/artifact-capture/batching-api-gateway-artifact-writer.js +492 -0
- package/dist/artifact-capture/fanout-artifact-writer.d.ts +14 -2
- package/dist/artifact-capture/fanout-artifact-writer.js +25 -4
- package/dist/artifact-capture/gcs-artifact-writer.d.ts +27 -1
- package/dist/artifact-capture/gcs-artifact-writer.js +168 -38
- package/dist/artifact-capture/instrumented-artifact-writer.d.ts +32 -0
- package/dist/artifact-capture/instrumented-artifact-writer.js +151 -0
- package/dist/artifact-capture/local-fs-artifact-writer.d.ts +8 -1
- package/dist/artifact-capture/local-fs-artifact-writer.js +23 -4
- package/dist/artifact-capture/parallel-emit.d.ts +43 -0
- package/dist/artifact-capture/parallel-emit.js +84 -0
- package/dist/artifact-capture/upload-metrics.d.ts +62 -0
- package/dist/artifact-capture/upload-metrics.js +125 -0
- package/dist/composition-root.d.ts +2 -2
- package/dist/composition-root.js +97 -11
- package/dist/orchestration/pipeline-orchestrator.js +97 -1
- package/dist/orchestration/steps/calculate-scores-step.js +9 -7
- package/dist/orchestration/steps/finalize-run-step.js +40 -8
- package/dist/pipeline/emit-eval-results.js +29 -11
- package/dist/pipeline/upload-test-outputs.d.ts +12 -5
- package/dist/pipeline/upload-test-outputs.js +27 -10
- package/package.json +1 -1
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
|
|
29
29
|
*/
|
|
30
30
|
import { Storage } from "@google-cloud/storage";
|
|
31
|
-
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
31
|
+
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
32
|
+
import { type UploadMetricsSink } from "./upload-metrics.js";
|
|
32
33
|
export interface GcsArtifactWriterOptions {
|
|
33
34
|
/** GCS bucket name (e.g., "ailf-artifacts") */
|
|
34
35
|
bucket: string;
|
|
@@ -38,12 +39,37 @@ export interface GcsArtifactWriterOptions {
|
|
|
38
39
|
* supplies a fake here to avoid real network calls.
|
|
39
40
|
*/
|
|
40
41
|
storage?: Storage;
|
|
42
|
+
/**
|
|
43
|
+
* Optional progress reporter + phaseId (W0053). When set, the writer
|
|
44
|
+
* publishes `phaseProgress` on every successful emit / appendNdjson /
|
|
45
|
+
* writeManifest so the CLI can render per-batch updates during the
|
|
46
|
+
* long artifact upload phase.
|
|
47
|
+
*/
|
|
48
|
+
progress?: ArtifactWriterProgressOptions;
|
|
49
|
+
/**
|
|
50
|
+
* W0056 — optional metrics sink that receives per-phase timing events.
|
|
51
|
+
* Defaults to a no-op so the hot path stays free when metrics are off.
|
|
52
|
+
*/
|
|
53
|
+
metrics?: UploadMetricsSink;
|
|
41
54
|
}
|
|
42
55
|
export declare class GcsArtifactWriter implements ArtifactWriter {
|
|
43
56
|
private client;
|
|
44
57
|
private readonly options;
|
|
45
58
|
private readonly ndjsonStreams;
|
|
59
|
+
private readonly metrics;
|
|
60
|
+
/**
|
|
61
|
+
* Bounds concurrent GCS PUTs.
|
|
62
|
+
*
|
|
63
|
+
* Producers after W0058 fire all emits synchronously and `Promise.all`
|
|
64
|
+
* once at the end — without an internal limiter, a full-literacy run
|
|
65
|
+
* would fan ~1,500 concurrent `file.save` calls, saturating the client
|
|
66
|
+
* connection pool and GCS's per-prefix write quota. The limiter
|
|
67
|
+
* preserves the pre-W0058 effective concurrency (8) that Prototype A
|
|
68
|
+
* measured safe, without forcing the producer to own that knob.
|
|
69
|
+
*/
|
|
70
|
+
private readonly limiter;
|
|
46
71
|
constructor(options: GcsArtifactWriterOptions);
|
|
72
|
+
private reportProgress;
|
|
47
73
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
48
74
|
appendNdjson<T extends ArtifactType>(type: T, association: AssociationValues, rows: readonly unknown[]): Promise<ArtifactRef | null>;
|
|
49
75
|
writeManifest(runId: RunId, manifest: RunManifest): Promise<ArtifactRef | null>;
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { Storage } from "@google-cloud/storage";
|
|
31
31
|
import { ARTIFACT_REGISTRY, buildManifestPreview, } from "../_vendor/ailf-core/index.js";
|
|
32
|
+
import { resolveUploadConcurrency } from "./parallel-emit.js";
|
|
32
33
|
import { redactArtifactData } from "./redact-artifact.js";
|
|
34
|
+
import { NO_OP_UPLOAD_METRICS, } from "./upload-metrics.js";
|
|
33
35
|
/**
|
|
34
36
|
* GCS's object compose operation accepts at most 32 source objects per call
|
|
35
37
|
* ([docs](https://cloud.google.com/storage/docs/composite-objects)). When an
|
|
@@ -42,11 +44,36 @@ export class GcsArtifactWriter {
|
|
|
42
44
|
client = null;
|
|
43
45
|
options;
|
|
44
46
|
ndjsonStreams = new Map();
|
|
47
|
+
metrics;
|
|
48
|
+
/**
|
|
49
|
+
* Bounds concurrent GCS PUTs.
|
|
50
|
+
*
|
|
51
|
+
* Producers after W0058 fire all emits synchronously and `Promise.all`
|
|
52
|
+
* once at the end — without an internal limiter, a full-literacy run
|
|
53
|
+
* would fan ~1,500 concurrent `file.save` calls, saturating the client
|
|
54
|
+
* connection pool and GCS's per-prefix write quota. The limiter
|
|
55
|
+
* preserves the pre-W0058 effective concurrency (8) that Prototype A
|
|
56
|
+
* measured safe, without forcing the producer to own that knob.
|
|
57
|
+
*/
|
|
58
|
+
limiter;
|
|
45
59
|
constructor(options) {
|
|
46
60
|
this.options = options;
|
|
61
|
+
this.metrics = options.metrics ?? NO_OP_UPLOAD_METRICS;
|
|
47
62
|
if (options.storage) {
|
|
48
63
|
this.client = options.storage;
|
|
49
64
|
}
|
|
65
|
+
this.limiter = new ConcurrencyLimiter(resolveUploadConcurrency());
|
|
66
|
+
}
|
|
67
|
+
reportProgress(ref) {
|
|
68
|
+
const progress = this.options.progress;
|
|
69
|
+
if (!progress)
|
|
70
|
+
return;
|
|
71
|
+
progress.reporter.phaseProgress({
|
|
72
|
+
phaseId: progress.phaseId,
|
|
73
|
+
items: 1,
|
|
74
|
+
bytes: ref.bytes,
|
|
75
|
+
label: ref.path,
|
|
76
|
+
});
|
|
50
77
|
}
|
|
51
78
|
// ---- Canonical W0049 API ------------------------------------------------
|
|
52
79
|
async emit(type, association, payload) {
|
|
@@ -68,18 +95,21 @@ export class GcsArtifactWriter {
|
|
|
68
95
|
layout: "bulk",
|
|
69
96
|
mime: descriptor.mime,
|
|
70
97
|
entryCount: entryCountOf(redacted),
|
|
98
|
+
type,
|
|
71
99
|
});
|
|
72
100
|
if (!ref)
|
|
73
101
|
return null;
|
|
74
|
-
|
|
102
|
+
const finalRef = preview === undefined ? ref : { ...ref, preview };
|
|
103
|
+
this.reportProgress(finalRef);
|
|
104
|
+
return finalRef;
|
|
75
105
|
}
|
|
76
106
|
// per-entry
|
|
77
107
|
const entryKey = descriptor.formatEntryKey(association);
|
|
78
108
|
const path = descriptor.objectPath(runId, entryKey);
|
|
79
|
-
const ref = await this.putBody(path, serializeForMime(redacted, descriptor.mime), { layout: "per-entry", mime: descriptor.mime });
|
|
109
|
+
const ref = await this.putBody(path, serializeForMime(redacted, descriptor.mime), { layout: "per-entry", mime: descriptor.mime, type });
|
|
80
110
|
if (!ref)
|
|
81
111
|
return null;
|
|
82
|
-
|
|
112
|
+
const finalRef = {
|
|
83
113
|
...ref,
|
|
84
114
|
path: `runs/${runId}/${descriptor.slug}`,
|
|
85
115
|
entryCount: 1,
|
|
@@ -92,6 +122,8 @@ export class GcsArtifactWriter {
|
|
|
92
122
|
},
|
|
93
123
|
],
|
|
94
124
|
};
|
|
125
|
+
this.reportProgress(finalRef);
|
|
126
|
+
return finalRef;
|
|
95
127
|
}
|
|
96
128
|
async appendNdjson(type, association, rows) {
|
|
97
129
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
@@ -119,21 +151,39 @@ export class GcsArtifactWriter {
|
|
|
119
151
|
const redactedRows = rows.map((r) => redactArtifactData(r));
|
|
120
152
|
const body = redactedRows.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
121
153
|
const bytes = Buffer.byteLength(body, "utf-8");
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
const saveOutcome = await this.limiter.run(async () => {
|
|
155
|
+
const start = Date.now();
|
|
156
|
+
let success = false;
|
|
157
|
+
try {
|
|
158
|
+
const storage = this.getClient();
|
|
159
|
+
await storage
|
|
160
|
+
.bucket(this.options.bucket)
|
|
161
|
+
.file(partPath)
|
|
162
|
+
.save(body, { contentType: "application/x-ndjson" });
|
|
163
|
+
success = true;
|
|
164
|
+
return { ok: true };
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
168
|
+
console.warn(` ⚠️ NDJSON part upload failed (non-blocking): ${partPath} — ${message}`);
|
|
169
|
+
return { ok: false };
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
this.metrics.record({
|
|
173
|
+
phase: "put",
|
|
174
|
+
writer: "GcsArtifactWriter",
|
|
175
|
+
type: `${type}:ndjson-part`,
|
|
176
|
+
ms: Date.now() - start,
|
|
177
|
+
bytes,
|
|
178
|
+
success,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (!saveOutcome.ok)
|
|
134
183
|
return null;
|
|
135
|
-
|
|
136
|
-
|
|
184
|
+
state.partCount++;
|
|
185
|
+
state.totalBytes += bytes;
|
|
186
|
+
const ref = {
|
|
137
187
|
store: "gcs",
|
|
138
188
|
bucket: this.options.bucket,
|
|
139
189
|
path: `runs/${runId}/${descriptor.slug}`,
|
|
@@ -148,14 +198,20 @@ export class GcsArtifactWriter {
|
|
|
148
198
|
},
|
|
149
199
|
],
|
|
150
200
|
};
|
|
201
|
+
this.reportProgress(ref);
|
|
202
|
+
return ref;
|
|
151
203
|
}
|
|
152
204
|
async writeManifest(runId, manifest) {
|
|
153
205
|
await this.finalizeNdjsonStreams();
|
|
154
206
|
const path = `runs/${runId}/manifest.json`;
|
|
155
|
-
|
|
207
|
+
const ref = await this.putBody(path, JSON.stringify(manifest), {
|
|
156
208
|
layout: "bulk",
|
|
157
209
|
mime: "application/json",
|
|
210
|
+
type: "manifest",
|
|
158
211
|
});
|
|
212
|
+
if (ref)
|
|
213
|
+
this.reportProgress(ref);
|
|
214
|
+
return ref;
|
|
159
215
|
}
|
|
160
216
|
// ---- Deprecated legacy surface (W0052) ----------------------------------
|
|
161
217
|
/** @deprecated Use `emit()` instead. Routes through the same GCS I/O. */
|
|
@@ -171,6 +227,7 @@ export class GcsArtifactWriter {
|
|
|
171
227
|
layout: "bulk",
|
|
172
228
|
mime: descriptor.mime,
|
|
173
229
|
entryCount: entryCountOf(redacted),
|
|
230
|
+
type,
|
|
174
231
|
});
|
|
175
232
|
}
|
|
176
233
|
/** @deprecated Use `emit()` per entry instead. */
|
|
@@ -197,6 +254,8 @@ export class GcsArtifactWriter {
|
|
|
197
254
|
const redacted = redactArtifactData(entry.data);
|
|
198
255
|
const body = serializeForMime(redacted, descriptor.mime);
|
|
199
256
|
const bytes = Buffer.byteLength(body, "utf-8");
|
|
257
|
+
const start = Date.now();
|
|
258
|
+
let success = false;
|
|
200
259
|
try {
|
|
201
260
|
await storage
|
|
202
261
|
.bucket(this.options.bucket)
|
|
@@ -204,11 +263,22 @@ export class GcsArtifactWriter {
|
|
|
204
263
|
.save(body, { contentType: descriptor.mime });
|
|
205
264
|
uploaded.push({ key: entry.key, bytes });
|
|
206
265
|
totalBytes += bytes;
|
|
266
|
+
success = true;
|
|
207
267
|
}
|
|
208
268
|
catch (err) {
|
|
209
269
|
const message = err instanceof Error ? err.message : String(err);
|
|
210
270
|
console.warn(` ⚠️ Artifact entry upload failed (non-blocking): ${path} — ${message}`);
|
|
211
271
|
}
|
|
272
|
+
finally {
|
|
273
|
+
this.metrics.record({
|
|
274
|
+
phase: "put",
|
|
275
|
+
writer: "GcsArtifactWriter",
|
|
276
|
+
type,
|
|
277
|
+
ms: Date.now() - start,
|
|
278
|
+
bytes,
|
|
279
|
+
success,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
212
282
|
}
|
|
213
283
|
if (uploaded.length === 0)
|
|
214
284
|
return null;
|
|
@@ -240,41 +310,69 @@ export class GcsArtifactWriter {
|
|
|
240
310
|
const storage = this.getClient();
|
|
241
311
|
const bucket = storage.bucket(this.options.bucket);
|
|
242
312
|
const partPaths = Array.from({ length: state.partCount }, (_, i) => `${state.finalPath}.part-${String(i).padStart(4, "0")}`);
|
|
313
|
+
const start = Date.now();
|
|
314
|
+
let success = false;
|
|
243
315
|
try {
|
|
244
316
|
const finalPath = await composeInGroups(bucket, partPaths, state.finalPath);
|
|
245
317
|
if (finalPath !== state.finalPath) {
|
|
246
318
|
console.warn(` ⚠️ NDJSON compose produced "${finalPath}" (expected "${state.finalPath}")`);
|
|
247
319
|
}
|
|
320
|
+
success = true;
|
|
248
321
|
}
|
|
249
322
|
catch (err) {
|
|
250
323
|
const message = err instanceof Error ? err.message : String(err);
|
|
251
324
|
console.warn(` ⚠️ NDJSON compose failed (non-blocking): ${state.finalPath} — ${message}`);
|
|
252
325
|
}
|
|
326
|
+
finally {
|
|
327
|
+
this.metrics.record({
|
|
328
|
+
phase: "compose",
|
|
329
|
+
writer: "GcsArtifactWriter",
|
|
330
|
+
type: `parts=${state.partCount}`,
|
|
331
|
+
ms: Date.now() - start,
|
|
332
|
+
bytes: state.totalBytes,
|
|
333
|
+
success,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
253
336
|
}
|
|
254
337
|
this.ndjsonStreams.clear();
|
|
255
338
|
}
|
|
256
339
|
async putBody(path, body, meta) {
|
|
257
340
|
const bytes = Buffer.byteLength(body, "utf-8");
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
341
|
+
return this.limiter.run(async () => {
|
|
342
|
+
const start = Date.now();
|
|
343
|
+
let success = false;
|
|
344
|
+
try {
|
|
345
|
+
const storage = this.getClient();
|
|
346
|
+
await storage
|
|
347
|
+
.bucket(this.options.bucket)
|
|
348
|
+
.file(path)
|
|
349
|
+
.save(body, { contentType: meta.mime });
|
|
350
|
+
success = true;
|
|
351
|
+
return {
|
|
352
|
+
store: "gcs",
|
|
353
|
+
bucket: this.options.bucket,
|
|
354
|
+
path,
|
|
355
|
+
bytes,
|
|
356
|
+
entryCount: meta.entryCount,
|
|
357
|
+
layout: meta.layout,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
362
|
+
console.warn(` ⚠️ Artifact upload failed (non-blocking): ${path} — ${message}`);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
this.metrics.record({
|
|
367
|
+
phase: "put",
|
|
368
|
+
writer: "GcsArtifactWriter",
|
|
369
|
+
type: meta.type,
|
|
370
|
+
ms: Date.now() - start,
|
|
371
|
+
bytes,
|
|
372
|
+
success,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
278
376
|
}
|
|
279
377
|
getClient() {
|
|
280
378
|
if (this.client)
|
|
@@ -286,6 +384,38 @@ export class GcsArtifactWriter {
|
|
|
286
384
|
// ---------------------------------------------------------------------------
|
|
287
385
|
// Helpers
|
|
288
386
|
// ---------------------------------------------------------------------------
|
|
387
|
+
/**
|
|
388
|
+
* Simple bounded-concurrency scheduler. Tasks beyond the limit queue
|
|
389
|
+
* FIFO; each completion wakes exactly one waiter. Stays FIFO-fair because
|
|
390
|
+
* no timer is involved. Used by `GcsArtifactWriter` to cap concurrent
|
|
391
|
+
* GCS I/O after producers stopped self-bounding via `parallelMap`.
|
|
392
|
+
*/
|
|
393
|
+
class ConcurrencyLimiter {
|
|
394
|
+
inflight = 0;
|
|
395
|
+
waiters = [];
|
|
396
|
+
limit;
|
|
397
|
+
constructor(limit) {
|
|
398
|
+
// Clamp at 1 so a misconfigured `resolveUploadConcurrency()` (0 or
|
|
399
|
+
// negative) cannot deadlock — the admission check in `run()` is
|
|
400
|
+
// `inflight >= limit`, so `limit=0` would wedge every caller.
|
|
401
|
+
this.limit = Math.max(1, Math.floor(limit));
|
|
402
|
+
}
|
|
403
|
+
async run(task) {
|
|
404
|
+
if (this.inflight >= this.limit) {
|
|
405
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
406
|
+
}
|
|
407
|
+
this.inflight++;
|
|
408
|
+
try {
|
|
409
|
+
return await task();
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
this.inflight--;
|
|
413
|
+
const next = this.waiters.shift();
|
|
414
|
+
if (next)
|
|
415
|
+
next();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
289
419
|
function serializeForMime(payload, mime) {
|
|
290
420
|
if (mime === "text/markdown" || mime === "application/yaml") {
|
|
291
421
|
if (typeof payload === "string")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InstrumentedArtifactWriter — decorator for W0056 spike instrumentation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps any `ArtifactWriter` and records end-to-end `emit` / `appendNdjson` /
|
|
5
|
+
* `writeManifest` durations into an `UploadMetrics` sink. The writer-internal
|
|
6
|
+
* phase timings (sign vs. PUT, compose groups) are recorded by the concrete
|
|
7
|
+
* writers; this decorator records the caller-observed totals so the summary
|
|
8
|
+
* shows both views.
|
|
9
|
+
*
|
|
10
|
+
* Triggers `UploadMetrics.summarize()` from `writeManifest` (after delegation)
|
|
11
|
+
* — that is the natural end-of-run signal and avoids a separate lifecycle
|
|
12
|
+
* hook in the composition root.
|
|
13
|
+
*/
|
|
14
|
+
import type { ArtifactEntry, ArtifactRef, ArtifactType, ArtifactWriter, AssociationValues, RunId, RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
15
|
+
import type { UploadMetrics } from "./upload-metrics.js";
|
|
16
|
+
export declare class InstrumentedArtifactWriter implements ArtifactWriter {
|
|
17
|
+
/**
|
|
18
|
+
* Exposed so `FinalizeRunStep` can walk decorator chains to reach the
|
|
19
|
+
* underlying `AccumulatingArtifactWriter` when `AILF_UPLOAD_METRICS=1`
|
|
20
|
+
* (which wraps the accumulator in this decorator). Mirrors the
|
|
21
|
+
* analogous field on `AccumulatingArtifactWriter`. Treat as read-only.
|
|
22
|
+
*/
|
|
23
|
+
readonly inner: ArtifactWriter;
|
|
24
|
+
private readonly metrics;
|
|
25
|
+
private readonly writerName;
|
|
26
|
+
constructor(inner: ArtifactWriter, metrics: UploadMetrics);
|
|
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
|
+
writeBulk(type: ArtifactType, runId: RunId, data: unknown): Promise<ArtifactRef | null>;
|
|
31
|
+
writePerEntry(type: ArtifactType, runId: RunId, entries: readonly ArtifactEntry[]): Promise<ArtifactRef | null>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InstrumentedArtifactWriter — decorator for W0056 spike instrumentation.
|
|
3
|
+
*
|
|
4
|
+
* Wraps any `ArtifactWriter` and records end-to-end `emit` / `appendNdjson` /
|
|
5
|
+
* `writeManifest` durations into an `UploadMetrics` sink. The writer-internal
|
|
6
|
+
* phase timings (sign vs. PUT, compose groups) are recorded by the concrete
|
|
7
|
+
* writers; this decorator records the caller-observed totals so the summary
|
|
8
|
+
* shows both views.
|
|
9
|
+
*
|
|
10
|
+
* Triggers `UploadMetrics.summarize()` from `writeManifest` (after delegation)
|
|
11
|
+
* — that is the natural end-of-run signal and avoids a separate lifecycle
|
|
12
|
+
* hook in the composition root.
|
|
13
|
+
*/
|
|
14
|
+
export class InstrumentedArtifactWriter {
|
|
15
|
+
/**
|
|
16
|
+
* Exposed so `FinalizeRunStep` can walk decorator chains to reach the
|
|
17
|
+
* underlying `AccumulatingArtifactWriter` when `AILF_UPLOAD_METRICS=1`
|
|
18
|
+
* (which wraps the accumulator in this decorator). Mirrors the
|
|
19
|
+
* analogous field on `AccumulatingArtifactWriter`. Treat as read-only.
|
|
20
|
+
*/
|
|
21
|
+
inner;
|
|
22
|
+
metrics;
|
|
23
|
+
writerName;
|
|
24
|
+
constructor(inner, metrics) {
|
|
25
|
+
this.inner = inner;
|
|
26
|
+
this.metrics = metrics;
|
|
27
|
+
this.writerName = inner.constructor.name;
|
|
28
|
+
}
|
|
29
|
+
async emit(type, association, payload) {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
let success = false;
|
|
32
|
+
try {
|
|
33
|
+
const ref = await this.inner.emit(type, association, payload);
|
|
34
|
+
success = ref !== null;
|
|
35
|
+
this.metrics.record({
|
|
36
|
+
phase: "emit",
|
|
37
|
+
writer: this.writerName,
|
|
38
|
+
type,
|
|
39
|
+
ms: Date.now() - start,
|
|
40
|
+
bytes: ref?.bytes,
|
|
41
|
+
success,
|
|
42
|
+
});
|
|
43
|
+
return ref;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
this.metrics.record({
|
|
47
|
+
phase: "emit",
|
|
48
|
+
writer: this.writerName,
|
|
49
|
+
type,
|
|
50
|
+
ms: Date.now() - start,
|
|
51
|
+
success: false,
|
|
52
|
+
});
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async appendNdjson(type, association, rows) {
|
|
57
|
+
const start = Date.now();
|
|
58
|
+
try {
|
|
59
|
+
const ref = await this.inner.appendNdjson(type, association, rows);
|
|
60
|
+
this.metrics.record({
|
|
61
|
+
phase: "ndjson-part",
|
|
62
|
+
writer: this.writerName,
|
|
63
|
+
type,
|
|
64
|
+
ms: Date.now() - start,
|
|
65
|
+
bytes: ref?.bytes,
|
|
66
|
+
success: ref !== null,
|
|
67
|
+
});
|
|
68
|
+
return ref;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
this.metrics.record({
|
|
72
|
+
phase: "ndjson-part",
|
|
73
|
+
writer: this.writerName,
|
|
74
|
+
type,
|
|
75
|
+
ms: Date.now() - start,
|
|
76
|
+
success: false,
|
|
77
|
+
});
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async writeManifest(runId, manifest) {
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
let ref = null;
|
|
84
|
+
try {
|
|
85
|
+
ref = await this.inner.writeManifest(runId, manifest);
|
|
86
|
+
this.metrics.record({
|
|
87
|
+
phase: "manifest",
|
|
88
|
+
writer: this.writerName,
|
|
89
|
+
type: "manifest",
|
|
90
|
+
ms: Date.now() - start,
|
|
91
|
+
bytes: ref?.bytes,
|
|
92
|
+
success: ref !== null,
|
|
93
|
+
});
|
|
94
|
+
return ref;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
// Summarize once, after the manifest write resolves, regardless of outcome.
|
|
98
|
+
await this.metrics.summarize();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async writeBulk(type, runId, data) {
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
try {
|
|
104
|
+
const ref = await this.inner.writeBulk(type, runId, data);
|
|
105
|
+
this.metrics.record({
|
|
106
|
+
phase: "emit",
|
|
107
|
+
writer: this.writerName,
|
|
108
|
+
type,
|
|
109
|
+
ms: Date.now() - start,
|
|
110
|
+
bytes: ref?.bytes,
|
|
111
|
+
success: ref !== null,
|
|
112
|
+
});
|
|
113
|
+
return ref;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
this.metrics.record({
|
|
117
|
+
phase: "emit",
|
|
118
|
+
writer: this.writerName,
|
|
119
|
+
type,
|
|
120
|
+
ms: Date.now() - start,
|
|
121
|
+
success: false,
|
|
122
|
+
});
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async writePerEntry(type, runId, entries) {
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
try {
|
|
129
|
+
const ref = await this.inner.writePerEntry(type, runId, entries);
|
|
130
|
+
this.metrics.record({
|
|
131
|
+
phase: "emit",
|
|
132
|
+
writer: this.writerName,
|
|
133
|
+
type,
|
|
134
|
+
ms: Date.now() - start,
|
|
135
|
+
bytes: ref?.bytes,
|
|
136
|
+
success: ref !== null,
|
|
137
|
+
});
|
|
138
|
+
return ref;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.metrics.record({
|
|
142
|
+
phase: "emit",
|
|
143
|
+
writer: this.writerName,
|
|
144
|
+
type,
|
|
145
|
+
ms: Date.now() - start,
|
|
146
|
+
success: false,
|
|
147
|
+
});
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M4)
|
|
37
37
|
* @see packages/eval/src/artifact-capture/gcs-artifact-writer.ts (mirror)
|
|
38
38
|
*/
|
|
39
|
-
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
39
|
+
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
40
40
|
export interface LocalFilesystemArtifactWriterOptions {
|
|
41
41
|
/**
|
|
42
42
|
* Absolute or cwd-relative root directory under which `runs/{runId}/…`
|
|
@@ -50,11 +50,18 @@ export interface LocalFilesystemArtifactWriterOptions {
|
|
|
50
50
|
* `--capture-exclude=LIST` in W0050 Slice 4.
|
|
51
51
|
*/
|
|
52
52
|
exclude?: readonly ArtifactType[];
|
|
53
|
+
/**
|
|
54
|
+
* Optional progress reporter + phaseId (W0053). When set, the writer
|
|
55
|
+
* publishes `phaseProgress` on every successful write so the CLI can
|
|
56
|
+
* render per-batch updates during long export phases.
|
|
57
|
+
*/
|
|
58
|
+
progress?: ArtifactWriterProgressOptions;
|
|
53
59
|
}
|
|
54
60
|
export declare class LocalFilesystemArtifactWriter implements ArtifactWriter {
|
|
55
61
|
private readonly options;
|
|
56
62
|
private readonly excludeSet;
|
|
57
63
|
constructor(options: LocalFilesystemArtifactWriterOptions);
|
|
64
|
+
private reportProgress;
|
|
58
65
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
59
66
|
appendNdjson<T extends ArtifactType>(type: T, association: AssociationValues, rows: readonly unknown[]): Promise<ArtifactRef | null>;
|
|
60
67
|
writeManifest(runId: RunId, manifest: RunManifest): Promise<ArtifactRef | null>;
|
|
@@ -50,6 +50,17 @@ export class LocalFilesystemArtifactWriter {
|
|
|
50
50
|
this.options = options;
|
|
51
51
|
this.excludeSet = new Set(options.exclude ?? []);
|
|
52
52
|
}
|
|
53
|
+
reportProgress(ref) {
|
|
54
|
+
const progress = this.options.progress;
|
|
55
|
+
if (!progress)
|
|
56
|
+
return;
|
|
57
|
+
progress.reporter.phaseProgress({
|
|
58
|
+
phaseId: progress.phaseId,
|
|
59
|
+
items: 1,
|
|
60
|
+
bytes: ref.bytes,
|
|
61
|
+
label: ref.path,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
53
64
|
// ---- Canonical W0049 API ------------------------------------------------
|
|
54
65
|
async emit(type, association, payload) {
|
|
55
66
|
if (this.excludeSet.has(type))
|
|
@@ -74,7 +85,7 @@ export class LocalFilesystemArtifactWriter {
|
|
|
74
85
|
const wrote = await this.writeAtomic(absPath, body);
|
|
75
86
|
if (!wrote)
|
|
76
87
|
return null;
|
|
77
|
-
|
|
88
|
+
const ref = {
|
|
78
89
|
store: "local",
|
|
79
90
|
bucket: this.options.rootDir,
|
|
80
91
|
path: relPath,
|
|
@@ -83,6 +94,8 @@ export class LocalFilesystemArtifactWriter {
|
|
|
83
94
|
layout: "bulk",
|
|
84
95
|
...(preview === undefined ? {} : { preview }),
|
|
85
96
|
};
|
|
97
|
+
this.reportProgress(ref);
|
|
98
|
+
return ref;
|
|
86
99
|
}
|
|
87
100
|
// per-entry
|
|
88
101
|
const entryKey = descriptor.formatEntryKey(association);
|
|
@@ -91,7 +104,7 @@ export class LocalFilesystemArtifactWriter {
|
|
|
91
104
|
const wrote = await this.writeAtomic(absPath, body);
|
|
92
105
|
if (!wrote)
|
|
93
106
|
return null;
|
|
94
|
-
|
|
107
|
+
const ref = {
|
|
95
108
|
store: "local",
|
|
96
109
|
bucket: this.options.rootDir,
|
|
97
110
|
path: `runs/${runId}/${descriptor.slug}`,
|
|
@@ -107,6 +120,8 @@ export class LocalFilesystemArtifactWriter {
|
|
|
107
120
|
},
|
|
108
121
|
],
|
|
109
122
|
};
|
|
123
|
+
this.reportProgress(ref);
|
|
124
|
+
return ref;
|
|
110
125
|
}
|
|
111
126
|
async appendNdjson(type, association, rows) {
|
|
112
127
|
if (this.excludeSet.has(type))
|
|
@@ -149,7 +164,7 @@ export class LocalFilesystemArtifactWriter {
|
|
|
149
164
|
catch {
|
|
150
165
|
// If stat fails we still have the current batch's bytes — acceptable.
|
|
151
166
|
}
|
|
152
|
-
|
|
167
|
+
const ref = {
|
|
153
168
|
store: "local",
|
|
154
169
|
bucket: this.options.rootDir,
|
|
155
170
|
path: `runs/${runId}/${descriptor.slug}`,
|
|
@@ -158,6 +173,8 @@ export class LocalFilesystemArtifactWriter {
|
|
|
158
173
|
layout: "per-entry",
|
|
159
174
|
entries: [{ key: entryKey, bytes: cumulative, association }],
|
|
160
175
|
};
|
|
176
|
+
this.reportProgress(ref);
|
|
177
|
+
return ref;
|
|
161
178
|
}
|
|
162
179
|
async writeManifest(runId, manifest) {
|
|
163
180
|
const relPath = `runs/${runId}/manifest.json`;
|
|
@@ -166,13 +183,15 @@ export class LocalFilesystemArtifactWriter {
|
|
|
166
183
|
const wrote = await this.writeAtomic(absPath, body);
|
|
167
184
|
if (!wrote)
|
|
168
185
|
return null;
|
|
169
|
-
|
|
186
|
+
const ref = {
|
|
170
187
|
store: "local",
|
|
171
188
|
bucket: this.options.rootDir,
|
|
172
189
|
path: relPath,
|
|
173
190
|
bytes: Buffer.byteLength(body, "utf-8"),
|
|
174
191
|
layout: "bulk",
|
|
175
192
|
};
|
|
193
|
+
this.reportProgress(ref);
|
|
194
|
+
return ref;
|
|
176
195
|
}
|
|
177
196
|
// ---- Deprecated legacy surface (W0052) ----------------------------------
|
|
178
197
|
/** @deprecated Use `emit()` instead. */
|