@sanity/ailf 4.0.7 → 4.2.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 (104) 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/generalized-task.d.ts +20 -3
  16. package/dist/_vendor/ailf-core/types/index.d.ts +4 -1
  17. package/dist/_vendor/ailf-core/types/pipeline-request.d.ts +112 -0
  18. package/dist/_vendor/ailf-core/types/pipeline-request.js +18 -0
  19. package/dist/_vendor/ailf-core/types/repo-config.d.ts +146 -0
  20. package/dist/_vendor/ailf-core/types/repo-config.js +18 -0
  21. package/dist/_vendor/ailf-shared/index.d.ts +7 -5
  22. package/dist/_vendor/ailf-shared/index.js +7 -5
  23. package/dist/adapters/api-client/types.d.ts +2 -5
  24. package/dist/adapters/doc-fetchers/sanity-doc-fetcher.d.ts +21 -5
  25. package/dist/adapters/doc-fetchers/sanity-doc-fetcher.js +129 -25
  26. package/dist/adapters/task-sources/content-lake-task-source.d.ts +58 -1
  27. package/dist/adapters/task-sources/content-lake-task-source.js +1 -1
  28. package/dist/adapters/task-sources/index.d.ts +1 -1
  29. package/dist/adapters/task-sources/index.js +1 -1
  30. package/dist/adapters/task-sources/repo-schemas.d.ts +19 -2
  31. package/dist/adapters/task-sources/repo-schemas.js +81 -2
  32. package/dist/adapters/task-sources/repo-task-source.js +11 -2
  33. package/dist/adapters/task-sources/repo-validation.d.ts +6 -6
  34. package/dist/adapters/task-sources/repo-validation.js +1 -1
  35. package/dist/agent-observer/agentic-provider.d.ts +1 -0
  36. package/dist/agent-observer/agentic-provider.js +43 -36
  37. package/dist/agent-observer/config-schemas.d.ts +61 -0
  38. package/dist/agent-observer/config-schemas.js +65 -0
  39. package/dist/agent-observer/provider.d.ts +1 -0
  40. package/dist/agent-observer/provider.js +19 -17
  41. package/dist/cli.js +4 -4
  42. package/dist/commands/validate-tasks.js +10 -4
  43. package/dist/composition-root.js +4 -2
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/job-store.js +2 -2
  47. package/dist/lib/dotenv-resolution.d.ts +21 -0
  48. package/dist/lib/dotenv-resolution.js +30 -0
  49. package/dist/orchestration/steps/mirror-repo-tasks-step.js +14 -3
  50. package/dist/orchestration/steps/run-eval-step.js +21 -3
  51. package/dist/pipeline/agent-behavior-report.d.ts +2 -8
  52. package/dist/pipeline/cache.d.ts +2 -2
  53. package/dist/pipeline/checks.d.ts +10 -2
  54. package/dist/pipeline/checks.js +14 -4
  55. package/dist/pipeline/compiler/literacy-bridge.js +2 -2
  56. package/dist/pipeline/compiler/mode-handlers/__fixtures__/agent-harness-example-tasks.js +0 -12
  57. package/dist/pipeline/compiler/mode-handlers/__fixtures__/knowledge-probe-example-tasks.js +0 -12
  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/compiler.js +44 -5
  62. package/dist/pipeline/compiler/mode-handlers/literacy/index.d.ts +1 -1
  63. package/dist/pipeline/compiler/mode-handlers/literacy/types.d.ts +3 -3
  64. package/dist/pipeline/compiler/promptfoo-compiler.js +7 -11
  65. package/dist/pipeline/compiler/provider-assembler.js +33 -3
  66. package/dist/pipeline/compiler/rubric-resolution.d.ts +2 -2
  67. package/dist/pipeline/mirror-repo-tasks.d.ts +13 -5
  68. package/dist/pipeline/mirror-repo-tasks.js +16 -8
  69. package/dist/pipeline/pr-comment.d.ts +22 -9
  70. package/dist/pipeline/pr-comment.js +52 -472
  71. package/dist/pipeline/resolve-mappings.d.ts +8 -3
  72. package/dist/promptfoo-providers/mock-path.d.ts +12 -0
  73. package/dist/promptfoo-providers/mock-path.js +15 -0
  74. package/dist/report-store.d.ts +63 -1
  75. package/dist/report-store.js +111 -31
  76. package/dist/sanity/client.d.ts +58 -0
  77. package/dist/sanity/client.js +106 -0
  78. package/dist/sanity/document-renderers.d.ts +68 -0
  79. package/dist/sanity/document-renderers.js +221 -0
  80. package/dist/sanity/queries.d.ts +21 -0
  81. package/dist/sanity/queries.js +71 -0
  82. package/dist/tasks/knowledge-probe/define-type-api.task.ts +2 -6
  83. package/dist/tasks/knowledge-probe/groq-projections.task.ts +0 -5
  84. package/dist/tasks/literacy/content-lake.task.ts +4 -10
  85. package/dist/tasks/literacy/frameworks.task.ts +2 -8
  86. package/dist/tasks/literacy/functions.task.ts +1 -4
  87. package/dist/tasks/literacy/groq.task.ts +3 -12
  88. package/dist/tasks/literacy/image-handling.task.ts +1 -4
  89. package/dist/tasks/literacy/nextjs-live.task.ts +1 -4
  90. package/dist/tasks/literacy/portable-text.task.ts +2 -8
  91. package/dist/tasks/literacy/studio-setup.task.ts +2 -8
  92. package/dist/tasks/literacy/visual-editing.task.ts +2 -8
  93. package/package.json +8 -7
  94. package/tasks/knowledge-probe/define-type-api.task.ts +2 -6
  95. package/tasks/knowledge-probe/groq-projections.task.ts +0 -5
  96. package/tasks/literacy/content-lake.task.ts +4 -10
  97. package/tasks/literacy/frameworks.task.ts +2 -8
  98. package/tasks/literacy/functions.task.ts +1 -4
  99. package/tasks/literacy/groq.task.ts +3 -12
  100. package/tasks/literacy/image-handling.task.ts +1 -4
  101. package/tasks/literacy/nextjs-live.task.ts +1 -4
  102. package/tasks/literacy/portable-text.task.ts +2 -8
  103. package/tasks/literacy/studio-setup.task.ts +2 -8
  104. package/tasks/literacy/visual-editing.task.ts +2 -8
