@sanity/ailf 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_vendor/ailf-core/artifact-capture/association.d.ts +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 +1 -1
- package/dist/orchestration/steps/finalize-run-step.js +33 -2
- 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 +3 -3
|
@@ -33,3 +33,40 @@ export interface AssocContext {
|
|
|
33
33
|
* boundary actually lives.
|
|
34
34
|
*/
|
|
35
35
|
export declare function assoc(ctx: AssocContext, partial?: Omit<AssociationValues, "run">): AssociationValues;
|
|
36
|
+
/**
|
|
37
|
+
* Literacy-mode task descriptions carry a `(gold)` / `(baseline)` suffix that
|
|
38
|
+
* encodes which half of the with-docs / without-docs pair the judgment belongs
|
|
39
|
+
* to. The D0033 artifact axes treat that variant as the `mode` axis — a single
|
|
40
|
+
* promptfoo run of evaluation mode "literacy" produces artifacts whose `mode`
|
|
41
|
+
* axis is "gold" or "baseline". All producers and all readers must agree on
|
|
42
|
+
* this decomposition, or signed-URL lookups 404.
|
|
43
|
+
*
|
|
44
|
+
* This helper is the single source of truth for the split. It's shared between:
|
|
45
|
+
* - eval's `emit-eval-results.ts` (rawResults, renderedPrompts,
|
|
46
|
+
* graderPrompts, graderJudgments)
|
|
47
|
+
* - eval's `upload-test-outputs.ts` (testOutputs)
|
|
48
|
+
* - core's `slim-report-summary.ts` (slim judgment / failure-mode ids)
|
|
49
|
+
* - studio's `JudgmentList#testOutputsKeyFor` (hover prefetch keys)
|
|
50
|
+
*
|
|
51
|
+
* Non-literacy modes (mcp-server, agent-harness, …) do not suffix their task
|
|
52
|
+
* descriptions; `splitTaskVariant` returns `variant: null` and the caller
|
|
53
|
+
* falls back to the high-level eval mode. See `resolveVariantMode`.
|
|
54
|
+
*
|
|
55
|
+
* Pure function — safe to call from browser or Node.
|
|
56
|
+
*/
|
|
57
|
+
export interface TaskVariantSplit {
|
|
58
|
+
/** Task id with any trailing `(gold)` / `(baseline)` suffix stripped. */
|
|
59
|
+
readonly task: string;
|
|
60
|
+
/** Lowercased variant (`"gold"` | `"baseline"`), or `null` when absent. */
|
|
61
|
+
readonly variant: "baseline" | "gold" | null;
|
|
62
|
+
}
|
|
63
|
+
export declare function splitTaskVariant(taskId: string): TaskVariantSplit;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the `mode` axis value for an artifact association: prefer the
|
|
66
|
+
* per-task variant when present, fall back to the high-level evaluation mode.
|
|
67
|
+
* Mirrors `mode = variant ?? defaultMode` in `slim-report-summary#slimJudgments`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveVariantMode(taskId: string, defaultMode: string): {
|
|
70
|
+
mode: string;
|
|
71
|
+
task: string;
|
|
72
|
+
};
|
|
@@ -26,3 +26,22 @@
|
|
|
26
26
|
export function assoc(ctx, partial = {}) {
|
|
27
27
|
return { run: ctx.runId, ...partial };
|
|
28
28
|
}
|
|
29
|
+
const VARIANT_SUFFIX_PATTERN = /\s*\((gold|baseline)\)\s*$/i;
|
|
30
|
+
export function splitTaskVariant(taskId) {
|
|
31
|
+
const match = VARIANT_SUFFIX_PATTERN.exec(taskId);
|
|
32
|
+
if (!match)
|
|
33
|
+
return { task: taskId, variant: null };
|
|
34
|
+
return {
|
|
35
|
+
task: taskId.slice(0, match.index).trim(),
|
|
36
|
+
variant: match[1].toLowerCase(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the `mode` axis value for an artifact association: prefer the
|
|
41
|
+
* per-task variant when present, fall back to the high-level evaluation mode.
|
|
42
|
+
* Mirrors `mode = variant ?? defaultMode` in `slim-report-summary#slimJudgments`.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveVariantMode(taskId, defaultMode) {
|
|
45
|
+
const { task, variant } = splitTaskVariant(taskId);
|
|
46
|
+
return { mode: variant ?? defaultMode, task };
|
|
47
|
+
}
|
|
@@ -21,4 +21,4 @@ export { defineConfig, defineFeatures, defineModeBase, defineModels, definePrici
|
|
|
21
21
|
export type { PricingEntry, PromptEntry, SourceEntry, } from "./config-helpers.js";
|
|
22
22
|
export { env } from "./env-helper.js";
|
|
23
23
|
export { NoOpArtifactWriter, NotImplementedError, } from "./ports/artifact-writer.js";
|
|
24
|
-
export { assoc, type AssocContext } from "./artifact-capture/association.js";
|
|
24
|
+
export { assoc, resolveVariantMode, splitTaskVariant, type AssocContext, type TaskVariantSplit, } from "./artifact-capture/association.js";
|
|
@@ -23,4 +23,4 @@ export * from "./batch-signing.js";
|
|
|
23
23
|
export { defineConfig, defineFeatures, defineModeBase, defineModels, definePricingTable, definePreset, definePrompts, defineRubrics, defineSchedules, defineSinks, defineSources, defineTask, defineThresholds, } from "./config-helpers.js";
|
|
24
24
|
export { env } from "./env-helper.js";
|
|
25
25
|
export { NoOpArtifactWriter, NotImplementedError, } from "./ports/artifact-writer.js";
|
|
26
|
-
export { assoc } from "./artifact-capture/association.js";
|
|
26
|
+
export { assoc, resolveVariantMode, splitTaskVariant, } from "./artifact-capture/association.js";
|
|
@@ -18,6 +18,7 @@ import type { CacheStore } from "./cache-store.js";
|
|
|
18
18
|
import type { DocFetcher } from "./doc-fetcher.js";
|
|
19
19
|
import type { EvalRunner } from "./eval-runner.js";
|
|
20
20
|
import type { Logger } from "./logger.js";
|
|
21
|
+
import type { ProgressReporter } from "./progress-reporter.js";
|
|
21
22
|
import type { TaskSource } from "./task-source.js";
|
|
22
23
|
/**
|
|
23
24
|
* Resolved pipeline configuration — the typed, validated result of
|
|
@@ -215,6 +216,13 @@ export interface AppContext {
|
|
|
215
216
|
readonly evalRunner: EvalRunner;
|
|
216
217
|
/** Structured logger */
|
|
217
218
|
readonly logger: Logger;
|
|
219
|
+
/**
|
|
220
|
+
* Progress reporter — carries `phase-start / phase-progress / phase-complete`
|
|
221
|
+
* events for long-running pipeline spans (W0053). The composition root always
|
|
222
|
+
* wires one; adapters default to `NoOpProgressReporter` for quiet / JSON /
|
|
223
|
+
* test loggers.
|
|
224
|
+
*/
|
|
225
|
+
readonly progress: ProgressReporter;
|
|
218
226
|
/** Plugin registry — mode handlers, assertions, rubric templates, etc. */
|
|
219
227
|
readonly registry: PluginRegistry;
|
|
220
228
|
/**
|
|
@@ -14,5 +14,7 @@ export type { EvalRunConfig, EvalRunner } from "./eval-runner.js";
|
|
|
14
14
|
export type { CompilationContext, CompileResultAssertion, CompileResultPrompt, CompileResultProvider, CompileResultTestCase, ModeCompileResult, ModeHandler, ModeProviderEntry, ModeRubricConfig, PromptTemplate, } from "./mode-handler.js";
|
|
15
15
|
export type { Logger } from "./logger.js";
|
|
16
16
|
export type { PipelineStep } from "./pipeline-step.js";
|
|
17
|
+
export type { ArtifactWriterProgressOptions, PhaseCompleteEvent, PhaseProgressEvent, PhaseStartEvent, ProgressReporter, } from "./progress-reporter.js";
|
|
18
|
+
export { ARTIFACT_EXPORT_PHASE_ID, NoOpProgressReporter, } from "./progress-reporter.js";
|
|
17
19
|
export type { TaskSource } from "./task-source.js";
|
|
18
20
|
export { canonicalDocRefLabel, isIdRef, isPathRef, isPerspectiveRef, isSlugRef, isTemplatedAssertion, } from "./task-source.js";
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
* Adapters (in packages/eval) implement these interfaces.
|
|
6
6
|
*/
|
|
7
7
|
export { NoOpArtifactWriter } from "./artifact-writer.js";
|
|
8
|
+
export { ARTIFACT_EXPORT_PHASE_ID, NoOpProgressReporter, } from "./progress-reporter.js";
|
|
8
9
|
export { canonicalDocRefLabel, isIdRef, isPathRef, isPerspectiveRef, isSlugRef, isTemplatedAssertion, } from "./task-source.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port: ProgressReporter — carries user-visible progress for long-running
|
|
3
|
+
* pipeline phases (artifact export, eval runs, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Events follow a `start / progress (batch) / complete` shape. Producers
|
|
6
|
+
* publish events without formatting; the CLI adapter decides cadence,
|
|
7
|
+
* layout, and whether to render at all.
|
|
8
|
+
*
|
|
9
|
+
* Scope today: the artifact export phase that runs after promptfoo finishes
|
|
10
|
+
* evaluation (W0053). Forward-compatible with future callers — e.g. the eval
|
|
11
|
+
* phase itself once we replace promptfoo's terminal-owned progress bar.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/work-items/W0053-cli-artifact-upload-progress.json
|
|
14
|
+
*/
|
|
15
|
+
export interface PhaseStartEvent {
|
|
16
|
+
/** Stable phase identifier. Adapters may use this to disambiguate if phases overlap. */
|
|
17
|
+
phaseId: string;
|
|
18
|
+
/** Human-readable label (e.g. `"Exporting run artifacts"`). */
|
|
19
|
+
label: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional extra context shown only on the header line (e.g. destination
|
|
22
|
+
* info such as `"local + GCS"`). Kept separate from `label` so the
|
|
23
|
+
* per-progress-line prefix stays short and scannable on every line.
|
|
24
|
+
*/
|
|
25
|
+
detail?: string;
|
|
26
|
+
/** Total work expected, when known up front. */
|
|
27
|
+
totalItems?: number;
|
|
28
|
+
/** Total bytes expected, when known up front. */
|
|
29
|
+
totalBytes?: number;
|
|
30
|
+
/** Wall-clock timestamp when the phase started (ms since epoch). */
|
|
31
|
+
startedAt: number;
|
|
32
|
+
}
|
|
33
|
+
export interface PhaseProgressEvent {
|
|
34
|
+
phaseId: string;
|
|
35
|
+
/** Items completed in this batch. Default `1`. */
|
|
36
|
+
items?: number;
|
|
37
|
+
/** Bytes transferred in this batch. Default `0`. */
|
|
38
|
+
bytes?: number;
|
|
39
|
+
/** Label for the most recent item (e.g. artifact path). */
|
|
40
|
+
label?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PhaseCompleteEvent {
|
|
43
|
+
phaseId: string;
|
|
44
|
+
itemsCompleted: number;
|
|
45
|
+
bytesCompleted: number;
|
|
46
|
+
durationMs: number;
|
|
47
|
+
/** Optional override for the final summary line. */
|
|
48
|
+
summary?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ProgressReporter {
|
|
51
|
+
phaseStart(event: PhaseStartEvent): void;
|
|
52
|
+
phaseProgress(event: PhaseProgressEvent): void;
|
|
53
|
+
phaseComplete(event: PhaseCompleteEvent): void;
|
|
54
|
+
}
|
|
55
|
+
/** No-op reporter — used when progress rendering is disabled (quiet / JSON logger / tests). */
|
|
56
|
+
export declare class NoOpProgressReporter implements ProgressReporter {
|
|
57
|
+
phaseStart(): void;
|
|
58
|
+
phaseProgress(): void;
|
|
59
|
+
phaseComplete(): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Canonical phase identifier for the post-eval artifact export span (W0053).
|
|
63
|
+
* Shared between writer wiring and the pipeline orchestrator so both sides
|
|
64
|
+
* publish to the same phase.
|
|
65
|
+
*/
|
|
66
|
+
export declare const ARTIFACT_EXPORT_PHASE_ID = "artifact-export";
|
|
67
|
+
/**
|
|
68
|
+
* Options shared by artifact writer adapters that publish progress events.
|
|
69
|
+
* Kept in the port module so every writer adapter imports the same shape.
|
|
70
|
+
*/
|
|
71
|
+
export interface ArtifactWriterProgressOptions {
|
|
72
|
+
reporter: ProgressReporter;
|
|
73
|
+
phaseId: string;
|
|
74
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port: ProgressReporter — carries user-visible progress for long-running
|
|
3
|
+
* pipeline phases (artifact export, eval runs, etc.).
|
|
4
|
+
*
|
|
5
|
+
* Events follow a `start / progress (batch) / complete` shape. Producers
|
|
6
|
+
* publish events without formatting; the CLI adapter decides cadence,
|
|
7
|
+
* layout, and whether to render at all.
|
|
8
|
+
*
|
|
9
|
+
* Scope today: the artifact export phase that runs after promptfoo finishes
|
|
10
|
+
* evaluation (W0053). Forward-compatible with future callers — e.g. the eval
|
|
11
|
+
* phase itself once we replace promptfoo's terminal-owned progress bar.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/work-items/W0053-cli-artifact-upload-progress.json
|
|
14
|
+
*/
|
|
15
|
+
/** No-op reporter — used when progress rendering is disabled (quiet / JSON logger / tests). */
|
|
16
|
+
export class NoOpProgressReporter {
|
|
17
|
+
phaseStart() { }
|
|
18
|
+
phaseProgress() { }
|
|
19
|
+
phaseComplete() { }
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Canonical phase identifier for the post-eval artifact export span (W0053).
|
|
23
|
+
* Shared between writer wiring and the pipeline orchestrator so both sides
|
|
24
|
+
* publish to the same phase.
|
|
25
|
+
*/
|
|
26
|
+
export const ARTIFACT_EXPORT_PHASE_ID = "artifact-export";
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M7)
|
|
16
16
|
* @see docs/work-items/W0051-report-slim-down-manifest-preview-hooks.json
|
|
17
17
|
*/
|
|
18
|
+
import { splitTaskVariant } from "../artifact-capture/association.js";
|
|
18
19
|
import { ARTIFACT_REGISTRY } from "../artifact-registry.js";
|
|
19
20
|
/**
|
|
20
21
|
* Transform a full pipeline `ScoreSummary` into its slim Report counterpart.
|
|
@@ -49,22 +50,6 @@ export function buildSlimReportSummary(summary, mode) {
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
50
51
|
// Judgments — axes {mode, task, model, grader}
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
|
-
/**
|
|
53
|
-
* Variant-suffix stripper. Judgments' `taskId` today carries `(gold)` /
|
|
54
|
-
* `(baseline)` suffixes that encode the pipeline mode. We strip the suffix
|
|
55
|
-
* to build the canonical `task` axis value and use the caller-supplied
|
|
56
|
-
* `mode` for the `mode` axis — matching how `formatEntryKey` is computed
|
|
57
|
-
* at producer emit time.
|
|
58
|
-
*/
|
|
59
|
-
function splitTaskVariant(taskId) {
|
|
60
|
-
const match = /\s*\((gold|baseline)\)\s*$/i.exec(taskId);
|
|
61
|
-
if (!match)
|
|
62
|
-
return { task: taskId, variant: null };
|
|
63
|
-
return {
|
|
64
|
-
task: taskId.slice(0, match.index).trim(),
|
|
65
|
-
variant: match[1].toLowerCase(),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
53
|
function slimJudgments(full, defaultMode) {
|
|
69
54
|
const descriptor = ARTIFACT_REGISTRY.graderJudgments;
|
|
70
55
|
const formatKey = descriptor.formatEntryKey;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsoleProgressReporter — the default terminal adapter for the
|
|
3
|
+
* ProgressReporter port (W0053).
|
|
4
|
+
*
|
|
5
|
+
* Renders phase-start / phase-progress / phase-complete as human-readable
|
|
6
|
+
* lines on stdout. Progress lines are rate-limited so a high-frequency
|
|
7
|
+
* producer (one event per artifact) never floods the terminal; the adapter
|
|
8
|
+
* flushes at most once per `tickMs` (default 1s) and always flushes the
|
|
9
|
+
* final line on `phaseComplete`.
|
|
10
|
+
*
|
|
11
|
+
* `verbose: true` emits one line per item — matches the CLI's existing
|
|
12
|
+
* `--verbose` semantics without re-rendering the rate-limited line twice.
|
|
13
|
+
*/
|
|
14
|
+
import type { PhaseCompleteEvent, PhaseProgressEvent, PhaseStartEvent, ProgressReporter } from "../../_vendor/ailf-core/index.d.ts";
|
|
15
|
+
export interface ConsoleProgressReporterOptions {
|
|
16
|
+
/** Minimum interval between rate-limited progress lines, in ms. Default 1000. */
|
|
17
|
+
tickMs?: number;
|
|
18
|
+
/** Emit one line per item rather than rate-limiting. */
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
/** Sink for output (tests override). Defaults to `console.log`. */
|
|
21
|
+
write?: (line: string) => void;
|
|
22
|
+
/** Clock (tests override). Defaults to `Date.now`. */
|
|
23
|
+
now?: () => number;
|
|
24
|
+
}
|
|
25
|
+
export declare class ConsoleProgressReporter implements ProgressReporter {
|
|
26
|
+
private readonly tickMs;
|
|
27
|
+
private readonly verbose;
|
|
28
|
+
private readonly write;
|
|
29
|
+
private readonly now;
|
|
30
|
+
private readonly phases;
|
|
31
|
+
constructor(options?: ConsoleProgressReporterOptions);
|
|
32
|
+
phaseStart(event: PhaseStartEvent): void;
|
|
33
|
+
phaseProgress(event: PhaseProgressEvent): void;
|
|
34
|
+
phaseComplete(event: PhaseCompleteEvent): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsoleProgressReporter — the default terminal adapter for the
|
|
3
|
+
* ProgressReporter port (W0053).
|
|
4
|
+
*
|
|
5
|
+
* Renders phase-start / phase-progress / phase-complete as human-readable
|
|
6
|
+
* lines on stdout. Progress lines are rate-limited so a high-frequency
|
|
7
|
+
* producer (one event per artifact) never floods the terminal; the adapter
|
|
8
|
+
* flushes at most once per `tickMs` (default 1s) and always flushes the
|
|
9
|
+
* final line on `phaseComplete`.
|
|
10
|
+
*
|
|
11
|
+
* `verbose: true` emits one line per item — matches the CLI's existing
|
|
12
|
+
* `--verbose` semantics without re-rendering the rate-limited line twice.
|
|
13
|
+
*/
|
|
14
|
+
export class ConsoleProgressReporter {
|
|
15
|
+
tickMs;
|
|
16
|
+
verbose;
|
|
17
|
+
write;
|
|
18
|
+
now;
|
|
19
|
+
phases = new Map();
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.tickMs = options.tickMs ?? 1000;
|
|
22
|
+
this.verbose = options.verbose ?? false;
|
|
23
|
+
this.write = options.write ?? ((line) => console.log(line));
|
|
24
|
+
this.now = options.now ?? (() => Date.now());
|
|
25
|
+
}
|
|
26
|
+
phaseStart(event) {
|
|
27
|
+
this.phases.set(event.phaseId, {
|
|
28
|
+
phaseId: event.phaseId,
|
|
29
|
+
label: event.label,
|
|
30
|
+
totalItems: event.totalItems,
|
|
31
|
+
totalBytes: event.totalBytes,
|
|
32
|
+
startedAt: event.startedAt,
|
|
33
|
+
itemsCompleted: 0,
|
|
34
|
+
bytesCompleted: 0,
|
|
35
|
+
lastFlushAt: event.startedAt,
|
|
36
|
+
});
|
|
37
|
+
const total = event.totalItems !== undefined ? ` (${event.totalItems} items)` : "";
|
|
38
|
+
const detail = event.detail ? ` → ${event.detail}` : "";
|
|
39
|
+
this.write("");
|
|
40
|
+
this.write(` ➜ ${event.label}${detail}${total}…`);
|
|
41
|
+
}
|
|
42
|
+
phaseProgress(event) {
|
|
43
|
+
const state = this.phases.get(event.phaseId);
|
|
44
|
+
if (!state)
|
|
45
|
+
return;
|
|
46
|
+
state.itemsCompleted += event.items ?? 1;
|
|
47
|
+
state.bytesCompleted += event.bytes ?? 0;
|
|
48
|
+
if (this.verbose) {
|
|
49
|
+
const item = event.label ?? `item ${state.itemsCompleted}`;
|
|
50
|
+
const bytes = event.bytes ?? 0;
|
|
51
|
+
this.write(` · ${progressPrefix(state)} ${item} (${formatBytes(bytes)})`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const now = this.now();
|
|
55
|
+
if (now - state.lastFlushAt < this.tickMs)
|
|
56
|
+
return;
|
|
57
|
+
state.lastFlushAt = now;
|
|
58
|
+
this.write(` ${renderProgressLine(state, now)}`);
|
|
59
|
+
}
|
|
60
|
+
phaseComplete(event) {
|
|
61
|
+
const state = this.phases.get(event.phaseId);
|
|
62
|
+
// Prefer the adapter's running totals — we've been counting each batch
|
|
63
|
+
// event anyway, and most producers call `phaseComplete` without knowing
|
|
64
|
+
// cumulative numbers themselves. Fall back to the event fields when the
|
|
65
|
+
// reporter has no state (e.g. first phaseComplete without a phaseStart).
|
|
66
|
+
const itemsCompleted = Math.max(state?.itemsCompleted ?? 0, event.itemsCompleted);
|
|
67
|
+
const bytesCompleted = Math.max(state?.bytesCompleted ?? 0, event.bytesCompleted);
|
|
68
|
+
const durationMs = event.durationMs;
|
|
69
|
+
const label = state?.label ?? event.phaseId;
|
|
70
|
+
const summary = event.summary ??
|
|
71
|
+
`${itemsCompleted} item${itemsCompleted === 1 ? "" : "s"} · ${formatBytes(bytesCompleted)} · ${formatDuration(durationMs)} elapsed`;
|
|
72
|
+
this.write(` ✓ ${label} — ${summary}`);
|
|
73
|
+
this.phases.delete(event.phaseId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Helpers
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Shared prefix used on every rate-limited and verbose progress line so the
|
|
81
|
+
* context is visible without having to scroll up to the phase header.
|
|
82
|
+
*/
|
|
83
|
+
function progressPrefix(state) {
|
|
84
|
+
return `${state.label}:`;
|
|
85
|
+
}
|
|
86
|
+
function renderProgressLine(state, now) {
|
|
87
|
+
const elapsed = formatDuration(now - state.startedAt);
|
|
88
|
+
const itemsPart = state.totalItems !== undefined
|
|
89
|
+
? `${state.itemsCompleted}/${state.totalItems}`
|
|
90
|
+
: String(state.itemsCompleted);
|
|
91
|
+
return `${progressPrefix(state)} ${itemsPart} · ${formatBytes(state.bytesCompleted)} · ${elapsed} elapsed`;
|
|
92
|
+
}
|
|
93
|
+
function formatBytes(bytes) {
|
|
94
|
+
if (bytes < 1024)
|
|
95
|
+
return `${bytes} B`;
|
|
96
|
+
if (bytes < 1024 * 1024)
|
|
97
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
98
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
99
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
100
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
101
|
+
}
|
|
102
|
+
function formatDuration(ms) {
|
|
103
|
+
if (ms < 1000)
|
|
104
|
+
return `${ms}ms`;
|
|
105
|
+
if (ms < 60_000)
|
|
106
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
107
|
+
const minutes = Math.floor(ms / 60_000);
|
|
108
|
+
const seconds = Math.round((ms % 60_000) / 1000);
|
|
109
|
+
return `${minutes}m${seconds}s`;
|
|
110
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Endpoints:
|
|
9
9
|
* - Bulk: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/upload-url
|
|
10
10
|
* - Per-entry: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/{entryKey}/upload-url
|
|
11
|
-
* - Manifest: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/
|
|
11
|
+
* - Manifest: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/upload-url
|
|
12
12
|
*
|
|
13
13
|
* ## W0049 API surface
|
|
14
14
|
*
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
|
|
29
29
|
*/
|
|
30
30
|
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 UploadMetricsSink } from "./upload-metrics.js";
|
|
31
32
|
export interface ApiGatewayArtifactWriterOptions {
|
|
32
33
|
/** Base URL of the API gateway (e.g., "https://ailf-api.sanity.build"). */
|
|
33
34
|
apiBaseUrl: string;
|
|
@@ -35,9 +36,15 @@ export interface ApiGatewayArtifactWriterOptions {
|
|
|
35
36
|
apiKey: string;
|
|
36
37
|
/** GCS bucket name — included in the returned ArtifactRef. */
|
|
37
38
|
bucket: string;
|
|
39
|
+
/**
|
|
40
|
+
* W0056 spike — optional metrics sink that receives per-phase timing events.
|
|
41
|
+
* Defaults to a no-op so the hot path stays free when metrics are off.
|
|
42
|
+
*/
|
|
43
|
+
metrics?: UploadMetricsSink;
|
|
38
44
|
}
|
|
39
45
|
export declare class ApiGatewayArtifactWriter implements ArtifactWriter {
|
|
40
46
|
private readonly options;
|
|
47
|
+
private readonly metrics;
|
|
41
48
|
constructor(options: ApiGatewayArtifactWriterOptions);
|
|
42
49
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
43
50
|
appendNdjson(): Promise<ArtifactRef | null>;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Endpoints:
|
|
9
9
|
* - Bulk: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/upload-url
|
|
10
10
|
* - Per-entry: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/{type}/{entryKey}/upload-url
|
|
11
|
-
* - Manifest: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/
|
|
11
|
+
* - Manifest: GET {apiBaseUrl}/v1/runs/{runId}/artifacts/upload-url
|
|
12
12
|
*
|
|
13
13
|
* ## W0049 API surface
|
|
14
14
|
*
|
|
@@ -28,10 +28,13 @@
|
|
|
28
28
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
|
|
29
29
|
*/
|
|
30
30
|
import { ARTIFACT_REGISTRY, NotImplementedError, } from "../_vendor/ailf-core/index.js";
|
|
31
|
+
import { NO_OP_UPLOAD_METRICS, } from "./upload-metrics.js";
|
|
31
32
|
export class ApiGatewayArtifactWriter {
|
|
32
33
|
options;
|
|
34
|
+
metrics;
|
|
33
35
|
constructor(options) {
|
|
34
36
|
this.options = options;
|
|
37
|
+
this.metrics = options.metrics ?? NO_OP_UPLOAD_METRICS;
|
|
35
38
|
}
|
|
36
39
|
// ---- Canonical W0049 API ------------------------------------------------
|
|
37
40
|
async emit(type, association, payload) {
|
|
@@ -46,12 +49,13 @@ export class ApiGatewayArtifactWriter {
|
|
|
46
49
|
return this.putJson(uploadUrlPath, payload, {
|
|
47
50
|
layout: "bulk",
|
|
48
51
|
entryCount: entryCountOf(payload),
|
|
52
|
+
type,
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
// per-entry
|
|
52
56
|
const entryKey = descriptor.formatEntryKey(association);
|
|
53
57
|
const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/${encodeURIComponent(entryKey)}/upload-url`;
|
|
54
|
-
const result = await this.putJsonRaw(uploadUrlPath, payload);
|
|
58
|
+
const result = await this.putJsonRaw(uploadUrlPath, payload, type);
|
|
55
59
|
if (!result)
|
|
56
60
|
return null;
|
|
57
61
|
return {
|
|
@@ -72,8 +76,11 @@ export class ApiGatewayArtifactWriter {
|
|
|
72
76
|
"the batch route.");
|
|
73
77
|
}
|
|
74
78
|
async writeManifest(runId, manifest) {
|
|
75
|
-
const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/
|
|
76
|
-
return this.putJson(uploadUrlPath, manifest, {
|
|
79
|
+
const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/upload-url`;
|
|
80
|
+
return this.putJson(uploadUrlPath, manifest, {
|
|
81
|
+
layout: "bulk",
|
|
82
|
+
type: "manifest",
|
|
83
|
+
});
|
|
77
84
|
}
|
|
78
85
|
// ---- Deprecated legacy surface (W0052) ----------------------------------
|
|
79
86
|
/** @deprecated Use `emit()` instead. */
|
|
@@ -82,6 +89,7 @@ export class ApiGatewayArtifactWriter {
|
|
|
82
89
|
return this.putJson(uploadUrlPath, data, {
|
|
83
90
|
layout: "bulk",
|
|
84
91
|
entryCount: entryCountOf(data),
|
|
92
|
+
type,
|
|
85
93
|
});
|
|
86
94
|
}
|
|
87
95
|
/** @deprecated Use `emit()` per entry instead. */
|
|
@@ -101,7 +109,7 @@ export class ApiGatewayArtifactWriter {
|
|
|
101
109
|
continue;
|
|
102
110
|
}
|
|
103
111
|
const uploadUrlPath = `/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(type)}/${encodeURIComponent(entry.key)}/upload-url`;
|
|
104
|
-
const result = await this.putJsonRaw(uploadUrlPath, entry.data);
|
|
112
|
+
const result = await this.putJsonRaw(uploadUrlPath, entry.data, type);
|
|
105
113
|
if (!result)
|
|
106
114
|
continue;
|
|
107
115
|
bucket = result.bucket;
|
|
@@ -122,7 +130,7 @@ export class ApiGatewayArtifactWriter {
|
|
|
122
130
|
}
|
|
123
131
|
// ---- Internals ----------------------------------------------------------
|
|
124
132
|
async putJson(uploadUrlPath, data, meta) {
|
|
125
|
-
const result = await this.putJsonRaw(uploadUrlPath, data);
|
|
133
|
+
const result = await this.putJsonRaw(uploadUrlPath, data, meta.type);
|
|
126
134
|
if (!result)
|
|
127
135
|
return null;
|
|
128
136
|
return {
|
|
@@ -134,23 +142,38 @@ export class ApiGatewayArtifactWriter {
|
|
|
134
142
|
layout: meta.layout,
|
|
135
143
|
};
|
|
136
144
|
}
|
|
137
|
-
async putJsonRaw(uploadUrlPath, data) {
|
|
145
|
+
async putJsonRaw(uploadUrlPath, data, type) {
|
|
138
146
|
const json = JSON.stringify(data);
|
|
139
147
|
const bytes = Buffer.byteLength(json, "utf-8");
|
|
140
148
|
try {
|
|
141
|
-
const signed = await this.fetchSignedUrl(uploadUrlPath);
|
|
149
|
+
const signed = await this.fetchSignedUrl(uploadUrlPath, type);
|
|
142
150
|
if (!signed)
|
|
143
151
|
return null;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
const putStart = Date.now();
|
|
153
|
+
let putSuccess = false;
|
|
154
|
+
try {
|
|
155
|
+
const putRes = await fetch(signed.url, {
|
|
156
|
+
body: json,
|
|
157
|
+
headers: signed.requiredHeaders,
|
|
158
|
+
method: "PUT",
|
|
159
|
+
});
|
|
160
|
+
if (!putRes.ok) {
|
|
161
|
+
console.warn(` ⚠️ Artifact upload failed (non-blocking): ${signed.path} — GCS PUT ${putRes.status} ${putRes.statusText}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
putSuccess = true;
|
|
165
|
+
return { bucket: signed.bucket, path: signed.path, bytes };
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
this.metrics.record({
|
|
169
|
+
phase: "put",
|
|
170
|
+
writer: "ApiGatewayArtifactWriter",
|
|
171
|
+
type,
|
|
172
|
+
ms: Date.now() - putStart,
|
|
173
|
+
bytes,
|
|
174
|
+
success: putSuccess,
|
|
175
|
+
});
|
|
152
176
|
}
|
|
153
|
-
return { bucket: signed.bucket, path: signed.path, bytes };
|
|
154
177
|
}
|
|
155
178
|
catch (err) {
|
|
156
179
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -158,33 +181,47 @@ export class ApiGatewayArtifactWriter {
|
|
|
158
181
|
return null;
|
|
159
182
|
}
|
|
160
183
|
}
|
|
161
|
-
async fetchSignedUrl(uploadUrlPath) {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
async fetchSignedUrl(uploadUrlPath, type) {
|
|
185
|
+
const start = Date.now();
|
|
186
|
+
let success = false;
|
|
187
|
+
try {
|
|
188
|
+
const url = `${this.options.apiBaseUrl.replace(/\/$/, "")}${uploadUrlPath}`;
|
|
189
|
+
const res = await fetch(url, {
|
|
190
|
+
headers: { Authorization: `Bearer ${this.options.apiKey}` },
|
|
191
|
+
method: "GET",
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
console.warn(` ⚠️ Signed-URL request failed: ${res.status} ${res.statusText}`);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const body = (await res.json());
|
|
198
|
+
if (body.object !== "signed_upload_url" ||
|
|
199
|
+
typeof body.url !== "string" ||
|
|
200
|
+
typeof body.path !== "string" ||
|
|
201
|
+
typeof body.bucket !== "string" ||
|
|
202
|
+
!body.requiredHeaders) {
|
|
203
|
+
console.warn(` ⚠️ Signed-URL response was malformed`);
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
success = true;
|
|
207
|
+
return {
|
|
208
|
+
bucket: body.bucket,
|
|
209
|
+
method: "PUT",
|
|
210
|
+
object: "signed_upload_url",
|
|
211
|
+
path: body.path,
|
|
212
|
+
requiredHeaders: body.requiredHeaders,
|
|
213
|
+
url: body.url,
|
|
214
|
+
};
|
|
170
215
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
216
|
+
finally {
|
|
217
|
+
this.metrics.record({
|
|
218
|
+
phase: "sign",
|
|
219
|
+
writer: "ApiGatewayArtifactWriter",
|
|
220
|
+
type,
|
|
221
|
+
ms: Date.now() - start,
|
|
222
|
+
success,
|
|
223
|
+
});
|
|
179
224
|
}
|
|
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
225
|
}
|
|
189
226
|
}
|
|
190
227
|
function entryCountOf(data) {
|