@sanity/ailf 4.0.6 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/bin/ailf.js +6 -1
  2. package/dist/_vendor/ailf-core/schemas/external-providers.d.ts +136 -0
  3. package/dist/_vendor/ailf-core/schemas/external-providers.js +136 -0
  4. package/dist/_vendor/ailf-core/schemas/index.d.ts +2 -0
  5. package/dist/_vendor/ailf-core/schemas/index.js +2 -0
  6. package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +2 -3
  7. package/dist/_vendor/ailf-core/schemas/report.d.ts +251 -0
  8. package/dist/_vendor/ailf-core/schemas/report.js +235 -0
  9. package/dist/_vendor/ailf-core/services/index.d.ts +1 -0
  10. package/dist/_vendor/ailf-core/services/index.js +1 -0
  11. package/dist/_vendor/ailf-core/services/report-to-markdown.d.ts +38 -0
  12. package/dist/_vendor/ailf-core/services/report-to-markdown.js +696 -0
  13. package/dist/_vendor/ailf-core/types/api-requests.d.ts +159 -0
  14. package/dist/_vendor/ailf-core/types/api-requests.js +27 -0
  15. package/dist/_vendor/ailf-core/types/index.d.ts +3 -0
  16. package/dist/_vendor/ailf-core/types/pipeline-request.d.ts +112 -0
  17. package/dist/_vendor/ailf-core/types/pipeline-request.js +18 -0
  18. package/dist/_vendor/ailf-core/types/repo-config.d.ts +146 -0
  19. package/dist/_vendor/ailf-core/types/repo-config.js +18 -0
  20. package/dist/_vendor/ailf-shared/index.d.ts +7 -5
  21. package/dist/_vendor/ailf-shared/index.js +7 -5
  22. package/dist/adapters/api-client/types.d.ts +2 -5
  23. package/dist/adapters/task-sources/content-lake-task-source.d.ts +58 -1
  24. package/dist/adapters/task-sources/content-lake-task-source.js +1 -1
  25. package/dist/adapters/task-sources/index.d.ts +1 -1
  26. package/dist/adapters/task-sources/index.js +1 -1
  27. package/dist/adapters/task-sources/repo-schemas.d.ts +3 -2
  28. package/dist/adapters/task-sources/repo-schemas.js +3 -1
  29. package/dist/adapters/task-sources/repo-task-source.d.ts +11 -1
  30. package/dist/adapters/task-sources/repo-task-source.js +7 -4
  31. package/dist/adapters/task-sources/repo-validation.d.ts +6 -6
  32. package/dist/adapters/task-sources/repo-validation.js +1 -1
  33. package/dist/agent-observer/agentic-provider.d.ts +1 -0
  34. package/dist/agent-observer/agentic-provider.js +43 -36
  35. package/dist/agent-observer/config-schemas.d.ts +61 -0
  36. package/dist/agent-observer/config-schemas.js +65 -0
  37. package/dist/agent-observer/provider.d.ts +1 -0
  38. package/dist/agent-observer/provider.js +19 -17
  39. package/dist/cli.js +4 -4
  40. package/dist/commands/validate-tasks.js +2 -2
  41. package/dist/composition-root.d.ts +7 -0
  42. package/dist/composition-root.js +27 -12
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/job-store.js +2 -2
  46. package/dist/lib/dotenv-resolution.d.ts +21 -0
  47. package/dist/lib/dotenv-resolution.js +30 -0
  48. package/dist/orchestration/steps/fetch-docs-step.js +10 -30
  49. package/dist/orchestration/steps/generate-configs-step.d.ts +8 -15
  50. package/dist/orchestration/steps/generate-configs-step.js +26 -118
  51. package/dist/orchestration/steps/mirror-repo-tasks-step.js +26 -3
  52. package/dist/orchestration/steps/run-eval-step.js +21 -3
  53. package/dist/pipeline/agent-behavior-report.d.ts +2 -8
  54. package/dist/pipeline/cache.d.ts +2 -2
  55. package/dist/pipeline/checks.d.ts +10 -2
  56. package/dist/pipeline/checks.js +14 -4
  57. package/dist/pipeline/compiler/literacy-bridge.js +2 -2
  58. package/dist/pipeline/compiler/mode-handlers/agent-harness/types.d.ts +2 -2
  59. package/dist/pipeline/compiler/mode-handlers/index.d.ts +1 -1
  60. package/dist/pipeline/compiler/mode-handlers/knowledge-probe/types.d.ts +2 -2
  61. package/dist/pipeline/compiler/mode-handlers/literacy/index.d.ts +1 -1
  62. package/dist/pipeline/compiler/mode-handlers/literacy/types.d.ts +3 -3
  63. package/dist/pipeline/compiler/promptfoo-compiler.js +7 -11
  64. package/dist/pipeline/compiler/provider-assembler.js +33 -3
  65. package/dist/pipeline/compiler/rubric-resolution.d.ts +2 -2
  66. package/dist/pipeline/mirror-repo-tasks.d.ts +13 -5
  67. package/dist/pipeline/mirror-repo-tasks.js +16 -8
  68. package/dist/pipeline/pr-comment.d.ts +22 -9
  69. package/dist/pipeline/pr-comment.js +52 -472
  70. package/dist/pipeline/resolve-mappings.d.ts +8 -3
  71. package/dist/promptfoo-providers/mock-path.d.ts +12 -0
  72. package/dist/promptfoo-providers/mock-path.js +15 -0
  73. package/dist/report-store.d.ts +63 -1
  74. package/dist/report-store.js +111 -31
  75. package/dist/sanity/client.d.ts +58 -0
  76. package/dist/sanity/client.js +106 -0
  77. package/package.json +8 -7
  78. package/dist/orchestration/load-pipeline-tasks.d.ts +0 -40
  79. package/dist/orchestration/load-pipeline-tasks.js +0 -57