@@ -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
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * document-renderers.ts
3
+ *
4
+ * Renderer registry that turns a Sanity document fetched by `_id` into
5
+ * Markdown for inclusion in a literacy task's grader context.
6
+ *
7
+ * The resolver fetches docs without an `_type` filter and dispatches here.
8
+ * Two tiers of fidelity:
9
+ *
10
+ * 1. Registered renderers (high fidelity) — `article`, `typesReference`.
11
+ * Hand-written for the document shapes we care about most.
12
+ * 2. Default renderer (best effort) — walks the doc, flattens portable-text
13
+ * fields, surfaces top-level scalars, follows references one level deep,
14
+ * skips framework-internal fields. Lets pinning a `marketingPage`,
15
+ * `glossaryEntry`, etc. work without AILF code changes.
16
+ *
17
+ * Adding a new high-fidelity renderer: implement a `DocumentRenderer` and
18
+ * register it in `BUILT_IN_RENDERERS` keyed by `_type`.
19
+ */
20
+ /**
21
+ * A Sanity document plus any references we've already resolved for it.
22
+ * The resolver fetches the doc once and may include common deref payloads
23
+ * (e.g. `latestVersion->{...}` for typesReference) so renderers don't need
24
+ * to issue additional client.fetch calls themselves.
25
+ */
26
+ export interface DocumentForRender {
27
+ _id: string;
28
+ _type: string;
29
+ [key: string]: unknown;
30
+ }
31
+ export interface RenderContext {
32
+ /**
33
+ * Async URL fetcher for renderers that need to follow `sanity.fileAsset`
34
+ * URLs (e.g. typedoc JSON for `typesReference` docs). Returns the raw
35
+ * body text or `null` on failure.
36
+ */
37
+ fetchUrl?: (url: string) => Promise<string | null>;
38
+ /**
39
+ * Soft cap on rendered content length, in bytes. Renderers should respect
40
+ * this and append a truncation notice rather than blow up grader context.
41
+ */
42
+ maxBytes?: number;
43
+ }
44
+ export interface RenderResult {
45
+ /** The rendered Markdown. Empty string is permitted but produces a warn. */
46
+ content: string;
47
+ /**
48
+ * The fidelity tier used. Resolvers can log differently based on this:
49
+ * - "high" — a registered renderer ran
50
+ * - "default" — fell through to the generic walker (info log)
51
+ */
52
+ fidelity: "high" | "default";
53
+ /**
54
+ * Stable display slug for this document. Articles use `slug.current`;
55
+ * other types fall back to a `<_type>:<_id>` form when no slug exists.
56
+ * Surfaces in `DocContext.slugs` for retrieval-metric attribution.
57
+ */
58
+ slug: string;
59
+ }
60
+ export type DocumentRenderer = (doc: DocumentForRender, ctx: RenderContext) => Promise<RenderResult> | RenderResult;
61
+ /**
62
+ * Render a document using the registered renderer for its `_type`, falling
63
+ * back to the default walker. The returned `fidelity` flag tells callers
64
+ * whether to emit the "info: dedicated renderer would help" log.
65
+ */
66
+ export declare function renderDocument(doc: DocumentForRender, ctx?: RenderContext): Promise<RenderResult>;
67
+ /** Exported for tests and consumers that want the registered set. */
68
+ export declare const REGISTERED_RENDERER_TYPES: string[];