@sanity/ailf 6.1.0 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_vendor/ailf-core/artifact-registry.d.ts +17 -0
- package/dist/_vendor/ailf-core/artifact-registry.js +14 -0
- package/dist/_vendor/ailf-core/ports/context.d.ts +10 -0
- package/dist/artifact-capture/api-gateway-artifact-writer.d.ts +11 -1
- package/dist/artifact-capture/api-gateway-artifact-writer.js +3 -1
- package/dist/artifact-capture/batching-api-gateway-artifact-writer.d.ts +11 -1
- package/dist/artifact-capture/batching-api-gateway-artifact-writer.js +3 -1
- package/dist/artifact-capture/gcs-artifact-writer.d.ts +11 -1
- package/dist/artifact-capture/gcs-artifact-writer.js +6 -3
- package/dist/artifact-capture/local-fs-artifact-writer.d.ts +11 -1
- package/dist/artifact-capture/local-fs-artifact-writer.js +6 -3
- package/dist/composition-root.d.ts +22 -5
- package/dist/composition-root.js +78 -8
- package/dist/orchestration/steps/gap-analysis-step.js +0 -1
- package/dist/report-store.d.ts +15 -1
- package/dist/report-store.js +25 -0
- package/package.json +1 -1
|
@@ -200,6 +200,23 @@ export interface ArtifactDescriptor<TEntry = unknown, TPreview = unknown> {
|
|
|
200
200
|
* descriptors. `entryKey` is ignored on versioned bulk paths.
|
|
201
201
|
*/
|
|
202
202
|
readonly objectPath: ArtifactObjectPath;
|
|
203
|
+
/**
|
|
204
|
+
* Extract the positional args (beyond `runId`) to pass to `objectPath`
|
|
205
|
+
* when this descriptor needs more than `runId` to build its path.
|
|
206
|
+
*
|
|
207
|
+
* The default plain-bulk path is `objectPath(runId)`. The default
|
|
208
|
+
* per-entry path is `objectPath(runId, entryKey)`. Descriptors whose
|
|
209
|
+
* path needs additional axes from `association` and/or fields from the
|
|
210
|
+
* payload (e.g. the bulk-versioned-with-report-axis carve-out used by
|
|
211
|
+
* the diagnosis descriptor — see `BULK_VERSIONED_WITH_REPORT_AXIS`)
|
|
212
|
+
* set this function. The writer calls it and spreads the result:
|
|
213
|
+
* `objectPath(runId, ...extractPathArgs(association, payload))`.
|
|
214
|
+
*
|
|
215
|
+
* Returning `[undefined]` (or any `undefined` element) is fine — the
|
|
216
|
+
* underlying builder is expected to throw a meaningful error in that
|
|
217
|
+
* case, which the writer surfaces via its existing try/catch.
|
|
218
|
+
*/
|
|
219
|
+
readonly extractPathArgs?: (association: AssociationValues, payload: unknown) => readonly (string | undefined)[];
|
|
203
220
|
/**
|
|
204
221
|
* Build a filename-safe entry key from association values. Only meaningful
|
|
205
222
|
* for `layout === "per-entry"` — bulk descriptors omit it.
|
|
@@ -674,6 +674,7 @@ function buildDescriptor(input) {
|
|
|
674
674
|
versionedBy: input.versionedBy,
|
|
675
675
|
pathSafetyMarker: input.pathSafetyMarker,
|
|
676
676
|
objectPath,
|
|
677
|
+
extractPathArgs: input.extractPathArgs,
|
|
677
678
|
formatEntryKey,
|
|
678
679
|
parseEntryKey,
|
|
679
680
|
manifestPreview: input.manifestPreview,
|
|
@@ -1185,6 +1186,19 @@ export const ARTIFACT_REGISTRY = {
|
|
|
1185
1186
|
writePolicy: "post-hoc",
|
|
1186
1187
|
versionedBy: "diagnosisVersion",
|
|
1187
1188
|
objectPath: diagnosisPathBuilder(),
|
|
1189
|
+
// The diagnosis path builder takes (runId, reportId, version). The
|
|
1190
|
+
// reportId comes from association.report; the version is a compound
|
|
1191
|
+
// `diagnosisVersion|cardVersion` synthesized from the payload's
|
|
1192
|
+
// `inputs` field via `encodeDiagnosisPathVersion`. Writers spread
|
|
1193
|
+
// this beyond `runId` when calling `objectPath`.
|
|
1194
|
+
extractPathArgs: (assoc, payload) => {
|
|
1195
|
+
const reportId = typeof assoc.report === "string" ? assoc.report : undefined;
|
|
1196
|
+
const diag = payload;
|
|
1197
|
+
const dv = diag?.inputs?.diagnosisVersion;
|
|
1198
|
+
const cv = diag?.inputs?.cardVersion;
|
|
1199
|
+
const version = dv && cv ? encodeDiagnosisPathVersion(dv, cv) : undefined;
|
|
1200
|
+
return [reportId, version];
|
|
1201
|
+
},
|
|
1188
1202
|
// Defense-in-depth: this descriptor's axes (`run`, `report`) are both
|
|
1189
1203
|
// bounded, so the `assertValidArtifactDescriptor` unbounded-axis rule
|
|
1190
1204
|
// does not fire and the carve-out is never consulted at module load
|
|
@@ -343,6 +343,16 @@ export interface ReportStorePort {
|
|
|
343
343
|
read(id: string): Promise<null | unknown>;
|
|
344
344
|
/** Patch synthesis telemetry onto a published report (Phase 6 / DIAG-06). */
|
|
345
345
|
patchSynthesis(id: string, telemetry: unknown): Promise<void>;
|
|
346
|
+
/**
|
|
347
|
+
* Patch a single artifact-manifest entry onto a published report.
|
|
348
|
+
*
|
|
349
|
+
* Used by deferred commands (e.g. `ailf interpret`) whose post-hoc writer
|
|
350
|
+
* produces a new ArtifactRef after the doc was already published. The
|
|
351
|
+
* pipeline path lifts the full manifest at publish time
|
|
352
|
+
* (publish-report-step); this is the post-hoc equivalent for one slot.
|
|
353
|
+
* Non-fatal on Sanity failure — mirrors `patchSynthesis`.
|
|
354
|
+
*/
|
|
355
|
+
patchArtifactManifest(id: string, slot: string, ref: unknown): Promise<void>;
|
|
346
356
|
}
|
|
347
357
|
/**
|
|
348
358
|
* Minimal report sink interface used by AppContext.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* @see docs/decisions/D0032-run-anchored-artifact-store.md
|
|
28
28
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
|
|
29
29
|
*/
|
|
30
|
-
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
30
|
+
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest, type WriteSource } from "../_vendor/ailf-core/index.d.ts";
|
|
31
31
|
import { type UploadMetricsSink } from "./upload-metrics.js";
|
|
32
32
|
export interface ApiGatewayArtifactWriterOptions {
|
|
33
33
|
/** Base URL of the API gateway (e.g., "https://ailf-api.sanity.build"). */
|
|
@@ -41,10 +41,20 @@ export interface ApiGatewayArtifactWriterOptions {
|
|
|
41
41
|
* Defaults to a no-op so the hot path stays free when metrics are off.
|
|
42
42
|
*/
|
|
43
43
|
metrics?: UploadMetricsSink;
|
|
44
|
+
/**
|
|
45
|
+
* Identifies what kind of execution context owns this writer instance.
|
|
46
|
+
* The D0050 guard refuses to emit a descriptor whose `writePolicy` is
|
|
47
|
+
* different from this value. `"pipeline"` (the default) is for writers
|
|
48
|
+
* driven by an `ailf run` pipeline; `"post-hoc"` is for writers driven
|
|
49
|
+
* by deferred commands like `ailf interpret` that emit descriptors
|
|
50
|
+
* declared `writePolicy: "post-hoc"` (D0050).
|
|
51
|
+
*/
|
|
52
|
+
writerSource?: WriteSource;
|
|
44
53
|
}
|
|
45
54
|
export declare class ApiGatewayArtifactWriter implements ArtifactWriter {
|
|
46
55
|
private readonly options;
|
|
47
56
|
private readonly metrics;
|
|
57
|
+
private readonly writerSource;
|
|
48
58
|
constructor(options: ApiGatewayArtifactWriterOptions);
|
|
49
59
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
50
60
|
appendNdjson(): Promise<ArtifactRef | null>;
|
|
@@ -33,14 +33,16 @@ import { NO_OP_UPLOAD_METRICS, } from "./upload-metrics.js";
|
|
|
33
33
|
export class ApiGatewayArtifactWriter {
|
|
34
34
|
options;
|
|
35
35
|
metrics;
|
|
36
|
+
writerSource;
|
|
36
37
|
constructor(options) {
|
|
37
38
|
this.options = options;
|
|
38
39
|
this.metrics = options.metrics ?? NO_OP_UPLOAD_METRICS;
|
|
40
|
+
this.writerSource = options.writerSource ?? "pipeline";
|
|
39
41
|
}
|
|
40
42
|
// ---- Canonical W0049 API ------------------------------------------------
|
|
41
43
|
async emit(type, association, payload) {
|
|
42
44
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
43
|
-
assertWritePolicyMatches(
|
|
45
|
+
assertWritePolicyMatches(this.writerSource, descriptor);
|
|
44
46
|
const runId = association.run;
|
|
45
47
|
if (!runId) {
|
|
46
48
|
console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* does this writer. Traces flow through the GCS-direct writer when ADC
|
|
26
26
|
* credentials are present.
|
|
27
27
|
*/
|
|
28
|
-
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
28
|
+
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type AssociationValues, type RunId, type RunManifest, type WriteSource } from "../_vendor/ailf-core/index.d.ts";
|
|
29
29
|
import { type UploadMetricsSink } from "./upload-metrics.js";
|
|
30
30
|
export interface BatchingApiGatewayArtifactWriterOptions {
|
|
31
31
|
/** Base URL of the API gateway (e.g., "https://ailf-api.sanity.build"). */
|
|
@@ -46,12 +46,22 @@ export interface BatchingApiGatewayArtifactWriterOptions {
|
|
|
46
46
|
putConcurrency?: number;
|
|
47
47
|
/** Optional metrics sink; defaults to no-op. */
|
|
48
48
|
metrics?: UploadMetricsSink;
|
|
49
|
+
/**
|
|
50
|
+
* Identifies what kind of execution context owns this writer instance.
|
|
51
|
+
* The D0050 guard refuses to emit a descriptor whose `writePolicy` is
|
|
52
|
+
* different from this value. `"pipeline"` (the default) is for writers
|
|
53
|
+
* driven by an `ailf run` pipeline; `"post-hoc"` is for writers driven
|
|
54
|
+
* by deferred commands like `ailf interpret` that emit descriptors
|
|
55
|
+
* declared `writePolicy: "post-hoc"` (D0050).
|
|
56
|
+
*/
|
|
57
|
+
writerSource?: WriteSource;
|
|
49
58
|
}
|
|
50
59
|
export declare class BatchingApiGatewayArtifactWriter implements ArtifactWriter {
|
|
51
60
|
private readonly options;
|
|
52
61
|
private readonly pending;
|
|
53
62
|
private flushing;
|
|
54
63
|
private microtaskScheduled;
|
|
64
|
+
private readonly writerSource;
|
|
55
65
|
constructor(options: BatchingApiGatewayArtifactWriterOptions);
|
|
56
66
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
57
67
|
appendNdjson(): Promise<ArtifactRef | null>;
|
|
@@ -51,6 +51,7 @@ export class BatchingApiGatewayArtifactWriter {
|
|
|
51
51
|
pending = [];
|
|
52
52
|
flushing = null;
|
|
53
53
|
microtaskScheduled = false;
|
|
54
|
+
writerSource;
|
|
54
55
|
constructor(options) {
|
|
55
56
|
this.options = {
|
|
56
57
|
apiBaseUrl: options.apiBaseUrl,
|
|
@@ -60,11 +61,12 @@ export class BatchingApiGatewayArtifactWriter {
|
|
|
60
61
|
putConcurrency: options.putConcurrency ?? DEFAULT_PUT_CONCURRENCY,
|
|
61
62
|
metrics: options.metrics ?? NO_OP_UPLOAD_METRICS,
|
|
62
63
|
};
|
|
64
|
+
this.writerSource = options.writerSource ?? "pipeline";
|
|
63
65
|
}
|
|
64
66
|
// ---- ArtifactWriter surface --------------------------------------------
|
|
65
67
|
async emit(type, association, payload) {
|
|
66
68
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
67
|
-
assertWritePolicyMatches(
|
|
69
|
+
assertWritePolicyMatches(this.writerSource, descriptor);
|
|
68
70
|
const runId = association.run;
|
|
69
71
|
if (!runId) {
|
|
70
72
|
console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
|
|
29
29
|
*/
|
|
30
30
|
import { Storage } from "@google-cloud/storage";
|
|
31
|
-
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
31
|
+
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssociationValues, type RunId, type RunManifest, type WriteSource } from "../_vendor/ailf-core/index.d.ts";
|
|
32
32
|
import { type UploadMetricsSink } from "./upload-metrics.js";
|
|
33
33
|
export interface GcsArtifactWriterOptions {
|
|
34
34
|
/** GCS bucket name (e.g., "ailf-artifacts") */
|
|
@@ -51,6 +51,15 @@ export interface GcsArtifactWriterOptions {
|
|
|
51
51
|
* Defaults to a no-op so the hot path stays free when metrics are off.
|
|
52
52
|
*/
|
|
53
53
|
metrics?: UploadMetricsSink;
|
|
54
|
+
/**
|
|
55
|
+
* Identifies what kind of execution context owns this writer instance.
|
|
56
|
+
* The D0050 guard refuses to emit a descriptor whose `writePolicy` is
|
|
57
|
+
* different from this value. `"pipeline"` (the default) is for writers
|
|
58
|
+
* driven by an `ailf run` pipeline; `"post-hoc"` is for writers driven
|
|
59
|
+
* by deferred commands like `ailf interpret` that emit descriptors
|
|
60
|
+
* declared `writePolicy: "post-hoc"` (D0050).
|
|
61
|
+
*/
|
|
62
|
+
writerSource?: WriteSource;
|
|
54
63
|
}
|
|
55
64
|
export declare class GcsArtifactWriter implements ArtifactWriter {
|
|
56
65
|
private client;
|
|
@@ -68,6 +77,7 @@ export declare class GcsArtifactWriter implements ArtifactWriter {
|
|
|
68
77
|
* measured safe, without forcing the producer to own that knob.
|
|
69
78
|
*/
|
|
70
79
|
private readonly limiter;
|
|
80
|
+
private readonly writerSource;
|
|
71
81
|
constructor(options: GcsArtifactWriterOptions);
|
|
72
82
|
private reportProgress;
|
|
73
83
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
@@ -57,9 +57,11 @@ export class GcsArtifactWriter {
|
|
|
57
57
|
* measured safe, without forcing the producer to own that knob.
|
|
58
58
|
*/
|
|
59
59
|
limiter;
|
|
60
|
+
writerSource;
|
|
60
61
|
constructor(options) {
|
|
61
62
|
this.options = options;
|
|
62
63
|
this.metrics = options.metrics ?? NO_OP_UPLOAD_METRICS;
|
|
64
|
+
this.writerSource = options.writerSource ?? "pipeline";
|
|
63
65
|
if (options.storage) {
|
|
64
66
|
this.client = options.storage;
|
|
65
67
|
}
|
|
@@ -79,7 +81,7 @@ export class GcsArtifactWriter {
|
|
|
79
81
|
// ---- Canonical W0049 API ------------------------------------------------
|
|
80
82
|
async emit(type, association, payload) {
|
|
81
83
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
82
|
-
assertWritePolicyMatches(
|
|
84
|
+
assertWritePolicyMatches(this.writerSource, descriptor);
|
|
83
85
|
const runId = association.run;
|
|
84
86
|
if (!runId) {
|
|
85
87
|
console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
|
|
@@ -92,7 +94,8 @@ export class GcsArtifactWriter {
|
|
|
92
94
|
const { body } = prepareUploadBody(payload, descriptor.mime);
|
|
93
95
|
const preview = buildManifestPreview(descriptor, payload);
|
|
94
96
|
if (descriptor.layout === "bulk") {
|
|
95
|
-
const
|
|
97
|
+
const extra = descriptor.extractPathArgs?.(association, payload) ?? [];
|
|
98
|
+
const path = descriptor.objectPath(runId, ...extra);
|
|
96
99
|
const ref = await this.putBody(path, body, {
|
|
97
100
|
layout: "bulk",
|
|
98
101
|
mime: descriptor.mime,
|
|
@@ -133,7 +136,7 @@ export class GcsArtifactWriter {
|
|
|
133
136
|
}
|
|
134
137
|
async appendNdjson(type, association, rows) {
|
|
135
138
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
136
|
-
assertWritePolicyMatches(
|
|
139
|
+
assertWritePolicyMatches(this.writerSource, descriptor);
|
|
137
140
|
if (descriptor.mime !== "application/x-ndjson") {
|
|
138
141
|
console.warn(` ⚠️ appendNdjson("${type}"): descriptor mime is ${descriptor.mime}, not application/x-ndjson — skipping`);
|
|
139
142
|
return null;
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md (§ M4)
|
|
37
37
|
* @see packages/eval/src/artifact-capture/gcs-artifact-writer.ts (mirror)
|
|
38
38
|
*/
|
|
39
|
-
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssociationValues, type RunId, type RunManifest } from "../_vendor/ailf-core/index.d.ts";
|
|
39
|
+
import { type ArtifactEntry, type ArtifactRef, type ArtifactType, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssociationValues, type RunId, type RunManifest, type WriteSource } from "../_vendor/ailf-core/index.d.ts";
|
|
40
40
|
export interface LocalFilesystemArtifactWriterOptions {
|
|
41
41
|
/**
|
|
42
42
|
* Absolute or cwd-relative root directory under which `runs/{runId}/…`
|
|
@@ -56,10 +56,20 @@ export interface LocalFilesystemArtifactWriterOptions {
|
|
|
56
56
|
* render per-batch updates during long export phases.
|
|
57
57
|
*/
|
|
58
58
|
progress?: ArtifactWriterProgressOptions;
|
|
59
|
+
/**
|
|
60
|
+
* Identifies what kind of execution context owns this writer instance.
|
|
61
|
+
* The D0050 guard refuses to emit a descriptor whose `writePolicy` is
|
|
62
|
+
* different from this value. `"pipeline"` (the default) is for writers
|
|
63
|
+
* driven by an `ailf run` pipeline; `"post-hoc"` is for writers driven
|
|
64
|
+
* by deferred commands like `ailf interpret` that emit descriptors
|
|
65
|
+
* declared `writePolicy: "post-hoc"` (D0050).
|
|
66
|
+
*/
|
|
67
|
+
writerSource?: WriteSource;
|
|
59
68
|
}
|
|
60
69
|
export declare class LocalFilesystemArtifactWriter implements ArtifactWriter {
|
|
61
70
|
private readonly options;
|
|
62
71
|
private readonly excludeSet;
|
|
72
|
+
private readonly writerSource;
|
|
63
73
|
constructor(options: LocalFilesystemArtifactWriterOptions);
|
|
64
74
|
private reportProgress;
|
|
65
75
|
emit<T extends ArtifactType>(type: T, association: AssociationValues, payload: unknown): Promise<ArtifactRef | null>;
|
|
@@ -46,9 +46,11 @@ import { redactArtifactData } from "./redact-artifact.js";
|
|
|
46
46
|
export class LocalFilesystemArtifactWriter {
|
|
47
47
|
options;
|
|
48
48
|
excludeSet;
|
|
49
|
+
writerSource;
|
|
49
50
|
constructor(options) {
|
|
50
51
|
this.options = options;
|
|
51
52
|
this.excludeSet = new Set(options.exclude ?? []);
|
|
53
|
+
this.writerSource = options.writerSource ?? "pipeline";
|
|
52
54
|
}
|
|
53
55
|
reportProgress(ref) {
|
|
54
56
|
const progress = this.options.progress;
|
|
@@ -66,7 +68,7 @@ export class LocalFilesystemArtifactWriter {
|
|
|
66
68
|
if (this.excludeSet.has(type))
|
|
67
69
|
return null;
|
|
68
70
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
69
|
-
assertWritePolicyMatches(
|
|
71
|
+
assertWritePolicyMatches(this.writerSource, descriptor);
|
|
70
72
|
const runId = association.run;
|
|
71
73
|
if (!runId) {
|
|
72
74
|
console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
|
|
@@ -81,7 +83,8 @@ export class LocalFilesystemArtifactWriter {
|
|
|
81
83
|
// descriptor's capBytes.
|
|
82
84
|
const preview = buildManifestPreview(descriptor, payload);
|
|
83
85
|
if (descriptor.layout === "bulk") {
|
|
84
|
-
const
|
|
86
|
+
const extra = descriptor.extractPathArgs?.(association, payload) ?? [];
|
|
87
|
+
const relPath = descriptor.objectPath(runId, ...extra);
|
|
85
88
|
const absPath = this.resolve(relPath);
|
|
86
89
|
const wrote = await this.writeAtomic(absPath, body);
|
|
87
90
|
if (!wrote)
|
|
@@ -128,7 +131,7 @@ export class LocalFilesystemArtifactWriter {
|
|
|
128
131
|
if (this.excludeSet.has(type))
|
|
129
132
|
return null;
|
|
130
133
|
const descriptor = ARTIFACT_REGISTRY[type];
|
|
131
|
-
assertWritePolicyMatches(
|
|
134
|
+
assertWritePolicyMatches(this.writerSource, descriptor);
|
|
132
135
|
if (descriptor.mime !== "application/x-ndjson") {
|
|
133
136
|
console.warn(` ⚠️ appendNdjson("${type}"): descriptor mime is ${descriptor.mime}, not application/x-ndjson — skipping`);
|
|
134
137
|
return null;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @see packages/core/src/ports/context.ts — AppContext interface
|
|
16
16
|
* @see docs/archive/exec-plans/ports-and-adapters/phase-7-composition-root.md
|
|
17
17
|
*/
|
|
18
|
-
import { type AppContext, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssertionRegistration, type CardRegistry, type DiagnosisRunner, type Logger, type ResolvedConfig } from "./_vendor/ailf-core/index.d.ts";
|
|
18
|
+
import { type AppContext, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssertionRegistration, type CardRegistry, type DiagnosisRunner, type Logger, type ResolvedConfig, type WriteSource } from "./_vendor/ailf-core/index.d.ts";
|
|
19
19
|
export type { LLMClientKeys } from "./_vendor/ailf-core/index.d.ts";
|
|
20
20
|
import { type BorderlineConsensusOptions, type BorderlineConsensusResult } from "./pipeline/borderline-consensus-runner.js";
|
|
21
21
|
import { CompositeTaskSource, ContentLakeTaskSource, RepoTaskSource } from "./adapters/task-sources/index.js";
|
|
@@ -44,7 +44,7 @@ export declare function createAppContext(config: ResolvedConfig): AppContext;
|
|
|
44
44
|
*
|
|
45
45
|
* Exported for unit-test access; not part of the public package API.
|
|
46
46
|
*/
|
|
47
|
-
export declare function createArtifactWriter(config: ResolvedConfig, logger: Logger, progress?: ArtifactWriterProgressOptions): ArtifactWriter;
|
|
47
|
+
export declare function createArtifactWriter(config: ResolvedConfig, logger: Logger, progress?: ArtifactWriterProgressOptions, writerSource?: WriteSource): ArtifactWriter;
|
|
48
48
|
/**
|
|
49
49
|
* Build the `TaskSource` adapter wired by the composition root for a
|
|
50
50
|
* given `ResolvedConfig`. Exported for test access — composition-root
|
|
@@ -110,10 +110,27 @@ export declare function buildDiagnosisRegistry(): CardRegistry;
|
|
|
110
110
|
*
|
|
111
111
|
* Wires the full 8-card registry, `loadAttributions` bound to the local
|
|
112
112
|
* filesystem (Phase-4 per-entry attribution objects at
|
|
113
|
-
* `{artifactsDir}/runs/{runId}/attribution/*.json`), and
|
|
114
|
-
*
|
|
113
|
+
* `{artifactsDir}/runs/{runId}/attribution/*.json`), and the real
|
|
114
|
+
* `diagnosisWriter` that emits the Diagnosis through a post-hoc-flagged
|
|
115
|
+
* artifact writer AND patches the report doc's
|
|
116
|
+
* `summary.artifactManifest.diagnosis` slot. Two steps because `ailf interpret`
|
|
117
|
+
* runs after the report doc has already been published — the pipeline path's
|
|
118
|
+
* publish-report-step.ts:187 lifts the in-memory run manifest into the doc at
|
|
119
|
+
* end-of-run, but that step never fires for a deferred command.
|
|
120
|
+
*
|
|
121
|
+
* The post-hoc writer is built with `writerSource: "post-hoc"` so the D0050
|
|
122
|
+
* guard accepts the diagnosis descriptor (`writePolicy: "post-hoc"`). Without
|
|
123
|
+
* this, every emit would be rejected at runtime.
|
|
124
|
+
*
|
|
125
|
+
* `diagnosisReader` is still a no-op shim: the Studio data path uses the
|
|
126
|
+
* artifact-manifest entry (populated by the writer + patch) plus a signed-URL
|
|
127
|
+
* fetch, so reader-side cache wiring is deferred to a follow-up W-item.
|
|
128
|
+
* Without the reader, `ailf interpret --refresh` cache hits are not yet served
|
|
129
|
+
* from GCS — they recompute.
|
|
115
130
|
*
|
|
116
131
|
* Plan-06 API/CLI consumers import this function from the composition root
|
|
117
132
|
* and pass `ctx` from `createAppContext(config)`.
|
|
118
133
|
*/
|
|
119
|
-
export declare function getDiagnosisRunner(ctx: AppContext
|
|
134
|
+
export declare function getDiagnosisRunner(ctx: AppContext, opts?: {
|
|
135
|
+
artifactWriter?: ArtifactWriter;
|
|
136
|
+
}): DiagnosisRunner;
|
package/dist/composition-root.js
CHANGED
|
@@ -192,7 +192,7 @@ const DEFAULT_LOCAL_ARTIFACTS_DIR = ".ailf/results/captures";
|
|
|
192
192
|
*
|
|
193
193
|
* Exported for unit-test access; not part of the public package API.
|
|
194
194
|
*/
|
|
195
|
-
export function createArtifactWriter(config, logger, progress) {
|
|
195
|
+
export function createArtifactWriter(config, logger, progress, writerSource = "pipeline") {
|
|
196
196
|
// Legacy `artifactUpload: false` still disables — treat as an alias for
|
|
197
197
|
// the canonical `artifactsDisabled: true` until W0052 removes it.
|
|
198
198
|
if (config.artifactsDisabled === true || config.artifactUpload === false) {
|
|
@@ -214,10 +214,11 @@ export function createArtifactWriter(config, logger, progress) {
|
|
|
214
214
|
// W0053: progress attaches to the OUTERMOST of (local-only | fanout). When
|
|
215
215
|
// fanout is wired, the delegates stay silent so we don't double-count the
|
|
216
216
|
// same caller-visible write across two backends.
|
|
217
|
-
const remote = createRemoteArtifactWriter(config, logger, metrics);
|
|
217
|
+
const remote = createRemoteArtifactWriter(config, logger, metrics, writerSource);
|
|
218
218
|
const local = new LocalFilesystemArtifactWriter({
|
|
219
219
|
rootDir,
|
|
220
220
|
exclude,
|
|
221
|
+
writerSource,
|
|
221
222
|
...(remote ? {} : { progress }),
|
|
222
223
|
});
|
|
223
224
|
// W0064 — when a remote backend is wired, list it first so its ArtifactRef
|
|
@@ -267,7 +268,7 @@ function resolveExcludeList(raw, logger) {
|
|
|
267
268
|
* the sole backend for that run, which is the D0033 M4 default for laptops
|
|
268
269
|
* and CI without GCS creds.
|
|
269
270
|
*/
|
|
270
|
-
function createRemoteArtifactWriter(config, logger, metrics) {
|
|
271
|
+
function createRemoteArtifactWriter(config, logger, metrics, writerSource = "pipeline") {
|
|
271
272
|
const bucket = config.artifactGcsBucket ?? DEFAULT_ARTIFACT_BUCKET;
|
|
272
273
|
const hasGcsCredentials = Boolean(process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GCLOUD_PROJECT);
|
|
273
274
|
if (hasGcsCredentials) {
|
|
@@ -279,6 +280,7 @@ function createRemoteArtifactWriter(config, logger, metrics) {
|
|
|
279
280
|
logger.debug(`Artifact remote backend: GcsArtifactWriter (ADC, bucket=${bucket}, defaultConcurrency=8)`);
|
|
280
281
|
return new GcsArtifactWriter({
|
|
281
282
|
bucket,
|
|
283
|
+
writerSource,
|
|
282
284
|
...(metrics ? { metrics } : {}),
|
|
283
285
|
});
|
|
284
286
|
}
|
|
@@ -306,6 +308,7 @@ function createRemoteArtifactWriter(config, logger, metrics) {
|
|
|
306
308
|
apiKey: config.apiKey,
|
|
307
309
|
bucket,
|
|
308
310
|
putConcurrency: concurrency,
|
|
311
|
+
writerSource,
|
|
309
312
|
...(metrics ? { metrics } : {}),
|
|
310
313
|
});
|
|
311
314
|
}
|
|
@@ -314,6 +317,7 @@ function createRemoteArtifactWriter(config, logger, metrics) {
|
|
|
314
317
|
apiBaseUrl: config.apiUrl,
|
|
315
318
|
apiKey: config.apiKey,
|
|
316
319
|
bucket,
|
|
320
|
+
writerSource,
|
|
317
321
|
...(metrics ? { metrics } : {}),
|
|
318
322
|
});
|
|
319
323
|
}
|
|
@@ -585,17 +589,83 @@ async function loadAttributionsFromLocalFs(runId, artifactsDir, logger) {
|
|
|
585
589
|
*
|
|
586
590
|
* Wires the full 8-card registry, `loadAttributions` bound to the local
|
|
587
591
|
* filesystem (Phase-4 per-entry attribution objects at
|
|
588
|
-
* `{artifactsDir}/runs/{runId}/attribution/*.json`), and
|
|
589
|
-
*
|
|
592
|
+
* `{artifactsDir}/runs/{runId}/attribution/*.json`), and the real
|
|
593
|
+
* `diagnosisWriter` that emits the Diagnosis through a post-hoc-flagged
|
|
594
|
+
* artifact writer AND patches the report doc's
|
|
595
|
+
* `summary.artifactManifest.diagnosis` slot. Two steps because `ailf interpret`
|
|
596
|
+
* runs after the report doc has already been published — the pipeline path's
|
|
597
|
+
* publish-report-step.ts:187 lifts the in-memory run manifest into the doc at
|
|
598
|
+
* end-of-run, but that step never fires for a deferred command.
|
|
599
|
+
*
|
|
600
|
+
* The post-hoc writer is built with `writerSource: "post-hoc"` so the D0050
|
|
601
|
+
* guard accepts the diagnosis descriptor (`writePolicy: "post-hoc"`). Without
|
|
602
|
+
* this, every emit would be rejected at runtime.
|
|
603
|
+
*
|
|
604
|
+
* `diagnosisReader` is still a no-op shim: the Studio data path uses the
|
|
605
|
+
* artifact-manifest entry (populated by the writer + patch) plus a signed-URL
|
|
606
|
+
* fetch, so reader-side cache wiring is deferred to a follow-up W-item.
|
|
607
|
+
* Without the reader, `ailf interpret --refresh` cache hits are not yet served
|
|
608
|
+
* from GCS — they recompute.
|
|
590
609
|
*
|
|
591
610
|
* Plan-06 API/CLI consumers import this function from the composition root
|
|
592
611
|
* and pass `ctx` from `createAppContext(config)`.
|
|
593
612
|
*/
|
|
594
|
-
export function getDiagnosisRunner(ctx) {
|
|
613
|
+
export function getDiagnosisRunner(ctx, opts) {
|
|
595
614
|
const artifactsDir = ctx.config.artifactsDir ?? DIAGNOSIS_LOCAL_ARTIFACTS_DIR;
|
|
596
|
-
//
|
|
615
|
+
// Post-hoc artifact writer — built with the same fanout/remote/local layering
|
|
616
|
+
// as the pipeline writer but flagged so the D0050 guard accepts post-hoc
|
|
617
|
+
// descriptors. Construction is per-runner so the AccumulatingArtifactWriter's
|
|
618
|
+
// internal manifest doesn't carry state between unrelated interpret runs.
|
|
619
|
+
// Tests inject their own writer via opts.artifactWriter; the production
|
|
620
|
+
// CLI / pipeline callers never pass it.
|
|
621
|
+
const postHocArtifactWriter = opts?.artifactWriter ??
|
|
622
|
+
createArtifactWriter(ctx.config, ctx.logger, undefined, "post-hoc");
|
|
623
|
+
// No-op reader — see JSDoc above. The Studio data path is manifest-driven,
|
|
624
|
+
// not reader-driven, so the writer + patch alone unblock Phase 7.
|
|
597
625
|
const diagnosisReader = async (_path) => null;
|
|
598
|
-
|
|
626
|
+
// Real writer — two-step persistence:
|
|
627
|
+
// 1. Emit the diagnosis payload through the post-hoc writer; the descriptor's
|
|
628
|
+
// `objectPath: diagnosisPathBuilder()` derives the storage path from
|
|
629
|
+
// `{runId, reportId, compoundVersion}`.
|
|
630
|
+
// 2. Patch the published report doc's `summary.artifactManifest.diagnosis`
|
|
631
|
+
// slot with the returned ArtifactRef, so Studio's slim-shape GROQ
|
|
632
|
+
// projection surfaces the entry. (The pipeline path runs this lift via
|
|
633
|
+
// publish-report-step.ts; that step never fires for a deferred command,
|
|
634
|
+
// hence the explicit patch here.)
|
|
635
|
+
//
|
|
636
|
+
// Errors are caught and logged rather than thrown — the diagnosis runner
|
|
637
|
+
// separates "compute" from "persist". Failed persistence should not panic
|
|
638
|
+
// the runner; the computed cards still surface to API/CLI callers in-memory.
|
|
639
|
+
// ReportStore.patchArtifactManifest is itself non-fatal on Sanity failure,
|
|
640
|
+
// so it does not need its own try/catch.
|
|
641
|
+
const diagnosisWriter = async (_descriptorPath, diagnosis) => {
|
|
642
|
+
let ref;
|
|
643
|
+
try {
|
|
644
|
+
// Anchor the diagnosis to the REPORT's run, not the post-hoc CLI's
|
|
645
|
+
// session run. `ctx.runId` is freshly generated per interpret
|
|
646
|
+
// invocation; the report doc's `provenance.runId` is what Studio
|
|
647
|
+
// and the signing endpoint look up. Using `assoc(ctx, ...)` would
|
|
648
|
+
// bind `run` to ctx.runId — the path would be writeable but
|
|
649
|
+
// unreachable from the Studio side.
|
|
650
|
+
ref = await postHocArtifactWriter.emit("diagnosis", { run: diagnosis.runId, report: diagnosis.reportId }, diagnosis);
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
ctx.logger.warn("diagnosis-emit-failed", {
|
|
654
|
+
reportId: diagnosis.reportId,
|
|
655
|
+
error: error instanceof Error ? error.message : String(error),
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!ref)
|
|
660
|
+
return;
|
|
661
|
+
if (!ctx.reportStore) {
|
|
662
|
+
ctx.logger.warn("diagnosis-emit: no reportStore on context", {
|
|
663
|
+
reportId: diagnosis.reportId,
|
|
664
|
+
});
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
await ctx.reportStore.patchArtifactManifest(diagnosis.reportId, "diagnosis", ref);
|
|
668
|
+
};
|
|
599
669
|
return createDiagnosisRunner({
|
|
600
670
|
llm: ctx.llmClient,
|
|
601
671
|
model: modelId("anthropic:claude-opus-4-6"),
|
|
@@ -215,7 +215,6 @@ export class GapAnalysisStep {
|
|
|
215
215
|
...(documentManifest !== undefined && { documentManifest }),
|
|
216
216
|
failureModes: failureModeReport,
|
|
217
217
|
lowScoringJudgments,
|
|
218
|
-
recommendations: gapReport,
|
|
219
218
|
scores: enrichedScores,
|
|
220
219
|
...(testResults !== undefined && { testResults }),
|
|
221
220
|
};
|
package/dist/report-store.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* @see docs/design-docs/report-store/domain-model.md
|
|
16
16
|
*/
|
|
17
17
|
import type { SanityClient } from "@sanity/client";
|
|
18
|
-
import type { SynthesisCostTelemetry } from "./_vendor/ailf-core/index.d.ts";
|
|
18
|
+
import type { ArtifactRef, ArtifactType, SynthesisCostTelemetry } from "./_vendor/ailf-core/index.d.ts";
|
|
19
19
|
import type { ComparisonReport, ISOTimestamp, LineageQuery, Report, ReportId, ReportProvenance, ScoreSummary } from "./pipeline/types.js";
|
|
20
20
|
/**
|
|
21
21
|
* Result of an auto-comparison, bundling the ComparisonReport with the
|
|
@@ -134,6 +134,20 @@ export declare class ReportStore {
|
|
|
134
134
|
* Document _id is `report-${reportId}` (see `toSanityReportDoc` L559).
|
|
135
135
|
*/
|
|
136
136
|
patchSynthesis(reportId: ReportId, telemetry: SynthesisCostTelemetry): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Patch a single artifact-manifest entry onto a published report.
|
|
139
|
+
*
|
|
140
|
+
* Used by deferred commands like `ailf interpret` whose post-hoc writer
|
|
141
|
+
* produces a new ArtifactRef *after* the report doc was published. The
|
|
142
|
+
* pipeline path lifts the full manifest into the doc at publish time
|
|
143
|
+
* (publish-report-step.ts:187); this method is the post-hoc equivalent
|
|
144
|
+
* for a single slot.
|
|
145
|
+
*
|
|
146
|
+
* Non-fatal on Sanity failure — mirrors `patchSynthesis` (L423).
|
|
147
|
+
*
|
|
148
|
+
* Document _id is `report-${reportId}` (see `toSanityReportDoc` L559).
|
|
149
|
+
*/
|
|
150
|
+
patchArtifactManifest(reportId: ReportId, slot: ArtifactType, ref: ArtifactRef): Promise<void>;
|
|
137
151
|
/**
|
|
138
152
|
* Query error arrays from the last N reports for chronic failure detection.
|
|
139
153
|
*
|
package/dist/report-store.js
CHANGED
|
@@ -327,6 +327,31 @@ export class ReportStore {
|
|
|
327
327
|
console.warn(` ⚠️ Failed to patch synthesis telemetry on report ${reportId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Patch a single artifact-manifest entry onto a published report.
|
|
332
|
+
*
|
|
333
|
+
* Used by deferred commands like `ailf interpret` whose post-hoc writer
|
|
334
|
+
* produces a new ArtifactRef *after* the report doc was published. The
|
|
335
|
+
* pipeline path lifts the full manifest into the doc at publish time
|
|
336
|
+
* (publish-report-step.ts:187); this method is the post-hoc equivalent
|
|
337
|
+
* for a single slot.
|
|
338
|
+
*
|
|
339
|
+
* Non-fatal on Sanity failure — mirrors `patchSynthesis` (L423).
|
|
340
|
+
*
|
|
341
|
+
* Document _id is `report-${reportId}` (see `toSanityReportDoc` L559).
|
|
342
|
+
*/
|
|
343
|
+
async patchArtifactManifest(reportId, slot, ref) {
|
|
344
|
+
try {
|
|
345
|
+
await this.client
|
|
346
|
+
.patch(`report-${reportId}`)
|
|
347
|
+
.setIfMissing({ "summary.artifactManifest": {} })
|
|
348
|
+
.set({ [`summary.artifactManifest.${slot}`]: ref })
|
|
349
|
+
.commit();
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
console.warn(` ⚠️ Failed to patch artifactManifest.${slot} on report ${reportId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
330
355
|
/**
|
|
331
356
|
* Query error arrays from the last N reports for chronic failure detection.
|
|
332
357
|
*
|