@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.
Files changed (91) hide show
  1. package/dist/_vendor/ailf-core/artifact-capture/association.d.ts +35 -0
  2. package/dist/_vendor/ailf-core/artifact-capture/association.js +28 -0
  3. package/dist/_vendor/ailf-core/artifact-registry.d.ts +124 -23
  4. package/dist/_vendor/ailf-core/artifact-registry.js +708 -64
  5. package/dist/_vendor/ailf-core/batch-signing.d.ts +64 -0
  6. package/dist/_vendor/ailf-core/batch-signing.js +23 -0
  7. package/dist/_vendor/ailf-core/index.d.ts +3 -2
  8. package/dist/_vendor/ailf-core/index.js +3 -2
  9. package/dist/_vendor/ailf-core/ports/artifact-writer.d.ts +59 -20
  10. package/dist/_vendor/ailf-core/ports/artifact-writer.js +33 -10
  11. package/dist/_vendor/ailf-core/ports/context.d.ts +20 -17
  12. package/dist/_vendor/ailf-core/ports/index.d.ts +0 -2
  13. package/dist/_vendor/ailf-core/schemas/pipeline.d.ts +6 -6
  14. package/dist/_vendor/ailf-core/services/index.d.ts +1 -0
  15. package/dist/_vendor/ailf-core/services/index.js +1 -0
  16. package/dist/_vendor/ailf-core/services/slim-report-summary.d.ts +31 -0
  17. package/dist/_vendor/ailf-core/services/slim-report-summary.js +217 -0
  18. package/dist/_vendor/ailf-core/types/branded-ids.d.ts +33 -0
  19. package/dist/_vendor/ailf-core/types/index.d.ts +202 -23
  20. package/dist/adapters/config-sources/file-config-adapter.js +0 -4
  21. package/dist/artifact-capture/accumulating-artifact-writer.d.ts +50 -0
  22. package/dist/artifact-capture/accumulating-artifact-writer.js +111 -0
  23. package/dist/artifact-capture/api-gateway-artifact-writer.d.ts +17 -4
  24. package/dist/artifact-capture/api-gateway-artifact-writer.js +58 -7
  25. package/dist/artifact-capture/emit-file.d.ts +28 -0
  26. package/dist/artifact-capture/emit-file.js +56 -0
  27. package/dist/artifact-capture/fanout-artifact-writer.d.ts +39 -0
  28. package/dist/artifact-capture/fanout-artifact-writer.js +76 -0
  29. package/dist/artifact-capture/gcs-artifact-writer.d.ts +40 -3
  30. package/dist/artifact-capture/gcs-artifact-writer.js +238 -14
  31. package/dist/artifact-capture/local-fs-artifact-writer.d.ts +71 -0
  32. package/dist/artifact-capture/local-fs-artifact-writer.js +273 -0
  33. package/dist/artifact-capture/redact-artifact.d.ts +3 -5
  34. package/dist/artifact-capture/redact-artifact.js +3 -5
  35. package/dist/cli.js +56 -2
  36. package/dist/commands/explain-handler.js +4 -4
  37. package/dist/commands/pipeline-action.d.ts +5 -4
  38. package/dist/commands/pipeline-action.js +33 -16
  39. package/dist/commands/pipeline.d.ts +4 -4
  40. package/dist/commands/pipeline.js +4 -4
  41. package/dist/commands/publish.js +4 -1
  42. package/dist/commands/runs.d.ts +18 -0
  43. package/dist/commands/runs.js +71 -0
  44. package/dist/composition-root.d.ts +13 -10
  45. package/dist/composition-root.js +74 -46
  46. package/dist/orchestration/build-app-context.js +4 -7
  47. package/dist/orchestration/pipeline-orchestrator.d.ts +1 -1
  48. package/dist/orchestration/pipeline-orchestrator.js +37 -46
  49. package/dist/orchestration/steps/calculate-scores-step.d.ts +1 -1
  50. package/dist/orchestration/steps/calculate-scores-step.js +19 -19
  51. package/dist/orchestration/steps/callback-step.d.ts +1 -1
  52. package/dist/orchestration/steps/callback-step.js +6 -4
  53. package/dist/orchestration/steps/compare-step.d.ts +1 -1
  54. package/dist/orchestration/steps/compare-step.js +4 -2
  55. package/dist/orchestration/steps/discovery-report-step.d.ts +1 -1
  56. package/dist/orchestration/steps/discovery-report-step.js +4 -1
  57. package/dist/orchestration/steps/fetch-docs-step.js +9 -15
  58. package/dist/orchestration/steps/finalize-run-step.js +21 -7
  59. package/dist/orchestration/steps/gap-analysis-step.js +34 -6
  60. package/dist/orchestration/steps/generate-configs-step.d.ts +1 -1
  61. package/dist/orchestration/steps/generate-configs-step.js +11 -11
  62. package/dist/orchestration/steps/publish-report-step.d.ts +1 -1
  63. package/dist/orchestration/steps/publish-report-step.js +24 -19
  64. package/dist/orchestration/steps/readiness-step.d.ts +1 -1
  65. package/dist/orchestration/steps/readiness-step.js +4 -1
  66. package/dist/orchestration/steps/report-step.d.ts +1 -1
  67. package/dist/orchestration/steps/report-step.js +6 -3
  68. package/dist/orchestration/steps/run-eval-step.js +14 -9
  69. package/dist/pipeline/compare.d.ts +2 -2
  70. package/dist/pipeline/emit-eval-results.d.ts +38 -0
  71. package/dist/pipeline/emit-eval-results.js +100 -0
  72. package/dist/pipeline/map-request-to-config.js +0 -4
  73. package/package.json +1 -1
  74. package/dist/_vendor/ailf-core/artifact-capture/noop-collector.d.ts +0 -14
  75. package/dist/_vendor/ailf-core/artifact-capture/noop-collector.js +0 -25
  76. package/dist/_vendor/ailf-core/ports/artifact-collector.d.ts +0 -94
  77. package/dist/_vendor/ailf-core/ports/artifact-collector.js +0 -13
  78. package/dist/_vendor/ailf-core/ports/capture-comparator.d.ts +0 -138
  79. package/dist/_vendor/ailf-core/ports/capture-comparator.js +0 -10
  80. package/dist/artifact-capture/comparator.d.ts +0 -22
  81. package/dist/artifact-capture/comparator.js +0 -493
  82. package/dist/artifact-capture/filesystem-collector.d.ts +0 -42
  83. package/dist/artifact-capture/filesystem-collector.js +0 -237
  84. package/dist/artifact-capture/gcs-collector.d.ts +0 -55
  85. package/dist/artifact-capture/gcs-collector.js +0 -117
  86. package/dist/commands/capture-compare.d.ts +0 -15
  87. package/dist/commands/capture-compare.js +0 -253
  88. package/dist/commands/capture-list.d.ts +0 -12
  89. package/dist/commands/capture-list.js +0 -150
  90. package/dist/commands/capture.d.ts +0 -9
  91. 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;