@@ -83,6 +83,9 @@ export declare class ReportStore {
83
83
  findComparableBaseline(query: LineageQuery): Promise<null | Report>;
84
84
  /**
85
85
  * Read a report by its ID.
86
+ *
87
+ * @throws {ReportSchemaValidationError} if the stored document fails the
88
+ * W0191 runtime schema gate. Sanity API failures still return null.
86
89
  */
87
90
  read(id: ReportId): Promise<null | Report>;
88
91
  /**
@@ -91,7 +94,10 @@ export declare class ReportStore {
91
94
  * Creates an immutable `ailf.report` document. The document _id is
92
95
  * prefixed with `report-` for easy GROQ filtering.
93
96
  *
94
- * @returns The report ID on success, null on failure (logged, not thrown)
97
+ * @returns The report ID on success, null on Sanity API failure (logged,
98
+ * not thrown — P5 local-first).
99
+ * @throws {ReportSchemaValidationError} if the report fails the W0191
100
+ * runtime schema gate. Schema drift is a bug, not an outage.
95
101
  */
96
102
  write(report: Report): Promise<null | ReportId>;
97
103
  /**
@@ -121,3 +127,59 @@ export declare class ReportStore {
121
127
  * Uses crypto.randomUUID() as a base and overwrites the timestamp portion.
122
128
  */
123
129
  export declare function generateReportId(): ReportId;
130
+ /**
131
+ * Tagged error thrown by `toReport` and `ReportStore.write` when a Content
132
+ * Lake document fails the W0191 runtime schema gate. Read paths
133
+ * (`read`, `findByFingerprint`, `findComparableBaseline`) re-throw this
134
+ * error rather than swallowing it via the P5 log-and-continue try/catch
135
+ * — schema drift is a bug to be surfaced, not an outage to be tolerated.
136
+ */
137
+ export declare class ReportSchemaValidationError extends Error {
138
+ constructor(message: string);
139
+ }
140
+ /**
141
+ * Build the Sanity `ailf.report` document shape for a domain `Report`.
142
+ *
143
+ * Inverse of `toReport`. Strips the baseline + experiment ScoreSummary
144
+ * bulk from any nested comparison (they duplicate `report.summary` and the
145
+ * `comparedAgainst` lineage), and lifts `artifactManifest` into the
146
+ * `summary.artifactManifest` slot where the D0032 GROQ projection expects
147
+ * it.
148
+ *
149
+ * Pure: takes a `Report`, returns a Sanity-shaped object literal. No I/O.
150
+ * Used by `ReportStore.write` (production) and round-trip contract tests
151
+ * (W0188) which need a deterministic forward-and-back mapping.
152
+ */
153
+ export interface SanityReportDoc {
154
+ _id: string;
155
+ _type: string;
156
+ comparison: null | Omit<ComparisonReport, "baseline" | "experiment">;
157
+ completedAt: string;
158
+ durationMs: number;
159
+ provenance: Report["provenance"];
160
+ reportId: ReportId;
161
+ summary: Report["summary"] & {
162
+ artifactManifest?: Report["artifactManifest"];
163
+ };
164
+ tag: null | string;
165
+ title: null | string;
166
+ }
167
+ export declare function toSanityReportDoc(report: Report): SanityReportDoc;
168
+ /**
169
+ * Convert a raw Sanity document to a typed Report.
170
+ *
171
+ * The Sanity document shape mirrors the Report type but includes Sanity
172
+ * metadata (_id, _type, _rev, etc.) that we strip.
173
+ */
174
+ export declare function toReport(doc: Record<string, unknown>): Report;
175
+ /**
176
+ * Remove the `baseline` and `experiment` ScoreSummary objects from a
177
+ * ComparisonReport, producing a slim copy suitable for persistence.
178
+ *
179
+ * These fields are redundant in the stored document:
180
+ * - `experiment` is byte-for-byte identical to `report.summary`
181
+ * - `baseline` is fetchable via `provenance.lineage.comparedAgainst`
182
+ *
183
+ * Everything else (deltas, areas, classifications) is preserved.
184
+ */
185
+ export declare function stripComparisonBulk(comparison: ComparisonReport): Omit<ComparisonReport, "baseline" | "experiment">;
@@ -14,7 +14,8 @@
14
14
  * @see docs/design-docs/report-store/architecture.md
15
15
  * @see docs/design-docs/report-store/domain-model.md
16
16
  */
17
- import { getSanityClient } from "./sanity/client.js";
17
+ import { ReportSchema } from "./_vendor/ailf-core/index.js";
18
+ import { getAilfSanityClient } from "./sanity/client.js";
18
19
  import { compare } from "./pipeline/compare.js";
19
20
  // ---------------------------------------------------------------------------
20
21
  // Constants
@@ -30,7 +31,7 @@ export class ReportStore {
30
31
  this.client = options.client;
31
32
  }
32
33
  else {
33
- this.client = getSanityClient({
34
+ this.client = getAilfSanityClient({
34
35
  ...(options.dataset ? { dataset: options.dataset } : {}),
35
36
  ...(options.projectId ? { projectId: options.projectId } : {}),
36
37
  ...(options.token ? { token: options.token } : {}),
@@ -130,6 +131,9 @@ export class ReportStore {
130
131
  return doc ? toReport(doc) : null;
131
132
  }
132
133
  catch (error) {
134
+ // W0191: schema-validation errors are bugs, not outages — surface them.
135
+ if (error instanceof ReportSchemaValidationError)
136
+ throw error;
133
137
  console.warn(` ⚠️ Failed to query cached report by fingerprint: ${error instanceof Error ? error.message : String(error)}`);
134
138
  return null;
135
139
  }
@@ -166,12 +170,18 @@ export class ReportStore {
166
170
  return doc ? toReport(doc) : null;
167
171
  }
168
172
  catch (error) {
173
+ // W0191: schema-validation errors are bugs, not outages — surface them.
174
+ if (error instanceof ReportSchemaValidationError)
175
+ throw error;
169
176
  console.warn(` ⚠️ Failed to query comparable baseline: ${error instanceof Error ? error.message : String(error)}`);
170
177
  return null;
171
178
  }
172
179
  }
173
180
  /**
174
181
  * Read a report by its ID.
182
+ *
183
+ * @throws {ReportSchemaValidationError} if the stored document fails the
184
+ * W0191 runtime schema gate. Sanity API failures still return null.
175
185
  */
176
186
  async read(id) {
177
187
  try {
@@ -179,6 +189,9 @@ export class ReportStore {
179
189
  return doc ? toReport(doc) : null;
180
190
  }
181
191
  catch (error) {
192
+ // W0191: schema-validation errors are bugs, not outages — surface them.
193
+ if (error instanceof ReportSchemaValidationError)
194
+ throw error;
182
195
  console.warn(` ⚠️ Failed to read report from Sanity: ${error instanceof Error ? error.message : String(error)}`);
183
196
  return null;
184
197
  }
@@ -189,36 +202,30 @@ export class ReportStore {
189
202
  * Creates an immutable `ailf.report` document. The document _id is
190
203
  * prefixed with `report-` for easy GROQ filtering.
191
204
  *
192
- * @returns The report ID on success, null on failure (logged, not thrown)
205
+ * @returns The report ID on success, null on Sanity API failure (logged,
206
+ * not thrown — P5 local-first).
207
+ * @throws {ReportSchemaValidationError} if the report fails the W0191
208
+ * runtime schema gate. Schema drift is a bug, not an outage.
193
209
  */
194
210
  async write(report) {
211
+ // W0191 runtime gate — parse the wire payload before sending it to the
212
+ // Content Lake so producer-side drift surfaces here rather than as
213
+ // silent undefined-propagation in Studio. Mirrors the W0073 gate in
214
+ // ContentLakeTaskSource. ZodErrors throw; network/auth failures
215
+ // continue to use the P5 log-and-continue path below.
216
+ //
217
+ // The schema validates the wire shape (top-level `id`, not Sanity's
218
+ // `_id`/`reportId`), so we hand it the SanityReportDoc with `id` added.
219
+ // ReportSchema is `.passthrough()` at the top level — the Sanity-only
220
+ // fields (`_id`, `_type`, `reportId`) ride through harmlessly.
221
+ const sanityDoc = toSanityReportDoc(report);
222
+ const parsed = ReportSchema.safeParse({ ...sanityDoc, id: report.id });
223
+ if (!parsed.success) {
224
+ throw new ReportSchemaValidationError(`ReportStore.write: ailf.report "${report.id}" failed schema validation:\n` +
225
+ formatReportZodIssues(parsed.error));
226
+ }
195
227
  try {
196
- // Strip baseline and experiment ScoreSummary objects from comparison
197
- // before persisting — they duplicate report.summary (experiment) and
198
- // are fetchable by ID via provenance.lineage.comparedAgainst (baseline).
199
- // This reduces document size by ~50-65% for full-mode reports.
200
- const comparison = report.comparison
201
- ? stripComparisonBulk(report.comparison)
202
- : null;
203
- await this.client.create({
204
- _id: `report-${report.id}`,
205
- _type: REPORT_TYPE,
206
- comparison,
207
- completedAt: report.completedAt,
208
- durationMs: report.durationMs,
209
- provenance: report.provenance,
210
- reportId: report.id,
211
- summary: {
212
- ...report.summary,
213
- // Artifact references live inside summary in Sanity so they're
214
- // projected automatically by the reportDetailQuery (D0032)
215
- ...(report.artifactManifest
216
- ? { artifactManifest: report.artifactManifest }
217
- : {}),
218
- },
219
- tag: report.tag ?? null,
220
- title: report.title ?? null,
221
- });
228
+ await this.client.create(sanityDoc);
222
229
  return report.id;
223
230
  }
224
231
  catch (error) {
@@ -279,13 +286,71 @@ export function generateReportId() {
279
286
  // ---------------------------------------------------------------------------
280
287
  // Sanity document → Report mapping
281
288
  // ---------------------------------------------------------------------------
289
+ /**
290
+ * Tagged error thrown by `toReport` and `ReportStore.write` when a Content
291
+ * Lake document fails the W0191 runtime schema gate. Read paths
292
+ * (`read`, `findByFingerprint`, `findComparableBaseline`) re-throw this
293
+ * error rather than swallowing it via the P5 log-and-continue try/catch
294
+ * — schema drift is a bug to be surfaced, not an outage to be tolerated.
295
+ */
296
+ export class ReportSchemaValidationError extends Error {
297
+ constructor(message) {
298
+ super(message);
299
+ this.name = "ReportSchemaValidationError";
300
+ }
301
+ }
302
+ export function toSanityReportDoc(report) {
303
+ const comparison = report.comparison
304
+ ? stripComparisonBulk(report.comparison)
305
+ : null;
306
+ return {
307
+ _id: `report-${report.id}`,
308
+ _type: REPORT_TYPE,
309
+ comparison,
310
+ completedAt: report.completedAt,
311
+ durationMs: report.durationMs,
312
+ provenance: report.provenance,
313
+ reportId: report.id,
314
+ summary: {
315
+ ...report.summary,
316
+ // Artifact references live inside summary in Sanity so they're
317
+ // projected automatically by the reportDetailQuery (D0032)
318
+ ...(report.artifactManifest
319
+ ? { artifactManifest: report.artifactManifest }
320
+ : {}),
321
+ },
322
+ tag: report.tag ?? null,
323
+ title: report.title ?? null,
324
+ };
325
+ }
282
326
  /**
283
327
  * Convert a raw Sanity document to a typed Report.
284
328
  *
285
329
  * The Sanity document shape mirrors the Report type but includes Sanity
286
330
  * metadata (_id, _type, _rev, etc.) that we strip.
287
331
  */
288
- function toReport(doc) {
332
+ export function toReport(doc) {
333
+ // W0191 runtime gate — parse the raw Sanity document before constructing
334
+ // the typed Report. The schema has `id` at the top level (matching the
335
+ // wire shape used on write); the stored document carries the same value
336
+ // under `reportId`, so build the parse candidate from `reportId` and
337
+ // surface the document `_id` (or `reportId`) on parse failures so
338
+ // operators can locate the malformed record.
339
+ const reportIdRaw = doc.reportId;
340
+ const candidate = {
341
+ ...doc,
342
+ id: reportIdRaw,
343
+ };
344
+ const parsed = ReportSchema.safeParse(candidate);
345
+ if (!parsed.success) {
346
+ const docKey = typeof doc._id === "string" && doc._id.length > 0
347
+ ? doc._id
348
+ : typeof reportIdRaw === "string" && reportIdRaw.length > 0
349
+ ? reportIdRaw
350
+ : "<unknown>";
351
+ throw new ReportSchemaValidationError(`ReportStore.toReport: ailf.report "${docKey}" failed schema ` +
352
+ `validation:\n${formatReportZodIssues(parsed.error)}`);
353
+ }
289
354
  const summary = doc.summary;
290
355
  const artifactManifest = summary?.artifactManifest;
291
356
  return {
@@ -300,6 +365,21 @@ function toReport(doc) {
300
365
  title: doc.title,
301
366
  };
302
367
  }
368
+ /**
369
+ * Format the first 5 ZodError issues for human-readable error output.
370
+ * Mirrors the W0073 ContentLakeTaskSource formatter so the two Content
371
+ * Lake gates produce comparable error shapes.
372
+ */
373
+ function formatReportZodIssues(error) {
374
+ const issues = error.issues
375
+ .slice(0, 5)
376
+ .map((i) => ` [${i.path.join(".")}]: ${i.message}`)
377
+ .join("\n");
378
+ const more = error.issues.length > 5
379
+ ? `\n …and ${error.issues.length - 5} more issue(s)`
380
+ : "";
381
+ return `${issues}${more}`;
382
+ }
303
383
  /**
304
384
  * Remove the `baseline` and `experiment` ScoreSummary objects from a
305
385
  * ComparisonReport, producing a slim copy suitable for persistence.
@@ -310,7 +390,7 @@ function toReport(doc) {
310
390
  *
311
391
  * Everything else (deltas, areas, classifications) is preserved.
312
392
  */
313
- function stripComparisonBulk(comparison) {
393
+ export function stripComparisonBulk(comparison) {
314
394
  const { baseline: _, experiment: __, ...slim } = comparison;
315
395
  return slim;
316
396
  }
@@ -36,3 +36,61 @@ export declare function createPublishedClient(source?: ResolvedSourceConfig): Sa
36
36
  * passed as a stacked array: [perspectiveId, "published"].
37
37
  */
38
38
  export declare function getSanityClient(overrides?: Partial<SanityConfig>, source?: ResolvedSourceConfig): SanityClient;
39
+ /**
40
+ * Get a Sanity client targeting the AILF private dataset.
41
+ *
42
+ * Use this for reads and writes against `ailf.*` document types. The
43
+ * returned client is cached when called without overrides.
44
+ *
45
+ * @param overrides - Per-call config overrides (e.g., explicit token, dataset).
46
+ */
47
+ export declare function getAilfSanityClient(overrides?: Partial<SanityConfig>): SanityClient;
48
+ /**
49
+ * Reset the cached AILF client. Test-only — call from `beforeEach` when
50
+ * mutating env vars between tests so the cached client doesn't leak.
51
+ */
52
+ 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
+ export interface BuildEditorialReferenceOptions {
73
+ /** Override the editorial-side project ID. Defaults to AILF project. */
74
+ projectId?: string;
75
+ /** Override the editorial dataset. Defaults to `AILF_EDITORIAL_DATASET`. */
76
+ dataset?: string;
77
+ /**
78
+ * Mark the reference as weak. Strongly recommended for any AILF→editorial
79
+ * link — keeps the AILF doc publishable when the target is deleted.
80
+ * Defaults to `true`.
81
+ */
82
+ weak?: boolean;
83
+ }
84
+ /**
85
+ * Build a Cross Dataset Reference from an AILF document into editorial content.
86
+ *
87
+ * Use this for any reference field on an `ailf.*` document that points at an
88
+ * editorial type (`article`, `docPage`, …). The schema-side counterpart is a
89
+ * `crossDatasetReference` field with matching `dataset` and `weak: true`.
90
+ *
91
+ * @example
92
+ * const docRef = buildEditorialReference(articleId)
93
+ * // → { _type: "reference", _projectId: "3do82whm",
94
+ * // _dataset: "next", _ref: articleId, _weak: true }
95
+ */
96
+ export declare function buildEditorialReference(refId: string, options?: BuildEditorialReferenceOptions): EditorialReference;
@@ -1,4 +1,14 @@
1
1
  import { createClient } from "@sanity/client";
2
+ // Transitional default for AILF operational documents while D0043 rollout
3
+ // is in progress. Code paths route through `getAilfSanityClient()` so the
4
+ // default flip to `ailf-prod-private` is a single-line change after the
5
+ // migration script (D0043 rollout step 5) ships and runs. Until then the
6
+ // default mirrors the editorial dataset to avoid silent dual-write across
7
+ // the cutover boundary.
8
+ const AILF_DATASET_DEFAULT = "next";
9
+ // Default editorial dataset that AILF cross-dataset references point at.
10
+ // Used by `buildEditorialReference()` and by Studio's CDR field config.
11
+ const EDITORIAL_DATASET_DEFAULT = "next";
2
12
  /**
3
13
  * Build the default Sanity client config by reading process.env at call time.
4
14
  *
@@ -84,3 +94,99 @@ export function getSanityClient(overrides, source) {
84
94
  _client = createClient(config);
85
95
  return _client;
86
96
  }
97
+ // ---------------------------------------------------------------------------
98
+ // AILF private-dataset client (D0043)
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Build the default config for clients that read/write AILF documents
102
+ * (ailf.report, ailf.task, ailf.job, ailf.featureArea, ailf.evalRequest).
103
+ *
104
+ * Dataset precedence: explicit `AILF_REPORT_DATASET` overrides; otherwise
105
+ * fall back to `SANITY_DATASET` so existing CI workflows that pin a
106
+ * test/staging dataset (e.g. Tier 2 with `SANITY_DATASET=ailf-test`)
107
+ * continue to work without a new env var. The hard-coded fallback is
108
+ * the editorial dataset name during the D0043 cutover window — the flip
109
+ * to `ailf-prod-private` happens after the migration script runs.
110
+ *
111
+ * Token resolution prefers the AILF-scoped token, falling back to
112
+ * the shared `SANITY_API_TOKEN`.
113
+ */
114
+ function getAilfDefaultConfig() {
115
+ return {
116
+ apiVersion: new Date().toISOString().split("T")[0],
117
+ dataset:
118
+ // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
119
+ process.env.AILF_REPORT_DATASET ||
120
+ // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
121
+ process.env.SANITY_DATASET ||
122
+ AILF_DATASET_DEFAULT,
123
+ projectId:
124
+ // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
125
+ process.env.AILF_REPORT_PROJECT_ID ||
126
+ // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
127
+ process.env.SANITY_PROJECT_ID ||
128
+ "3do82whm",
129
+ token: process.env.AILF_REPORT_SANITY_API_TOKEN ?? process.env.SANITY_API_TOKEN,
130
+ useCdn: false,
131
+ };
132
+ }
133
+ let _ailfClient = null;
134
+ /**
135
+ * Get a Sanity client targeting the AILF private dataset.
136
+ *
137
+ * Use this for reads and writes against `ailf.*` document types. The
138
+ * returned client is cached when called without overrides.
139
+ *
140
+ * @param overrides - Per-call config overrides (e.g., explicit token, dataset).
141
+ */
142
+ export function getAilfSanityClient(overrides) {
143
+ if (_ailfClient && !overrides) {
144
+ return _ailfClient;
145
+ }
146
+ const config = { ...getAilfDefaultConfig(), ...overrides };
147
+ const client = createClient(config);
148
+ if (!overrides)
149
+ _ailfClient = client;
150
+ return client;
151
+ }
152
+ /**
153
+ * Reset the cached AILF client. Test-only — call from `beforeEach` when
154
+ * mutating env vars between tests so the cached client doesn't leak.
155
+ */
156
+ export function resetAilfSanityClientCache() {
157
+ _ailfClient = null;
158
+ }
159
+ /**
160
+ * Build a Cross Dataset Reference from an AILF document into editorial content.
161
+ *
162
+ * Use this for any reference field on an `ailf.*` document that points at an
163
+ * editorial type (`article`, `docPage`, …). The schema-side counterpart is a
164
+ * `crossDatasetReference` field with matching `dataset` and `weak: true`.
165
+ *
166
+ * @example
167
+ * const docRef = buildEditorialReference(articleId)
168
+ * // → { _type: "reference", _projectId: "3do82whm",
169
+ * // _dataset: "next", _ref: articleId, _weak: true }
170
+ */
171
+ export function buildEditorialReference(refId, options = {}) {
172
+ if (!refId) {
173
+ throw new Error("buildEditorialReference: refId is required");
174
+ }
175
+ // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
176
+ const envProjectId = process.env.AILF_REPORT_PROJECT_ID || process.env.SANITY_PROJECT_ID;
177
+ // Editorial dataset precedence: explicit AILF_EDITORIAL_DATASET overrides;
178
+ // otherwise reuse SANITY_DATASET (which CI workflows already pin to the
179
+ // correct editorial dataset); fall back to "next" only when nothing is set.
180
+ // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
181
+ const envDataset = process.env.AILF_EDITORIAL_DATASET || process.env.SANITY_DATASET;
182
+ const projectId = options.projectId ?? envProjectId ?? "3do82whm";
183
+ const dataset = options.dataset ?? envDataset ?? EDITORIAL_DATASET_DEFAULT;
184
+ const weak = options.weak ?? true;
185
+ return {
186
+ _type: "crossDatasetReference",
187
+ _projectId: projectId,
188
+ _dataset: dataset,
189
+ _ref: refId,
190
+ ...(weak ? { _weak: true } : {}),
191
+ };
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf",
3
- "version": "4.0.6",
3
+ "version": "4.1.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -53,6 +53,7 @@
53
53
  "nock": "^14.0.13",
54
54
  "tsx": "^4.19.2",
55
55
  "typescript": "^5.7.3",
56
+ "vitest": "^4.1.5",
56
57
  "@sanity/ailf-core": "0.1.0",
57
58
  "@sanity/ailf-shared": "0.1.0"
58
59
  },
@@ -74,12 +75,12 @@
74
75
  "cli": "tsx src/cli.ts",
75
76
  "pipeline": "tsx src/cli.ts pipeline",
76
77
  "validate": "tsx src/cli.ts validate config",
77
- "test": "tsx --test src/__tests__/*.test.ts src/adapters/**/__tests__/*.adapter.test.ts",
78
- "test:e2e": "AILF_E2E=1 tsx --test src/__tests__/e2e/*.e2e.test.ts",
79
- "test:e2e:adapters": "AILF_E2E=1 tsx --test src/adapters/**/__tests__/*.adapter.test.ts",
80
- "test:e2e:api": "AILF_E2E_API=1 tsx --test src/__tests__/api-tier2-tenant-integration.test.ts src/__tests__/gcs-artifact-writer-roundtrip.test.ts",
81
- "test:tier3:roundtrip": "AILF_E2E_API=1 AILF_E2E_GITHUB_DISPATCH=1 tsx --test src/__tests__/api-tier3-round-trip.test.ts",
82
- "test:all": "AILF_E2E=1 tsx --test src/__tests__/*.test.ts src/pipeline/compiler/__tests__/*.test.ts src/__tests__/e2e/*.e2e.test.ts src/adapters/**/__tests__/*.adapter.test.ts",
78
+ "test": "vitest run",
79
+ "test:e2e": "AILF_E2E=1 vitest run src/__tests__/e2e",
80
+ "test:e2e:adapters": "AILF_E2E=1 vitest run src/adapters",
81
+ "test:e2e:api": "AILF_E2E_API=1 vitest run src/__tests__/api-tier2-tenant-integration.test.ts src/__tests__/gcs-artifact-writer-roundtrip.test.ts",
82
+ "test:tier3:roundtrip": "AILF_E2E_API=1 AILF_E2E_GITHUB_DISPATCH=1 vitest run src/__tests__/api-tier3-round-trip.test.ts",
83
+ "test:all": "AILF_E2E=1 vitest run",
83
84
  "pr-comment": "tsx src/cli.ts pr-comment",
84
85
  "coverage-audit": "tsx src/cli.ts report coverage",
85
86
  "readiness-report": "tsx src/cli.ts report readiness",
@@ -1,40 +0,0 @@
1
- /**
2
- * Shared task loading for pipeline orchestration steps.
3
- *
4
- * Both FetchDocsStep and GenerateConfigsStep need to see the same set of
5
- * tasks. This function loads from filesystem .task.ts files — the
6
- * authoritative source for the current pipeline architecture.
7
- *
8
- * Background: The composition root wires ctx.taskSource to
9
- * ContentLakeTaskSource by default, but GenerateConfigsStep bypasses it
10
- * and loads directly from the filesystem. FetchDocsStep must use the
11
- * same source to avoid a mismatch where configs reference context files
12
- * that were never fetched.
13
- *
14
- * @see packages/eval/src/orchestration/steps/generate-configs-step.ts
15
- * @see packages/eval/src/orchestration/steps/fetch-docs-step.ts
16
- */
17
- import type { GeneralizedTaskDefinition } from "../_vendor/ailf-core/index.d.ts";
18
- export interface LoadPipelineTasksOptions {
19
- /** Absolute path to the eval package root (packages/eval) */
20
- rootDir: string;
21
- /** Evaluation mode — determines the tasks/{mode}/ subdirectory */
22
- mode: string;
23
- /** Optional extra directory for repo-based tasks (--repo-tasks-path) */
24
- repoTasksPath?: string;
25
- /**
26
- * When `"repo"`, load ONLY from `repoTasksPath` and skip the AILF
27
- * bundled `tasks/${mode}/` directory. Mirrors the composition-root
28
- * contract for `taskSourceType: "repo"` (see composition-root.ts).
29
- */
30
- taskSourceType?: "content-lake" | "repo";
31
- }
32
- /**
33
- * Load task definitions from the filesystem, matching the pipeline's
34
- * authoritative task source.
35
- *
36
- * Discovers and loads `*.task.ts` files from `tasks/{mode}/` and
37
- * optionally `--repo-tasks-path`. Tasks whose `mode` field doesn't
38
- * match the requested mode are excluded.
39
- */
40
- export declare function loadPipelineTasks(opts: LoadPipelineTasksOptions): Promise<GeneralizedTaskDefinition[]>;
@@ -1,57 +0,0 @@
1
- /**
2
- * Shared task loading for pipeline orchestration steps.
3
- *
4
- * Both FetchDocsStep and GenerateConfigsStep need to see the same set of
5
- * tasks. This function loads from filesystem .task.ts files — the
6
- * authoritative source for the current pipeline architecture.
7
- *
8
- * Background: The composition root wires ctx.taskSource to
9
- * ContentLakeTaskSource by default, but GenerateConfigsStep bypasses it
10
- * and loads directly from the filesystem. FetchDocsStep must use the
11
- * same source to avoid a mismatch where configs reference context files
12
- * that were never fetched.
13
- *
14
- * @see packages/eval/src/orchestration/steps/generate-configs-step.ts
15
- * @see packages/eval/src/orchestration/steps/fetch-docs-step.ts
16
- */
17
- import { resolve } from "path";
18
- import { discoverTsTaskFiles, loadTsTaskFile, } from "../adapters/task-sources/task-file-loader.js";
19
- import { resolveVendoredSubdir } from "../pipeline/compiler/config-loader.js";
20
- /**
21
- * Load task definitions from the filesystem, matching the pipeline's
22
- * authoritative task source.
23
- *
24
- * Discovers and loads `*.task.ts` files from `tasks/{mode}/` and
25
- * optionally `--repo-tasks-path`. Tasks whose `mode` field doesn't
26
- * match the requested mode are excluded.
27
- */
28
- export async function loadPipelineTasks(opts) {
29
- const dirs = [];
30
- if (opts.taskSourceType !== "repo") {
31
- dirs.push(resolveVendoredSubdir(opts.rootDir, `tasks/${opts.mode}`));
32
- }
33
- else if (!opts.repoTasksPath) {
34
- throw new Error('taskSourceType "repo" requires repoTasksPath to be set (no AILF defaults loaded in repo-only mode)');
35
- }
36
- if (opts.repoTasksPath) {
37
- const repoDir = resolve(opts.repoTasksPath);
38
- if (!dirs.includes(repoDir)) {
39
- dirs.push(repoDir);
40
- }
41
- }
42
- const tasks = [];
43
- for (const dir of dirs) {
44
- const files = discoverTsTaskFiles(dir);
45
- for (const file of files) {
46
- const raw = await loadTsTaskFile(file);
47
- for (const t of raw.tasks) {
48
- const task = t;
49
- // Filter to matching mode (skip tasks from other modes in same dir)
50
- if (!("mode" in task) || task.mode === opts.mode) {
51
- tasks.push(task);
52
- }
53
- }
54
- }
55
- }
56
- return tasks;
57
- }