@sanity/ailf 2.8.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_vendor/ailf-core/artifact-capture/association.d.ts +35 -0
- package/dist/_vendor/ailf-core/artifact-capture/association.js +28 -0
- package/dist/_vendor/ailf-core/artifact-registry.d.ts +124 -23
- package/dist/_vendor/ailf-core/artifact-registry.js +708 -64
- package/dist/_vendor/ailf-core/batch-signing.d.ts +64 -0
- package/dist/_vendor/ailf-core/batch-signing.js +23 -0
- package/dist/_vendor/ailf-core/index.d.ts +3 -2
- package/dist/_vendor/ailf-core/index.js +3 -2
- package/dist/_vendor/ailf-core/ports/artifact-writer.d.ts +59 -20
- package/dist/_vendor/ailf-core/ports/artifact-writer.js +33 -10
- package/dist/_vendor/ailf-core/ports/context.d.ts +20 -17
- package/dist/_vendor/ailf-core/ports/index.d.ts +0 -2
- package/dist/_vendor/ailf-core/schemas/pipeline.d.ts +6 -6
- package/dist/_vendor/ailf-core/services/index.d.ts +1 -0
- package/dist/_vendor/ailf-core/services/index.js +1 -0
- package/dist/_vendor/ailf-core/services/slim-report-summary.d.ts +31 -0
- package/dist/_vendor/ailf-core/services/slim-report-summary.js +217 -0
- package/dist/_vendor/ailf-core/types/branded-ids.d.ts +33 -0
- package/dist/_vendor/ailf-core/types/index.d.ts +202 -23
- package/dist/adapters/config-sources/file-config-adapter.js +0 -4
- package/dist/artifact-capture/accumulating-artifact-writer.d.ts +50 -0
- package/dist/artifact-capture/accumulating-artifact-writer.js +111 -0
- package/dist/artifact-capture/api-gateway-artifact-writer.d.ts +17 -4
- package/dist/artifact-capture/api-gateway-artifact-writer.js +58 -7
- package/dist/artifact-capture/emit-file.d.ts +28 -0
- package/dist/artifact-capture/emit-file.js +56 -0
- package/dist/artifact-capture/fanout-artifact-writer.d.ts +39 -0
- package/dist/artifact-capture/fanout-artifact-writer.js +76 -0
- package/dist/artifact-capture/gcs-artifact-writer.d.ts +40 -3
- package/dist/artifact-capture/gcs-artifact-writer.js +238 -14
- package/dist/artifact-capture/local-fs-artifact-writer.d.ts +71 -0
- package/dist/artifact-capture/local-fs-artifact-writer.js +273 -0
- package/dist/artifact-capture/redact-artifact.d.ts +3 -5
- package/dist/artifact-capture/redact-artifact.js +3 -5
- package/dist/cli.js +56 -2
- package/dist/commands/explain-handler.js +4 -4
- package/dist/commands/pipeline-action.d.ts +5 -4
- package/dist/commands/pipeline-action.js +33 -16
- package/dist/commands/pipeline.d.ts +4 -4
- package/dist/commands/pipeline.js +4 -4
- package/dist/commands/publish.js +4 -1
- package/dist/commands/runs.d.ts +18 -0
- package/dist/commands/runs.js +71 -0
- package/dist/composition-root.d.ts +13 -10
- package/dist/composition-root.js +74 -46
- package/dist/orchestration/build-app-context.js +4 -7
- package/dist/orchestration/pipeline-orchestrator.d.ts +1 -1
- package/dist/orchestration/pipeline-orchestrator.js +37 -46
- package/dist/orchestration/steps/calculate-scores-step.d.ts +1 -1
- package/dist/orchestration/steps/calculate-scores-step.js +19 -19
- package/dist/orchestration/steps/callback-step.d.ts +1 -1
- package/dist/orchestration/steps/callback-step.js +6 -4
- package/dist/orchestration/steps/compare-step.d.ts +1 -1
- package/dist/orchestration/steps/compare-step.js +4 -2
- package/dist/orchestration/steps/discovery-report-step.d.ts +1 -1
- package/dist/orchestration/steps/discovery-report-step.js +4 -1
- package/dist/orchestration/steps/fetch-docs-step.js +9 -15
- package/dist/orchestration/steps/finalize-run-step.js +21 -7
- package/dist/orchestration/steps/gap-analysis-step.js +34 -6
- package/dist/orchestration/steps/generate-configs-step.d.ts +1 -1
- package/dist/orchestration/steps/generate-configs-step.js +11 -11
- package/dist/orchestration/steps/publish-report-step.d.ts +1 -1
- package/dist/orchestration/steps/publish-report-step.js +24 -19
- package/dist/orchestration/steps/readiness-step.d.ts +1 -1
- package/dist/orchestration/steps/readiness-step.js +4 -1
- package/dist/orchestration/steps/report-step.d.ts +1 -1
- package/dist/orchestration/steps/report-step.js +6 -3
- package/dist/orchestration/steps/run-eval-step.js +14 -9
- package/dist/pipeline/compare.d.ts +2 -2
- package/dist/pipeline/emit-eval-results.d.ts +38 -0
- package/dist/pipeline/emit-eval-results.js +100 -0
- package/dist/pipeline/map-request-to-config.js +0 -4
- package/package.json +1 -1
- package/dist/_vendor/ailf-core/artifact-capture/noop-collector.d.ts +0 -14
- package/dist/_vendor/ailf-core/artifact-capture/noop-collector.js +0 -25
- package/dist/_vendor/ailf-core/ports/artifact-collector.d.ts +0 -94
- package/dist/_vendor/ailf-core/ports/artifact-collector.js +0 -13
- package/dist/_vendor/ailf-core/ports/capture-comparator.d.ts +0 -138
- package/dist/_vendor/ailf-core/ports/capture-comparator.js +0 -10
- package/dist/artifact-capture/comparator.d.ts +0 -22
- package/dist/artifact-capture/comparator.js +0 -493
- package/dist/artifact-capture/filesystem-collector.d.ts +0 -42
- package/dist/artifact-capture/filesystem-collector.js +0 -237
- package/dist/artifact-capture/gcs-collector.d.ts +0 -55
- package/dist/artifact-capture/gcs-collector.js +0 -117
- package/dist/commands/capture-compare.d.ts +0 -15
- package/dist/commands/capture-compare.js +0 -253
- package/dist/commands/capture-list.d.ts +0 -12
- package/dist/commands/capture-list.js +0 -150
- package/dist/commands/capture.d.ts +0 -9
- package/dist/commands/capture.js +0 -16
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FilesystemArtifactCollector — writes captured artifacts to a local directory.
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
* Design principles:
|
|
9
|
-
* - capture() and captureFile() are synchronous (no I/O during step execution)
|
|
10
|
-
* - flush() does all I/O at pipeline end
|
|
11
|
-
* - Failures in capture/captureFile are swallowed (P5: non-blocking)
|
|
12
|
-
*/
|
|
13
|
-
import { execFileSync } from "node:child_process";
|
|
14
|
-
import { copyFileSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { redactArtifactData } from "./redact-artifact.js";
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Timestamp-prefixed capture ID (same shape as report-store.ts IDs)
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
function generateCaptureId() {
|
|
21
|
-
const now = Date.now();
|
|
22
|
-
const uuid = crypto.randomUUID();
|
|
23
|
-
const hex = now.toString(16).padStart(12, "0");
|
|
24
|
-
return (hex.slice(0, 8) +
|
|
25
|
-
"-" +
|
|
26
|
-
hex.slice(8, 12) +
|
|
27
|
-
"-7" +
|
|
28
|
-
uuid.slice(15, 18) +
|
|
29
|
-
"-" +
|
|
30
|
-
uuid.slice(19, 23) +
|
|
31
|
-
"-" +
|
|
32
|
-
uuid.slice(24));
|
|
33
|
-
}
|
|
34
|
-
function formatTimestamp(date) {
|
|
35
|
-
const y = date.getFullYear();
|
|
36
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
37
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
38
|
-
const h = String(date.getHours()).padStart(2, "0");
|
|
39
|
-
const min = String(date.getMinutes()).padStart(2, "0");
|
|
40
|
-
const s = String(date.getSeconds()).padStart(2, "0");
|
|
41
|
-
return `${y}${m}${d}-${h}${min}${s}`;
|
|
42
|
-
}
|
|
43
|
-
function detectFormat(entry) {
|
|
44
|
-
if (entry.filePath) {
|
|
45
|
-
const ext = path.extname(entry.filePath).toLowerCase();
|
|
46
|
-
if (ext === ".json")
|
|
47
|
-
return "json";
|
|
48
|
-
if (ext === ".md")
|
|
49
|
-
return "markdown";
|
|
50
|
-
if (ext === ".yaml" || ext === ".yml")
|
|
51
|
-
return "text";
|
|
52
|
-
return "text";
|
|
53
|
-
}
|
|
54
|
-
// In-memory data — if it's an object/array, it's JSON
|
|
55
|
-
if (typeof entry.data === "object" && entry.data !== null)
|
|
56
|
-
return "json";
|
|
57
|
-
return "text";
|
|
58
|
-
}
|
|
59
|
-
function fileExtension(format) {
|
|
60
|
-
switch (format) {
|
|
61
|
-
case "json":
|
|
62
|
-
return ".json";
|
|
63
|
-
case "markdown":
|
|
64
|
-
return ".md";
|
|
65
|
-
default:
|
|
66
|
-
return ".txt";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Collector
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
export class FilesystemArtifactCollector {
|
|
73
|
-
enabled = true;
|
|
74
|
-
extrasEnabled;
|
|
75
|
-
entries = [];
|
|
76
|
-
captureId;
|
|
77
|
-
outputDir;
|
|
78
|
-
startedAt;
|
|
79
|
-
options;
|
|
80
|
-
constructor(options) {
|
|
81
|
-
this.options = options;
|
|
82
|
-
this.extrasEnabled = options.extras;
|
|
83
|
-
this.captureId = generateCaptureId();
|
|
84
|
-
this.startedAt = new Date().toISOString();
|
|
85
|
-
// Use the last 4 hex chars (from the random UUID portion) — the first
|
|
86
|
-
// chars are timestamp-derived and change too slowly to disambiguate.
|
|
87
|
-
const shortId = this.captureId.replace(/-/g, "").slice(-4);
|
|
88
|
-
const timestamp = formatTimestamp(new Date());
|
|
89
|
-
this.outputDir = path.join(options.captureDir, `${options.mode}-${timestamp}-${shortId}`);
|
|
90
|
-
}
|
|
91
|
-
capture(step, type, data, meta) {
|
|
92
|
-
try {
|
|
93
|
-
this.entries.push({
|
|
94
|
-
step,
|
|
95
|
-
type,
|
|
96
|
-
data,
|
|
97
|
-
meta,
|
|
98
|
-
capturedAt: new Date().toISOString(),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
// P5: non-blocking — swallow capture errors
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
captureFile(step, type, filePath, meta) {
|
|
106
|
-
try {
|
|
107
|
-
this.entries.push({
|
|
108
|
-
step,
|
|
109
|
-
type,
|
|
110
|
-
filePath,
|
|
111
|
-
meta,
|
|
112
|
-
capturedAt: new Date().toISOString(),
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
// P5: non-blocking — swallow capture errors
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async flush() {
|
|
120
|
-
const manifestEntries = [];
|
|
121
|
-
let totalBytes = 0;
|
|
122
|
-
// Create the root output directory
|
|
123
|
-
mkdirSync(this.outputDir, { recursive: true });
|
|
124
|
-
for (const entry of this.entries) {
|
|
125
|
-
try {
|
|
126
|
-
const format = detectFormat(entry);
|
|
127
|
-
const ext = entry.filePath
|
|
128
|
-
? path.extname(entry.filePath)
|
|
129
|
-
: fileExtension(format);
|
|
130
|
-
const relativePath = path.join(entry.step, `${entry.type}${ext}`);
|
|
131
|
-
const absolutePath = path.join(this.outputDir, relativePath);
|
|
132
|
-
// Ensure step subdirectory exists
|
|
133
|
-
mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
134
|
-
// Write the artifact — JSON artifacts are redacted before persistence
|
|
135
|
-
let bytes;
|
|
136
|
-
if (entry.filePath && format === "json") {
|
|
137
|
-
// JSON file on disk: read → parse → redact → write
|
|
138
|
-
const raw = readFileSync(entry.filePath, "utf-8");
|
|
139
|
-
try {
|
|
140
|
-
const parsed = JSON.parse(raw);
|
|
141
|
-
const redacted = redactArtifactData(parsed);
|
|
142
|
-
const content = JSON.stringify(redacted, null, 2);
|
|
143
|
-
writeFileSync(absolutePath, content, "utf-8");
|
|
144
|
-
bytes = Buffer.byteLength(content, "utf-8");
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// Unparseable JSON — copy as-is (comparator will scan it later)
|
|
148
|
-
copyFileSync(entry.filePath, absolutePath);
|
|
149
|
-
bytes = statSync(absolutePath).size;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
else if (entry.filePath) {
|
|
153
|
-
// Non-JSON file: copy as-is
|
|
154
|
-
copyFileSync(entry.filePath, absolutePath);
|
|
155
|
-
bytes = statSync(absolutePath).size;
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
let content;
|
|
159
|
-
if (format === "json") {
|
|
160
|
-
try {
|
|
161
|
-
content = JSON.stringify(redactArtifactData(entry.data), null, 2);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Fallback: serialize unredacted (e.g., circular references)
|
|
165
|
-
content = JSON.stringify(entry.data, null, 2);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
content = String(entry.data ?? "");
|
|
170
|
-
}
|
|
171
|
-
writeFileSync(absolutePath, content, "utf-8");
|
|
172
|
-
bytes = Buffer.byteLength(content, "utf-8");
|
|
173
|
-
}
|
|
174
|
-
totalBytes += bytes;
|
|
175
|
-
manifestEntries.push({
|
|
176
|
-
step: entry.step,
|
|
177
|
-
type: entry.type,
|
|
178
|
-
path: relativePath,
|
|
179
|
-
capturedAt: entry.capturedAt,
|
|
180
|
-
bytes,
|
|
181
|
-
format,
|
|
182
|
-
...(entry.meta ? { meta: entry.meta } : {}),
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
// P5: skip individual artifact failures, continue with the rest
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// Write manifest
|
|
190
|
-
const manifest = {
|
|
191
|
-
version: 1,
|
|
192
|
-
captureId: this.captureId,
|
|
193
|
-
startedAt: this.startedAt,
|
|
194
|
-
completedAt: new Date().toISOString(),
|
|
195
|
-
pipeline: {
|
|
196
|
-
mode: this.options.mode,
|
|
197
|
-
...(this.options.pipeline?.variant
|
|
198
|
-
? { variant: this.options.pipeline.variant }
|
|
199
|
-
: {}),
|
|
200
|
-
...(this.options.pipeline?.source
|
|
201
|
-
? { source: this.options.pipeline.source }
|
|
202
|
-
: {}),
|
|
203
|
-
...(this.options.pipeline?.areas
|
|
204
|
-
? { areas: this.options.pipeline.areas }
|
|
205
|
-
: {}),
|
|
206
|
-
},
|
|
207
|
-
artifacts: manifestEntries,
|
|
208
|
-
};
|
|
209
|
-
const manifestContent = JSON.stringify(manifest, null, 2);
|
|
210
|
-
writeFileSync(path.join(this.outputDir, "manifest.json"), manifestContent, "utf-8");
|
|
211
|
-
// Compress to tar.gz if configured
|
|
212
|
-
if (this.options.compress) {
|
|
213
|
-
try {
|
|
214
|
-
const archivePath = `${this.outputDir}.tar.gz`;
|
|
215
|
-
const parentDir = path.dirname(this.outputDir);
|
|
216
|
-
const dirName = path.basename(this.outputDir);
|
|
217
|
-
execFileSync("tar", ["-czf", archivePath, "-C", parentDir, dirName]);
|
|
218
|
-
rmSync(this.outputDir, { recursive: true });
|
|
219
|
-
return {
|
|
220
|
-
artifactCount: manifestEntries.length,
|
|
221
|
-
destination: archivePath,
|
|
222
|
-
totalBytes,
|
|
223
|
-
compressed: true,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// Non-blocking: compression failed, keep the raw directory
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
artifactCount: manifestEntries.length,
|
|
232
|
-
destination: this.outputDir,
|
|
233
|
-
totalBytes,
|
|
234
|
-
compressed: false,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GcsArtifactCollector — decorator that uploads capture artifacts to GCS.
|
|
3
|
-
*
|
|
4
|
-
* Wraps the FilesystemArtifactCollector: local flush first (preserving
|
|
5
|
-
* the existing manifest + redaction logic), then upload to a GCS bucket.
|
|
6
|
-
*
|
|
7
|
-
* Design principles:
|
|
8
|
-
* - P5: Non-blocking — GCS upload failure should not block the pipeline.
|
|
9
|
-
* Local artifacts are always preserved.
|
|
10
|
-
* - Decorator pattern — delegates capture() and captureFile() to the inner
|
|
11
|
-
* collector unchanged. Only flush() adds the GCS upload step.
|
|
12
|
-
* - Lazy client — GCS Storage client is created on first flush(), not at
|
|
13
|
-
* construction (same pattern as BigQuerySink).
|
|
14
|
-
*
|
|
15
|
-
* @see docs/decisions/D0030-external-artifact-store.md
|
|
16
|
-
* @see docs/work-items/W0035-gcs-artifact-output.json
|
|
17
|
-
*/
|
|
18
|
-
import type { ArtifactCollector, CaptureFlushResult } from "../_vendor/ailf-core/index.d.ts";
|
|
19
|
-
export interface GcsCollectorOptions {
|
|
20
|
-
/** GCS bucket name (e.g., "ailf-artifacts") */
|
|
21
|
-
bucket: string;
|
|
22
|
-
/** Object prefix in the bucket (e.g., "captures/") */
|
|
23
|
-
prefix?: string;
|
|
24
|
-
/** Path to service account credentials JSON (optional — falls back to ADC) */
|
|
25
|
-
credentials?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface GcsFlushResult extends CaptureFlushResult {
|
|
28
|
-
/** GCS upload status */
|
|
29
|
-
gcs: {
|
|
30
|
-
status: "uploaded";
|
|
31
|
-
bucket: string;
|
|
32
|
-
path: string;
|
|
33
|
-
} | {
|
|
34
|
-
status: "skipped";
|
|
35
|
-
reason: string;
|
|
36
|
-
} | {
|
|
37
|
-
status: "failed";
|
|
38
|
-
error: string;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export declare class GcsArtifactCollector implements ArtifactCollector {
|
|
42
|
-
get enabled(): boolean;
|
|
43
|
-
get extrasEnabled(): boolean;
|
|
44
|
-
private client;
|
|
45
|
-
private readonly inner;
|
|
46
|
-
private readonly options;
|
|
47
|
-
constructor(inner: ArtifactCollector, options: GcsCollectorOptions);
|
|
48
|
-
capture(step: string, type: string, data: unknown, meta?: Record<string, unknown>): void;
|
|
49
|
-
captureFile(step: string, type: string, filePath: string, meta?: Record<string, unknown>): void;
|
|
50
|
-
flush(): Promise<GcsFlushResult>;
|
|
51
|
-
/** Lazily create the GCS Storage client. */
|
|
52
|
-
private getClient;
|
|
53
|
-
/** Upload the flushed artifact (tar.gz or directory) to GCS. */
|
|
54
|
-
private uploadToGcs;
|
|
55
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GcsArtifactCollector — decorator that uploads capture artifacts to GCS.
|
|
3
|
-
*
|
|
4
|
-
* Wraps the FilesystemArtifactCollector: local flush first (preserving
|
|
5
|
-
* the existing manifest + redaction logic), then upload to a GCS bucket.
|
|
6
|
-
*
|
|
7
|
-
* Design principles:
|
|
8
|
-
* - P5: Non-blocking — GCS upload failure should not block the pipeline.
|
|
9
|
-
* Local artifacts are always preserved.
|
|
10
|
-
* - Decorator pattern — delegates capture() and captureFile() to the inner
|
|
11
|
-
* collector unchanged. Only flush() adds the GCS upload step.
|
|
12
|
-
* - Lazy client — GCS Storage client is created on first flush(), not at
|
|
13
|
-
* construction (same pattern as BigQuerySink).
|
|
14
|
-
*
|
|
15
|
-
* @see docs/decisions/D0030-external-artifact-store.md
|
|
16
|
-
* @see docs/work-items/W0035-gcs-artifact-output.json
|
|
17
|
-
*/
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
19
|
-
import { Storage } from "@google-cloud/storage";
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Collector
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
export class GcsArtifactCollector {
|
|
24
|
-
get enabled() {
|
|
25
|
-
return this.inner.enabled;
|
|
26
|
-
}
|
|
27
|
-
get extrasEnabled() {
|
|
28
|
-
return this.inner.extrasEnabled;
|
|
29
|
-
}
|
|
30
|
-
client = null;
|
|
31
|
-
inner;
|
|
32
|
-
options;
|
|
33
|
-
constructor(inner, options) {
|
|
34
|
-
this.inner = inner;
|
|
35
|
-
this.options = options;
|
|
36
|
-
}
|
|
37
|
-
capture(step, type, data, meta) {
|
|
38
|
-
this.inner.capture(step, type, data, meta);
|
|
39
|
-
}
|
|
40
|
-
captureFile(step, type, filePath, meta) {
|
|
41
|
-
this.inner.captureFile(step, type, filePath, meta);
|
|
42
|
-
}
|
|
43
|
-
async flush() {
|
|
44
|
-
// Step 1: Flush to local filesystem first (always succeeds or throws)
|
|
45
|
-
const localResult = await this.inner.flush();
|
|
46
|
-
// Step 2: Upload to GCS (non-blocking — P5)
|
|
47
|
-
if (localResult.artifactCount === 0) {
|
|
48
|
-
return {
|
|
49
|
-
...localResult,
|
|
50
|
-
gcs: { status: "skipped", reason: "No artifacts to upload" },
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const gcsPath = await this.uploadToGcs(localResult);
|
|
55
|
-
return {
|
|
56
|
-
...localResult,
|
|
57
|
-
gcs: {
|
|
58
|
-
status: "uploaded",
|
|
59
|
-
bucket: this.options.bucket,
|
|
60
|
-
path: gcsPath,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
66
|
-
console.warn(` ⚠️ GCS upload failed (non-blocking): ${message}`);
|
|
67
|
-
return {
|
|
68
|
-
...localResult,
|
|
69
|
-
gcs: { status: "failed", error: message },
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// -----------------------------------------------------------------------
|
|
74
|
-
// Private helpers
|
|
75
|
-
// -----------------------------------------------------------------------
|
|
76
|
-
/** Lazily create the GCS Storage client. */
|
|
77
|
-
getClient() {
|
|
78
|
-
if (this.client)
|
|
79
|
-
return this.client;
|
|
80
|
-
this.client = this.options.credentials
|
|
81
|
-
? new Storage({ keyFilename: this.options.credentials })
|
|
82
|
-
: new Storage();
|
|
83
|
-
return this.client;
|
|
84
|
-
}
|
|
85
|
-
/** Upload the flushed artifact (tar.gz or directory) to GCS. */
|
|
86
|
-
async uploadToGcs(result) {
|
|
87
|
-
const storage = this.getClient();
|
|
88
|
-
const bucket = storage.bucket(this.options.bucket);
|
|
89
|
-
const prefix = this.options.prefix ?? "captures/";
|
|
90
|
-
if (result.compressed) {
|
|
91
|
-
// Upload the tar.gz directly
|
|
92
|
-
const fileName = result.destination.split("/").pop() ?? "capture.tar.gz";
|
|
93
|
-
const gcsPath = `${prefix}${fileName}`;
|
|
94
|
-
const fileContent = readFileSync(result.destination);
|
|
95
|
-
await bucket.file(gcsPath).save(fileContent, {
|
|
96
|
-
contentType: "application/gzip",
|
|
97
|
-
metadata: {
|
|
98
|
-
artifactCount: String(result.artifactCount),
|
|
99
|
-
totalBytes: String(result.totalBytes),
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
return gcsPath;
|
|
103
|
-
}
|
|
104
|
-
// Uncompressed: upload the manifest.json as the representative file.
|
|
105
|
-
// The full directory could be uploaded file-by-file, but for the
|
|
106
|
-
// capture use case (forensic archive), the compressed bundle is the
|
|
107
|
-
// expected path. Upload just the manifest as a reference.
|
|
108
|
-
const manifestPath = `${result.destination}/manifest.json`;
|
|
109
|
-
const dirName = result.destination.split("/").pop() ?? "capture";
|
|
110
|
-
const gcsPath = `${prefix}${dirName}/manifest.json`;
|
|
111
|
-
const manifestContent = readFileSync(manifestPath, "utf-8");
|
|
112
|
-
await bucket.file(gcsPath).save(manifestContent, {
|
|
113
|
-
contentType: "application/json",
|
|
114
|
-
});
|
|
115
|
-
return gcsPath;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* capture compare — compare two pipeline capture directories.
|
|
3
|
-
*
|
|
4
|
-
* Reads manifest.json from both captures, runs compareCaptures(),
|
|
5
|
-
* and prints a human-readable table or JSON diff report.
|
|
6
|
-
*
|
|
7
|
-
* Supports both raw directories and .tar.gz archives.
|
|
8
|
-
*
|
|
9
|
-
* Exit codes:
|
|
10
|
-
* 0 — captures are equivalent
|
|
11
|
-
* 1 — differences found
|
|
12
|
-
* 2 — error (missing files, invalid manifest, etc.)
|
|
13
|
-
*/
|
|
14
|
-
import { Command } from "commander";
|
|
15
|
-
export declare function createCaptureCompareCommand(): Command;
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* capture compare — compare two pipeline capture directories.
|
|
3
|
-
*
|
|
4
|
-
* Reads manifest.json from both captures, runs compareCaptures(),
|
|
5
|
-
* and prints a human-readable table or JSON diff report.
|
|
6
|
-
*
|
|
7
|
-
* Supports both raw directories and .tar.gz archives.
|
|
8
|
-
*
|
|
9
|
-
* Exit codes:
|
|
10
|
-
* 0 — captures are equivalent
|
|
11
|
-
* 1 — differences found
|
|
12
|
-
* 2 — error (missing files, invalid manifest, etc.)
|
|
13
|
-
*/
|
|
14
|
-
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
import { join, resolve } from "node:path";
|
|
18
|
-
import { Command, Option } from "commander";
|
|
19
|
-
import { compareCaptures } from "../artifact-capture/comparator.js";
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Command factory
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
export function createCaptureCompareCommand() {
|
|
24
|
-
return new Command("compare")
|
|
25
|
-
.description("Compare two pipeline capture directories")
|
|
26
|
-
.argument("<baseline>", "Path to baseline capture (directory or .tar.gz)")
|
|
27
|
-
.argument("<experiment>", "Path to experiment capture (directory or .tar.gz)")
|
|
28
|
-
.addOption(new Option("-m, --mode <mode>", "Comparison mode")
|
|
29
|
-
.choices(["inventory", "structural", "strict"])
|
|
30
|
-
.default("inventory"))
|
|
31
|
-
.option("-f, --format <fmt>", "Output format: table or json", "table")
|
|
32
|
-
.option("-o, --output <path>", "Write JSON report to file")
|
|
33
|
-
.option("--score-threshold <n>", "Aggregate score regression threshold (points)", parseFloat, 5)
|
|
34
|
-
.option("--task-threshold <n>", "Per-task score regression threshold (points)", parseFloat, 10)
|
|
35
|
-
.option("--timing-threshold <n>", "Step timing multiplier threshold", parseFloat, 2)
|
|
36
|
-
.option("--json-depth <n>", "JSON structural diff depth", parseInt, 3)
|
|
37
|
-
.action(async (baselinePath, experimentPath, opts) => {
|
|
38
|
-
const cleanups = [];
|
|
39
|
-
try {
|
|
40
|
-
const baseline = resolveCapturePath(resolve(baselinePath), cleanups);
|
|
41
|
-
const experiment = resolveCapturePath(resolve(experimentPath), cleanups);
|
|
42
|
-
console.log("");
|
|
43
|
-
console.log(" ailf capture compare");
|
|
44
|
-
console.log(" " + "─".repeat(40));
|
|
45
|
-
console.log("");
|
|
46
|
-
console.log(` Baseline: ${baselinePath}`);
|
|
47
|
-
console.log(` Experiment: ${experimentPath}`);
|
|
48
|
-
console.log(` Mode: ${opts.mode}`);
|
|
49
|
-
console.log("");
|
|
50
|
-
const report = compareCaptures(baseline, experiment, {
|
|
51
|
-
mode: opts.mode,
|
|
52
|
-
scoreThresholds: {
|
|
53
|
-
aggregate: opts.scoreThreshold,
|
|
54
|
-
perTask: opts.taskThreshold,
|
|
55
|
-
},
|
|
56
|
-
timingThresholds: { multiplier: opts.timingThreshold },
|
|
57
|
-
jsonDiffDepth: opts.jsonDepth,
|
|
58
|
-
});
|
|
59
|
-
if (opts.format === "json") {
|
|
60
|
-
const json = JSON.stringify(report, null, 2);
|
|
61
|
-
if (opts.output) {
|
|
62
|
-
writeFileSync(opts.output, json, "utf-8");
|
|
63
|
-
console.log(` Report written to ${opts.output}`);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
console.log(json);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
printTableReport(report);
|
|
71
|
-
if (opts.output) {
|
|
72
|
-
const json = JSON.stringify(report, null, 2);
|
|
73
|
-
writeFileSync(opts.output, json, "utf-8");
|
|
74
|
-
console.log(` Report also written to ${opts.output}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
process.exitCode = report.equivalent ? 0 : 1;
|
|
78
|
-
}
|
|
79
|
-
catch (err) {
|
|
80
|
-
console.error(` Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
-
process.exitCode = 2;
|
|
82
|
-
}
|
|
83
|
-
finally {
|
|
84
|
-
for (const cleanup of cleanups) {
|
|
85
|
-
try {
|
|
86
|
-
cleanup();
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// Best-effort cleanup
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Path resolution (handles tar.gz, subdirectories, raw dirs)
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
function resolveCapturePath(inputPath, cleanups) {
|
|
99
|
-
if (!existsSync(inputPath)) {
|
|
100
|
-
throw new Error(`Path does not exist: ${inputPath}`);
|
|
101
|
-
}
|
|
102
|
-
if (inputPath.endsWith(".tar.gz")) {
|
|
103
|
-
const tempDir = mkdtempSync(join(tmpdir(), "ailf-capture-cmp-"));
|
|
104
|
-
cleanups.push(() => rmSync(tempDir, { recursive: true, force: true }));
|
|
105
|
-
execFileSync("tar", ["-xzf", inputPath, "-C", tempDir]);
|
|
106
|
-
return findManifestDir(tempDir);
|
|
107
|
-
}
|
|
108
|
-
return findManifestDir(inputPath);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Find the directory containing manifest.json.
|
|
112
|
-
*
|
|
113
|
-
* Handles two cases:
|
|
114
|
-
* 1. Path IS the capture dir (contains manifest.json directly)
|
|
115
|
-
* 2. Path is the parent captures/ dir (contains a single timestamped subdir)
|
|
116
|
-
*/
|
|
117
|
-
function findManifestDir(dir) {
|
|
118
|
-
if (existsSync(join(dir, "manifest.json")))
|
|
119
|
-
return dir;
|
|
120
|
-
// Look one level down for a capture subdirectory
|
|
121
|
-
const entries = readdirSync(dir).filter((e) => !e.startsWith(".") && !e.endsWith(".tar.gz"));
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
const sub = join(dir, entry);
|
|
124
|
-
if (existsSync(join(sub, "manifest.json")))
|
|
125
|
-
return sub;
|
|
126
|
-
}
|
|
127
|
-
throw new Error(`No manifest.json found in ${dir} or its subdirectories. ` +
|
|
128
|
-
`Is this a valid capture directory?`);
|
|
129
|
-
}
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// Table formatting
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
function printTableReport(report) {
|
|
134
|
-
// Inventory
|
|
135
|
-
console.log(" Inventory");
|
|
136
|
-
console.log(" " + "─".repeat(40));
|
|
137
|
-
console.log(` Common: ${report.inventory.common.length} artifact(s)`);
|
|
138
|
-
if (report.inventory.added.length > 0) {
|
|
139
|
-
console.log(` Added: ${report.inventory.added.length} (${report.inventory.added.join(", ")})`);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
console.log(" Added: 0");
|
|
143
|
-
}
|
|
144
|
-
if (report.inventory.removed.length > 0) {
|
|
145
|
-
console.log(` Removed: ${report.inventory.removed.length} (${report.inventory.removed.join(", ")})`);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
console.log(" Removed: 0");
|
|
149
|
-
}
|
|
150
|
-
console.log("");
|
|
151
|
-
// Content diff preview (structural/strict modes)
|
|
152
|
-
if (report.content && report.content.length > 0) {
|
|
153
|
-
console.log(" Content Changes");
|
|
154
|
-
console.log(" " + "─".repeat(40));
|
|
155
|
-
for (const diff of report.content.slice(0, 10)) {
|
|
156
|
-
printContentDiff(diff);
|
|
157
|
-
}
|
|
158
|
-
if (report.content.length > 10) {
|
|
159
|
-
console.log(` ... and ${report.content.length - 10} more changed artifact(s)`);
|
|
160
|
-
}
|
|
161
|
-
console.log("");
|
|
162
|
-
}
|
|
163
|
-
// Scores
|
|
164
|
-
if (report.scores) {
|
|
165
|
-
console.log(" Scores");
|
|
166
|
-
console.log(" " + "─".repeat(40));
|
|
167
|
-
const { baselineMean, currentMean, delta } = report.scores;
|
|
168
|
-
const icon = delta > 0 ? "+" : delta < 0 ? "" : " ";
|
|
169
|
-
console.log(` Aggregate: ${baselineMean.toFixed(1)} -> ${currentMean.toFixed(1)} (${icon}${delta.toFixed(1)})`);
|
|
170
|
-
if (report.scores.breaches.length > 0) {
|
|
171
|
-
console.log(` Breaches: ${report.scores.breaches.length}`);
|
|
172
|
-
for (const b of report.scores.breaches) {
|
|
173
|
-
console.log(` - ${b}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
console.log(" Breaches: none");
|
|
178
|
-
}
|
|
179
|
-
console.log("");
|
|
180
|
-
}
|
|
181
|
-
// Timing
|
|
182
|
-
if (report.timing) {
|
|
183
|
-
console.log(" Timing");
|
|
184
|
-
console.log(" " + "─".repeat(40));
|
|
185
|
-
const { totalDeltaMs } = report.timing;
|
|
186
|
-
const sign = totalDeltaMs >= 0 ? "+" : "";
|
|
187
|
-
console.log(` Total delta: ${sign}${totalDeltaMs}ms`);
|
|
188
|
-
if (report.timing.breaches.length > 0) {
|
|
189
|
-
console.log(` Breaches: ${report.timing.breaches.length}`);
|
|
190
|
-
for (const b of report.timing.breaches) {
|
|
191
|
-
console.log(` - ${b}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
console.log(" Breaches: none");
|
|
196
|
-
}
|
|
197
|
-
console.log("");
|
|
198
|
-
}
|
|
199
|
-
// Security
|
|
200
|
-
console.log(" Security");
|
|
201
|
-
console.log(" " + "─".repeat(40));
|
|
202
|
-
if (report.security.leaksFound) {
|
|
203
|
-
console.log(` Leaks: ${report.security.violations.length} finding(s)`);
|
|
204
|
-
for (const v of report.security.violations.slice(0, 5)) {
|
|
205
|
-
console.log(` - ${v.file}: ${v.detail}`);
|
|
206
|
-
}
|
|
207
|
-
if (report.security.violations.length > 5) {
|
|
208
|
-
console.log(` ... and ${report.security.violations.length - 5} more`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
console.log(" Leaks: none");
|
|
213
|
-
}
|
|
214
|
-
console.log("");
|
|
215
|
-
// Result
|
|
216
|
-
if (report.equivalent) {
|
|
217
|
-
console.log(" Result: EQUIVALENT");
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
console.log(` Result: DIFFERENCES FOUND`);
|
|
221
|
-
console.log(` ${report.summary}`);
|
|
222
|
-
}
|
|
223
|
-
console.log("");
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Print up to 3 changed key paths for a content diff.
|
|
227
|
-
*/
|
|
228
|
-
function printContentDiff(diff) {
|
|
229
|
-
console.log(` ${diff.artifactKey} (${diff.format})`);
|
|
230
|
-
if (Array.isArray(diff.changes)) {
|
|
231
|
-
// JSON diff — show up to 3 changed paths
|
|
232
|
-
const jsonChanges = diff.changes;
|
|
233
|
-
for (const change of jsonChanges.slice(0, 3)) {
|
|
234
|
-
if (change.baseline === undefined) {
|
|
235
|
-
console.log(` + ${change.path} (added)`);
|
|
236
|
-
}
|
|
237
|
-
else if (change.experiment === undefined) {
|
|
238
|
-
console.log(` - ${change.path} (removed)`);
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
console.log(` ~ ${change.path} (changed)`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (jsonChanges.length > 3) {
|
|
245
|
-
console.log(` ... ${jsonChanges.length - 3} more change(s)`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
// Text/markdown diff
|
|
250
|
-
const { addedLines, removedLines } = diff.changes;
|
|
251
|
-
console.log(` +${addedLines} / -${removedLines} lines`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* capture list — list pipeline captures in a directory.
|
|
3
|
-
*
|
|
4
|
-
* Scans a capture directory for subdirectories containing manifest.json,
|
|
5
|
-
* reads each manifest, and prints a summary table sorted by date.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* ailf capture list # default: .ailf/results/captures/
|
|
9
|
-
* ailf capture list ./my-captures # custom directory
|
|
10
|
-
*/
|
|
11
|
-
import { Command } from "commander";
|
|
12
|
-
export declare function createCaptureListCommand(): Command;
|