@sanity/ailf 6.1.0 → 6.1.2
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/_vendor/ailf-shared/editorial-reference.d.ts +48 -0
- package/dist/_vendor/ailf-shared/editorial-reference.js +43 -0
- package/dist/_vendor/ailf-shared/index.d.ts +1 -0
- package/dist/_vendor/ailf-shared/index.js +1 -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/pipeline/mirror-repo-tasks.d.ts +1 -1
- package/dist/report-store.d.ts +15 -1
- package/dist/report-store.js +25 -0
- package/dist/sanity/client.d.ts +6 -21
- package/dist/sanity/client.js +20 -22
- 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.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross Dataset Reference (CDR) write shape (D0043).
|
|
3
|
+
*
|
|
4
|
+
* The shape AILF documents use to reference editorial content (article,
|
|
5
|
+
* docPage, …) across the dataset boundary (`ailf-prod-private` →
|
|
6
|
+
* editorial). Sanity's Mutation API requires `_projectId` even when both
|
|
7
|
+
* datasets live in the same project (W0154 spike Finding 13). `_weak: true`
|
|
8
|
+
* keeps the AILF document publishable when the editorial target is retired
|
|
9
|
+
* (Finding 16) and matches the W0155 schema declaration.
|
|
10
|
+
*
|
|
11
|
+
* Canonical builder for this shape. Used by:
|
|
12
|
+
*
|
|
13
|
+
* - `packages/eval/src/sanity/client.ts:buildEditorialReference()` — the
|
|
14
|
+
* env-aware runtime wrapper that supplies projectId + dataset from
|
|
15
|
+
* env, used by the eval pipeline's mirror step (W0156).
|
|
16
|
+
* - `packages/studio/scripts/migrate-to-private-dataset.ts` — the
|
|
17
|
+
* one-shot corpus migration script (W0159).
|
|
18
|
+
*
|
|
19
|
+
* Adding a field here is a single edit; both consumers pick it up without
|
|
20
|
+
* drift. That is the load-bearing property — the migration script and the
|
|
21
|
+
* runtime write path must emit byte-identical CDR shapes.
|
|
22
|
+
*/
|
|
23
|
+
export interface EditorialReference {
|
|
24
|
+
_type: "crossDatasetReference";
|
|
25
|
+
_projectId: string;
|
|
26
|
+
_dataset: string;
|
|
27
|
+
_ref: string;
|
|
28
|
+
_weak?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface MakeEditorialReferenceArgs {
|
|
31
|
+
/** Sanity project ID hosting both source and dest datasets. */
|
|
32
|
+
projectId: string;
|
|
33
|
+
/** The editorial dataset name (e.g. "next"). */
|
|
34
|
+
dataset: string;
|
|
35
|
+
/** The document ID being referenced. */
|
|
36
|
+
refId: string;
|
|
37
|
+
/** Whether the reference is weak. Defaults to true for AILF→editorial. */
|
|
38
|
+
weak?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Construct an editorial Cross Dataset Reference. Pure — no env reads, no
|
|
42
|
+
* side effects. Callers that need env-aware defaults (the eval pipeline)
|
|
43
|
+
* wrap this; callers with explicit config (the migration script) call it
|
|
44
|
+
* directly.
|
|
45
|
+
*
|
|
46
|
+
* @throws if `refId` is empty.
|
|
47
|
+
*/
|
|
48
|
+
export declare function makeEditorialReference(args: MakeEditorialReferenceArgs): EditorialReference;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross Dataset Reference (CDR) write shape (D0043).
|
|
3
|
+
*
|
|
4
|
+
* The shape AILF documents use to reference editorial content (article,
|
|
5
|
+
* docPage, …) across the dataset boundary (`ailf-prod-private` →
|
|
6
|
+
* editorial). Sanity's Mutation API requires `_projectId` even when both
|
|
7
|
+
* datasets live in the same project (W0154 spike Finding 13). `_weak: true`
|
|
8
|
+
* keeps the AILF document publishable when the editorial target is retired
|
|
9
|
+
* (Finding 16) and matches the W0155 schema declaration.
|
|
10
|
+
*
|
|
11
|
+
* Canonical builder for this shape. Used by:
|
|
12
|
+
*
|
|
13
|
+
* - `packages/eval/src/sanity/client.ts:buildEditorialReference()` — the
|
|
14
|
+
* env-aware runtime wrapper that supplies projectId + dataset from
|
|
15
|
+
* env, used by the eval pipeline's mirror step (W0156).
|
|
16
|
+
* - `packages/studio/scripts/migrate-to-private-dataset.ts` — the
|
|
17
|
+
* one-shot corpus migration script (W0159).
|
|
18
|
+
*
|
|
19
|
+
* Adding a field here is a single edit; both consumers pick it up without
|
|
20
|
+
* drift. That is the load-bearing property — the migration script and the
|
|
21
|
+
* runtime write path must emit byte-identical CDR shapes.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Construct an editorial Cross Dataset Reference. Pure — no env reads, no
|
|
25
|
+
* side effects. Callers that need env-aware defaults (the eval pipeline)
|
|
26
|
+
* wrap this; callers with explicit config (the migration script) call it
|
|
27
|
+
* directly.
|
|
28
|
+
*
|
|
29
|
+
* @throws if `refId` is empty.
|
|
30
|
+
*/
|
|
31
|
+
export function makeEditorialReference(args) {
|
|
32
|
+
if (!args.refId) {
|
|
33
|
+
throw new Error("makeEditorialReference: refId is required");
|
|
34
|
+
}
|
|
35
|
+
const weak = args.weak ?? true;
|
|
36
|
+
return {
|
|
37
|
+
_type: "crossDatasetReference",
|
|
38
|
+
_projectId: args.projectId,
|
|
39
|
+
_dataset: args.dataset,
|
|
40
|
+
_ref: args.refId,
|
|
41
|
+
...(weak ? { _weak: true } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export { computeCanaryDrift, type CanaryDriftReport, type CanaryReportSlim, type DriftEntry, type DriftThresholds, type DriftVerdict, } from "./canary-drift.js";
|
|
21
21
|
export { type DocumentRef } from "./document-ref.js";
|
|
22
|
+
export { makeEditorialReference, type EditorialReference, type MakeEditorialReferenceArgs, } from "./editorial-reference.js";
|
|
22
23
|
export { FEATURE_FLAGS, type FeatureFlag, type FeatureFlagKey, } from "./feature-flags.js";
|
|
23
24
|
export { GRADE_BOUNDARIES, scoreGrade, type ScoreGrade, } from "./score-grades.js";
|
|
24
25
|
export { NOISE_THRESHOLD } from "./noise-threshold.js";
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* surface against future regressions.
|
|
19
19
|
*/
|
|
20
20
|
export { computeCanaryDrift, } from "./canary-drift.js";
|
|
21
|
+
export { makeEditorialReference, } from "./editorial-reference.js";
|
|
21
22
|
export { FEATURE_FLAGS, } from "./feature-flags.js";
|
|
22
23
|
export { GRADE_BOUNDARIES, scoreGrade, } from "./score-grades.js";
|
|
23
24
|
export { NOISE_THRESHOLD } from "./noise-threshold.js";
|
|
@@ -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
|
};
|
|
@@ -135,7 +135,7 @@ export declare function buildMirrorDocument(task: LiteracyTaskDefinition, opts:
|
|
|
135
135
|
_key: string;
|
|
136
136
|
reason: string;
|
|
137
137
|
} | {
|
|
138
|
-
doc?: import("
|
|
138
|
+
doc?: import("@sanity/ailf-shared").EditorialReference | undefined;
|
|
139
139
|
docId?: string | undefined;
|
|
140
140
|
refType: string;
|
|
141
141
|
_key: string;
|
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
|
*
|
package/dist/sanity/client.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type SanityClient } from "@sanity/client";
|
|
2
|
+
import { type EditorialReference } from "../_vendor/ailf-shared/index.d.ts";
|
|
2
3
|
import type { ResolvedSourceConfig } from "../sources.js";
|
|
4
|
+
export type { EditorialReference };
|
|
3
5
|
export interface SanityConfig {
|
|
4
6
|
apiVersion: string;
|
|
5
7
|
dataset: string;
|
|
@@ -50,25 +52,6 @@ export declare function getAilfSanityClient(overrides?: Partial<SanityConfig>):
|
|
|
50
52
|
* mutating env vars between tests so the cached client doesn't leak.
|
|
51
53
|
*/
|
|
52
54
|
export declare function resetAilfSanityClientCache(): void;
|
|
53
|
-
/**
|
|
54
|
-
* The write shape for a Cross Dataset Reference. Sanity's Mutation API
|
|
55
|
-
* requires `_projectId` even when the target dataset lives in the same
|
|
56
|
-
* project (spike Finding 13). `_weak: true` keeps the AILF source doc
|
|
57
|
-
* publishable when the editorial target is retired (Finding 16).
|
|
58
|
-
*
|
|
59
|
-
* The literal `_type: "crossDatasetReference"` matches the receiving
|
|
60
|
-
* Studio schema field type and the validated migration script
|
|
61
|
-
* (`packages/studio/scripts/poc-migrate-to-private.ts`).
|
|
62
|
-
*
|
|
63
|
-
* @see docs/decisions/D0043-private-dataset-with-cdrs.md
|
|
64
|
-
*/
|
|
65
|
-
export interface EditorialReference {
|
|
66
|
-
_type: "crossDatasetReference";
|
|
67
|
-
_projectId: string;
|
|
68
|
-
_dataset: string;
|
|
69
|
-
_ref: string;
|
|
70
|
-
_weak?: boolean;
|
|
71
|
-
}
|
|
72
55
|
export interface BuildEditorialReferenceOptions {
|
|
73
56
|
/** Override the editorial-side project ID. Defaults to AILF project. */
|
|
74
57
|
projectId?: string;
|
|
@@ -82,7 +65,9 @@ export interface BuildEditorialReferenceOptions {
|
|
|
82
65
|
weak?: boolean;
|
|
83
66
|
}
|
|
84
67
|
/**
|
|
85
|
-
* Build a Cross Dataset Reference from an AILF document into editorial content
|
|
68
|
+
* Build a Cross Dataset Reference from an AILF document into editorial content,
|
|
69
|
+
* with env-aware defaults for `projectId` / `dataset` so call sites can pass
|
|
70
|
+
* just the `refId` in the common case.
|
|
86
71
|
*
|
|
87
72
|
* Use this for any reference field on an `ailf.*` document that points at an
|
|
88
73
|
* editorial type (`article`, `docPage`, …). The schema-side counterpart is a
|
|
@@ -90,7 +75,7 @@ export interface BuildEditorialReferenceOptions {
|
|
|
90
75
|
*
|
|
91
76
|
* @example
|
|
92
77
|
* const docRef = buildEditorialReference(articleId)
|
|
93
|
-
* // → { _type: "
|
|
78
|
+
* // → { _type: "crossDatasetReference", _projectId: "3do82whm",
|
|
94
79
|
* // _dataset: "next", _ref: articleId, _weak: true }
|
|
95
80
|
*/
|
|
96
81
|
export declare function buildEditorialReference(refId: string, options?: BuildEditorialReferenceOptions): EditorialReference;
|
package/dist/sanity/client.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createClient } from "@sanity/client";
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
2
|
+
import { makeEditorialReference, } from "../_vendor/ailf-shared/index.js";
|
|
3
|
+
// Belt-and-suspenders default for AILF operational documents per D0043.
|
|
4
|
+
// In practice this constant is never the load-bearing value in any
|
|
5
|
+
// production-shaped env: the cascade in `getAilfDefaultConfig()` is
|
|
6
|
+
// `AILF_REPORT_DATASET ?? SANITY_DATASET ?? AILF_DATASET_DEFAULT`, and
|
|
7
|
+
// every deployed env sets at least `SANITY_DATASET` for editorial reads.
|
|
8
|
+
// The constant matters only for ad-hoc runs with no env at all — where
|
|
9
|
+
// landing in `ailf-prod-private` is safer than polluting the editorial
|
|
10
|
+
// dataset with stray writes.
|
|
11
|
+
const AILF_DATASET_DEFAULT = "ailf-prod-private";
|
|
9
12
|
// Default editorial dataset that AILF cross-dataset references point at.
|
|
10
13
|
// Used by `buildEditorialReference()` and by Studio's CDR field config.
|
|
11
14
|
const EDITORIAL_DATASET_DEFAULT = "next";
|
|
@@ -157,7 +160,9 @@ export function resetAilfSanityClientCache() {
|
|
|
157
160
|
_ailfClient = null;
|
|
158
161
|
}
|
|
159
162
|
/**
|
|
160
|
-
* Build a Cross Dataset Reference from an AILF document into editorial content
|
|
163
|
+
* Build a Cross Dataset Reference from an AILF document into editorial content,
|
|
164
|
+
* with env-aware defaults for `projectId` / `dataset` so call sites can pass
|
|
165
|
+
* just the `refId` in the common case.
|
|
161
166
|
*
|
|
162
167
|
* Use this for any reference field on an `ailf.*` document that points at an
|
|
163
168
|
* editorial type (`article`, `docPage`, …). The schema-side counterpart is a
|
|
@@ -165,13 +170,10 @@ export function resetAilfSanityClientCache() {
|
|
|
165
170
|
*
|
|
166
171
|
* @example
|
|
167
172
|
* const docRef = buildEditorialReference(articleId)
|
|
168
|
-
* // → { _type: "
|
|
173
|
+
* // → { _type: "crossDatasetReference", _projectId: "3do82whm",
|
|
169
174
|
* // _dataset: "next", _ref: articleId, _weak: true }
|
|
170
175
|
*/
|
|
171
176
|
export function buildEditorialReference(refId, options = {}) {
|
|
172
|
-
if (!refId) {
|
|
173
|
-
throw new Error("buildEditorialReference: refId is required");
|
|
174
|
-
}
|
|
175
177
|
// oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
|
|
176
178
|
const envProjectId = process.env.AILF_REPORT_PROJECT_ID || process.env.SANITY_PROJECT_ID;
|
|
177
179
|
// Editorial dataset precedence: explicit AILF_EDITORIAL_DATASET overrides;
|
|
@@ -179,14 +181,10 @@ export function buildEditorialReference(refId, options = {}) {
|
|
|
179
181
|
// correct editorial dataset); fall back to "next" only when nothing is set.
|
|
180
182
|
// oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
|
|
181
183
|
const envDataset = process.env.AILF_EDITORIAL_DATASET || process.env.SANITY_DATASET;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
_dataset: dataset,
|
|
189
|
-
_ref: refId,
|
|
190
|
-
...(weak ? { _weak: true } : {}),
|
|
191
|
-
};
|
|
184
|
+
return makeEditorialReference({
|
|
185
|
+
projectId: options.projectId ?? envProjectId ?? "3do82whm",
|
|
186
|
+
dataset: options.dataset ?? envDataset ?? EDITORIAL_DATASET_DEFAULT,
|
|
187
|
+
refId,
|
|
188
|
+
weak: options.weak,
|
|
189
|
+
});
|
|
192
190
|
}
|