@sanity/ailf 4.1.0 → 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.
- package/dist/_vendor/ailf-core/types/generalized-task.d.ts +20 -3
- package/dist/_vendor/ailf-core/types/index.d.ts +1 -1
- package/dist/adapters/doc-fetchers/sanity-doc-fetcher.d.ts +21 -5
- package/dist/adapters/doc-fetchers/sanity-doc-fetcher.js +129 -25
- package/dist/adapters/task-sources/repo-schemas.d.ts +16 -0
- package/dist/adapters/task-sources/repo-schemas.js +78 -1
- package/dist/adapters/task-sources/repo-task-source.js +11 -2
- package/dist/commands/validate-tasks.js +8 -2
- package/dist/pipeline/compiler/mode-handlers/__fixtures__/agent-harness-example-tasks.js +0 -12
- package/dist/pipeline/compiler/mode-handlers/__fixtures__/knowledge-probe-example-tasks.js +0 -12
- package/dist/pipeline/compiler/mode-handlers/literacy/compiler.js +44 -5
- package/dist/sanity/document-renderers.d.ts +68 -0
- package/dist/sanity/document-renderers.js +221 -0
- package/dist/sanity/queries.d.ts +21 -0
- package/dist/sanity/queries.js +71 -0
- package/dist/tasks/knowledge-probe/define-type-api.task.ts +2 -6
- package/dist/tasks/knowledge-probe/groq-projections.task.ts +0 -5
- package/dist/tasks/literacy/content-lake.task.ts +4 -10
- package/dist/tasks/literacy/frameworks.task.ts +2 -8
- package/dist/tasks/literacy/functions.task.ts +1 -4
- package/dist/tasks/literacy/groq.task.ts +3 -12
- package/dist/tasks/literacy/image-handling.task.ts +1 -4
- package/dist/tasks/literacy/nextjs-live.task.ts +1 -4
- package/dist/tasks/literacy/portable-text.task.ts +2 -8
- package/dist/tasks/literacy/studio-setup.task.ts +2 -8
- package/dist/tasks/literacy/visual-editing.task.ts +2 -8
- package/package.json +1 -1
- package/tasks/knowledge-probe/define-type-api.task.ts +2 -6
- package/tasks/knowledge-probe/groq-projections.task.ts +0 -5
- package/tasks/literacy/content-lake.task.ts +4 -10
- package/tasks/literacy/frameworks.task.ts +2 -8
- package/tasks/literacy/functions.task.ts +1 -4
- package/tasks/literacy/groq.task.ts +3 -12
- package/tasks/literacy/image-handling.task.ts +1 -4
- package/tasks/literacy/nextjs-live.task.ts +1 -4
- package/tasks/literacy/portable-text.task.ts +2 -8
- package/tasks/literacy/studio-setup.task.ts +2 -8
- package/tasks/literacy/visual-editing.task.ts +2 -8
|
@@ -97,6 +97,19 @@ export interface TaskOptions {
|
|
|
97
97
|
/** Arbitrary Promptfoo overrides (escape hatch) */
|
|
98
98
|
promptfooOverrides?: Record<string, unknown>;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Variable keys reserved by the AILF compilers. Populated automatically
|
|
102
|
+
* from canonical task fields; forbidden in `prompt.vars` to prevent silent
|
|
103
|
+
* override of canonical values.
|
|
104
|
+
*/
|
|
105
|
+
export type ReservedPromptVarKey = "task" | "docs" | "__featureArea";
|
|
106
|
+
/**
|
|
107
|
+
* Variables for prompt-template interpolation. Freeform extras are
|
|
108
|
+
* allowed; reserved keys (see {@link ReservedPromptVarKey}) are forbidden.
|
|
109
|
+
*/
|
|
110
|
+
export type PromptVars = Record<string, unknown> & {
|
|
111
|
+
[K in ReservedPromptVarKey]?: never;
|
|
112
|
+
};
|
|
100
113
|
/** Fields shared by all task modes */
|
|
101
114
|
export interface TaskCommonFields {
|
|
102
115
|
/** Unique task identifier */
|
|
@@ -121,7 +134,11 @@ export interface TaskCommonFields {
|
|
|
121
134
|
providers?: TaskProviderConfig[];
|
|
122
135
|
/** Task-level execution options */
|
|
123
136
|
options?: TaskOptions;
|
|
124
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Prompt configuration. Reserved `vars` keys (see
|
|
139
|
+
* {@link ReservedPromptVarKey}) are forbidden — use `prompt.text` for
|
|
140
|
+
* the prompt body and `context.docs` for documentation references.
|
|
141
|
+
*/
|
|
125
142
|
prompt?: {
|
|
126
143
|
/** Named prompt template */
|
|
127
144
|
template?: string;
|
|
@@ -129,8 +146,8 @@ export interface TaskCommonFields {
|
|
|
129
146
|
text?: string;
|
|
130
147
|
/** System message override */
|
|
131
148
|
systemMessage?: string;
|
|
132
|
-
/** Variables for template interpolation */
|
|
133
|
-
vars?:
|
|
149
|
+
/** Variables for template interpolation (reserved keys forbidden) */
|
|
150
|
+
vars?: PromptVars;
|
|
134
151
|
};
|
|
135
152
|
/** Arbitrary metadata */
|
|
136
153
|
metadata?: Record<string, unknown>;
|
|
@@ -28,7 +28,7 @@ export type { AilfEvalWorkflow, AilfEvalWorkflowJob, AilfEvalWorkflowStep, RepoA
|
|
|
28
28
|
export type { PipelineRequest, PipelineRequestCallback, PipelineRequestCallerExecutor, PipelineRequestCallerGit, PipelineRequestCallerOwner, PipelineRequestDebug, PipelineRequestTaskSource, } from "./pipeline-request.js";
|
|
29
29
|
export type { ArtifactId, AssociationAxis, AssociationValues, Brand, EntryKey, Err, FixtureId, IdValidationError, NewReportId, Ok, ProviderId, PromptId, Result, ResultId, RubricId, RunFingerprint, RunId, SuiteId, TaskId, TaskSlug, TraceId, } from "./branded-ids.js";
|
|
30
30
|
export { err, fixtureId, generateRunId, ok, providerId, resultId, runId, suiteId, taskId, traceId, } from "./branded-ids.js";
|
|
31
|
-
export type { AgentHarnessTaskDefinition, ContentLakeAuthorableMode, ContentLakeAuthorableTask, CustomTaskDefinition, GeneralizedAssertionDefinition, GeneralizedDocRef, GeneralizedTaskDefinition, GeneralizedTemplatedAssertion, GeneralizedValueAssertion, IdDocRef, KnowledgeProbeTaskDefinition, LiteracyTaskDefinition, MCPServerTaskDefinition, PathDocRef, PerspectiveDocRef, RubricRef, SlugDocRef, TaskCommonFields, TaskDifficulty, TaskOptions, TaskProviderConfig, TaskStatus, } from "./generalized-task.js";
|
|
31
|
+
export type { AgentHarnessTaskDefinition, ContentLakeAuthorableMode, ContentLakeAuthorableTask, CustomTaskDefinition, GeneralizedAssertionDefinition, GeneralizedDocRef, GeneralizedTaskDefinition, GeneralizedTemplatedAssertion, GeneralizedValueAssertion, IdDocRef, KnowledgeProbeTaskDefinition, LiteracyTaskDefinition, MCPServerTaskDefinition, PromptVars, PathDocRef, PerspectiveDocRef, ReservedPromptVarKey, RubricRef, SlugDocRef, TaskCommonFields, TaskDifficulty, TaskOptions, TaskProviderConfig, TaskStatus, } from "./generalized-task.js";
|
|
32
32
|
type DocumentRef = _DocumentRef;
|
|
33
33
|
/** Aggregated retrieval metrics for a feature area */
|
|
34
34
|
export interface AreaRetrievalMetrics {
|
|
@@ -21,13 +21,29 @@ export declare class SanityDocFetcher implements DocFetcher {
|
|
|
21
21
|
private fetchInternal;
|
|
22
22
|
private fetchManifest;
|
|
23
23
|
/**
|
|
24
|
-
* Batch-resolve document ID refs
|
|
24
|
+
* Batch-resolve document ID refs without filtering on `_type`.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Articles are routed back into the slug-flow (so manifest, perspective
|
|
27
|
+
* diffing, and overlay handling continue to work unchanged). Non-articles
|
|
28
|
+
* are rendered eagerly via the renderer registry — `typesReference` gets
|
|
29
|
+
* a high-fidelity formatter; anything else falls through to the default
|
|
30
|
+
* walker so authors can pin marketing pages, glossary entries, etc.
|
|
31
|
+
* without an AILF code change.
|
|
32
|
+
*
|
|
33
|
+
* Three log shapes:
|
|
34
|
+
* - `info` — doc rendered with the default formatter (suggests a
|
|
35
|
+
* dedicated renderer would improve fidelity)
|
|
36
|
+
* - `warn` — registered renderer produced empty content (W0195 AC#3)
|
|
37
|
+
* - `warn` — id had no matching document (existed/wrong tenant/typo)
|
|
38
|
+
*/
|
|
39
|
+
private resolveIdRefs;
|
|
40
|
+
/**
|
|
41
|
+
* Fetch the raw body of a Sanity file asset URL, used by the
|
|
42
|
+
* `typesReference` renderer to inline typedoc JSON. Returns `null`
|
|
43
|
+
* on any HTTP/network failure rather than throwing — the renderer
|
|
44
|
+
* surfaces a placeholder so the rest of the context still renders.
|
|
29
45
|
*/
|
|
30
|
-
private
|
|
46
|
+
private fetchAttachmentBody;
|
|
31
47
|
/**
|
|
32
48
|
* Resolve path-based canonical doc references to their article slugs.
|
|
33
49
|
*
|
|
@@ -18,8 +18,9 @@ import { join } from "path";
|
|
|
18
18
|
import { canonicalDocRefLabel, isIdRef, isPathRef, isPerspectiveRef, isSlugRef, } from "../../_vendor/ailf-core/index.js";
|
|
19
19
|
import { fetchUrlContent, } from "../../pipeline/fetch-url-content.js";
|
|
20
20
|
import { createPerspectiveClient, createPublishedClient, getSanityClient, } from "../../sanity/client.js";
|
|
21
|
+
import { renderDocument, } from "../../sanity/document-renderers.js";
|
|
21
22
|
import { toMarkdown } from "../../sanity/portable-text.js";
|
|
22
|
-
import { ALL_ARTICLES_QUERY, ALL_FEATURE_AREAS, ARTICLE_BY_ID_QUERY, ARTICLE_BY_SLUG_QUERY, ARTICLE_BY_SLUG_WITH_PERSPECTIVE_QUERY, ARTICLE_SLUG_BY_PATH_QUERY, ARTICLE_SLUG_BY_SECTION_PATH_QUERY, ARTICLES_IN_RELEASE_QUERY, ARTICLES_METADATA_BY_SLUGS_QUERY, FEATURE_AREA_QUERIES, } from "../../sanity/queries.js";
|
|
23
|
+
import { ALL_ARTICLES_QUERY, ALL_FEATURE_AREAS, ARTICLE_BY_ID_QUERY, ARTICLE_BY_SLUG_QUERY, ARTICLE_BY_SLUG_WITH_PERSPECTIVE_QUERY, ARTICLE_SLUG_BY_PATH_QUERY, ARTICLE_SLUG_BY_SECTION_PATH_QUERY, ARTICLES_IN_RELEASE_QUERY, ARTICLES_METADATA_BY_SLUGS_QUERY, DOCS_BY_IDS_QUERY, FEATURE_AREA_QUERIES, } from "../../sanity/queries.js";
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Helpers
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -99,9 +100,11 @@ export class SanityDocFetcher {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
|
-
// Resolve ID refs
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Resolve ID refs. Articles get added to allSlugs (legacy slug-flow takes
|
|
104
|
+
// over for fetch + render). Non-articles render directly via the renderer
|
|
105
|
+
// registry and bypass the slug-keyed content map.
|
|
106
|
+
const idResolution = await this.resolveIdRefs(idRefs, source);
|
|
107
|
+
for (const slug of idResolution.idToSlug.values()) {
|
|
105
108
|
allSlugs.add(slug);
|
|
106
109
|
}
|
|
107
110
|
// Resolve path refs → slugs
|
|
@@ -118,8 +121,14 @@ export class SanityDocFetcher {
|
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
const metadata = {};
|
|
121
|
-
// 2. Fetch document manifest (traceability metadata)
|
|
124
|
+
// 2. Fetch document manifest (traceability metadata). Articles flow
|
|
125
|
+
// through fetchManifest; non-article id-refs contribute their own
|
|
126
|
+
// entries from the resolution we already did above.
|
|
122
127
|
const manifest = await this.fetchManifest(allSlugs, source);
|
|
128
|
+
for (const extra of idResolution.extraManifest) {
|
|
129
|
+
manifest.push(extra);
|
|
130
|
+
}
|
|
131
|
+
manifest.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
123
132
|
if (manifest.length > 0) {
|
|
124
133
|
metadata.manifest = manifest;
|
|
125
134
|
}
|
|
@@ -232,12 +241,22 @@ export class SanityDocFetcher {
|
|
|
232
241
|
}
|
|
233
242
|
continue;
|
|
234
243
|
}
|
|
244
|
+
// Non-article id-refs bypass the slug-keyed content map: their
|
|
245
|
+
// rendered Markdown was produced by `resolveIdRefs` and is cached
|
|
246
|
+
// by document `_id`. Article id-refs continue to go through the
|
|
247
|
+
// slug-flow below (consistent with overlay/perspective handling).
|
|
248
|
+
if (isIdRef(ref) && idResolution.contentById.has(ref.id)) {
|
|
249
|
+
const entry = idResolution.contentById.get(ref.id);
|
|
250
|
+
parts.push(entry.content);
|
|
251
|
+
slugs.push(entry.slug);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
235
254
|
let slug;
|
|
236
255
|
if (isSlugRef(ref)) {
|
|
237
256
|
slug = ref.slug;
|
|
238
257
|
}
|
|
239
258
|
else if (isIdRef(ref)) {
|
|
240
|
-
slug = idToSlug.get(ref.id);
|
|
259
|
+
slug = idResolution.idToSlug.get(ref.id);
|
|
241
260
|
}
|
|
242
261
|
else if (isPathRef(ref)) {
|
|
243
262
|
slug = pathToSlug.get(ref.path);
|
|
@@ -289,40 +308,125 @@ export class SanityDocFetcher {
|
|
|
289
308
|
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
290
309
|
}
|
|
291
310
|
// -----------------------------------------------------------------------
|
|
292
|
-
// Private: Resolve ID refs
|
|
311
|
+
// Private: Resolve ID refs (type-agnostic)
|
|
293
312
|
// -----------------------------------------------------------------------
|
|
294
313
|
/**
|
|
295
|
-
* Batch-resolve document ID refs
|
|
314
|
+
* Batch-resolve document ID refs without filtering on `_type`.
|
|
315
|
+
*
|
|
316
|
+
* Articles are routed back into the slug-flow (so manifest, perspective
|
|
317
|
+
* diffing, and overlay handling continue to work unchanged). Non-articles
|
|
318
|
+
* are rendered eagerly via the renderer registry — `typesReference` gets
|
|
319
|
+
* a high-fidelity formatter; anything else falls through to the default
|
|
320
|
+
* walker so authors can pin marketing pages, glossary entries, etc.
|
|
321
|
+
* without an AILF code change.
|
|
296
322
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
323
|
+
* Three log shapes:
|
|
324
|
+
* - `info` — doc rendered with the default formatter (suggests a
|
|
325
|
+
* dedicated renderer would improve fidelity)
|
|
326
|
+
* - `warn` — registered renderer produced empty content (W0195 AC#3)
|
|
327
|
+
* - `warn` — id had no matching document (existed/wrong tenant/typo)
|
|
300
328
|
*/
|
|
301
|
-
async
|
|
302
|
-
const
|
|
329
|
+
async resolveIdRefs(idRefs, source) {
|
|
330
|
+
const idToSlug = new Map();
|
|
331
|
+
const contentById = new Map();
|
|
332
|
+
const extraManifest = [];
|
|
303
333
|
if (idRefs.length === 0)
|
|
304
|
-
return
|
|
334
|
+
return { idToSlug, contentById, extraManifest };
|
|
305
335
|
const uniqueIds = [...new Set(idRefs.map((r) => r.id))];
|
|
336
|
+
const taskByRef = new Map();
|
|
337
|
+
for (const { id, taskId } of idRefs) {
|
|
338
|
+
const existing = taskByRef.get(id) ?? [];
|
|
339
|
+
existing.push(taskId);
|
|
340
|
+
taskByRef.set(id, existing);
|
|
341
|
+
}
|
|
306
342
|
const client = source?.perspective
|
|
307
343
|
? createPerspectiveClient(source.perspective, source)
|
|
308
344
|
: getSanityClient(toSanityOverrides(source));
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
345
|
+
const docs = await client.fetch(DOCS_BY_IDS_QUERY, {
|
|
346
|
+
ids: uniqueIds,
|
|
347
|
+
});
|
|
348
|
+
const docsById = new Map(docs.map((d) => [d._id, d]));
|
|
349
|
+
let articleCount = 0;
|
|
350
|
+
let highFidelity = 0;
|
|
351
|
+
let defaultFidelity = 0;
|
|
352
|
+
for (const id of uniqueIds) {
|
|
353
|
+
const taskIds = taskByRef.get(id) ?? [];
|
|
354
|
+
const doc = docsById.get(id);
|
|
355
|
+
if (!doc) {
|
|
356
|
+
console.warn(` [warn] doc "${id}" not found (referenced by task(s): ${taskIds.join(", ")})`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
// Article id-refs flow back into the slug-keyed pipeline so manifest,
|
|
360
|
+
// perspective diff, and document overlay continue to work. The slug
|
|
361
|
+
// projection in DOCS_BY_IDS_QUERY mirrors ARTICLE_PROJECTION.
|
|
362
|
+
if (doc._type === "article") {
|
|
363
|
+
const slug = doc.slug;
|
|
364
|
+
if (typeof slug === "string" && slug.length > 0) {
|
|
365
|
+
idToSlug.set(id, slug);
|
|
366
|
+
articleCount += 1;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.warn(` [warn] article "${id}" has no slug (referenced by task(s): ${taskIds.join(", ")})`);
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const rendered = await renderDocument(doc, {
|
|
374
|
+
fetchUrl: this.fetchAttachmentBody,
|
|
375
|
+
});
|
|
376
|
+
if (!rendered.content) {
|
|
377
|
+
console.warn(` [warn] doc "${id}" resolved as "${doc._type}" but produced empty context (referenced by task(s): ${taskIds.join(", ")})`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
contentById.set(id, {
|
|
381
|
+
content: rendered.content,
|
|
382
|
+
slug: rendered.slug,
|
|
383
|
+
type: doc._type,
|
|
384
|
+
});
|
|
385
|
+
const _rev = doc._rev;
|
|
386
|
+
const title = doc.title;
|
|
387
|
+
extraManifest.push({
|
|
388
|
+
_id: doc._id,
|
|
389
|
+
_rev: typeof _rev === "string" ? _rev : "",
|
|
390
|
+
slug: rendered.slug,
|
|
391
|
+
title: typeof title === "string" ? title : `(${doc._type})`,
|
|
392
|
+
});
|
|
393
|
+
if (rendered.fidelity === "default") {
|
|
394
|
+
console.log(` [info] doc "${id}" rendered with default formatter — a dedicated renderer for "${doc._type}" would likely improve grader fidelity`);
|
|
395
|
+
defaultFidelity += 1;
|
|
316
396
|
}
|
|
317
397
|
else {
|
|
318
|
-
|
|
398
|
+
highFidelity += 1;
|
|
319
399
|
}
|
|
320
400
|
}
|
|
321
|
-
if (
|
|
322
|
-
console.log(` Resolved ${
|
|
401
|
+
if (articleCount > 0) {
|
|
402
|
+
console.log(` Resolved ${articleCount} article id ref(s) to slugs`);
|
|
323
403
|
}
|
|
324
|
-
|
|
404
|
+
if (highFidelity + defaultFidelity > 0) {
|
|
405
|
+
console.log(` Resolved ${highFidelity + defaultFidelity} non-article id ref(s) (${highFidelity} high-fidelity, ${defaultFidelity} default)`);
|
|
406
|
+
}
|
|
407
|
+
return { idToSlug, contentById, extraManifest };
|
|
325
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Fetch the raw body of a Sanity file asset URL, used by the
|
|
411
|
+
* `typesReference` renderer to inline typedoc JSON. Returns `null`
|
|
412
|
+
* on any HTTP/network failure rather than throwing — the renderer
|
|
413
|
+
* surfaces a placeholder so the rest of the context still renders.
|
|
414
|
+
*/
|
|
415
|
+
fetchAttachmentBody = async (url) => {
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch(url);
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
console.warn(` [warn] attachment fetch failed for ${url}: HTTP ${response.status}`);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
return await response.text();
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
426
|
+
console.warn(` [warn] attachment fetch failed for ${url}: ${msg}`);
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
326
430
|
// -----------------------------------------------------------------------
|
|
327
431
|
// Private: Resolve path refs to slugs
|
|
328
432
|
// -----------------------------------------------------------------------
|
|
@@ -1425,6 +1425,22 @@ export declare function parseCanonicalTaskFile(raw: unknown, filename: string):
|
|
|
1425
1425
|
* GeneralizedTaskDefinition shape.
|
|
1426
1426
|
*/
|
|
1427
1427
|
export declare function detectLegacyFieldNames(raw: unknown, filename: string): string[];
|
|
1428
|
+
interface MigrationResult {
|
|
1429
|
+
migrated: unknown;
|
|
1430
|
+
warnings: string[];
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Pre-process legacy `prompt.vars.{task,docs,__featureArea}` into the
|
|
1434
|
+
* canonical shape. Backwards-compatible: legacy-shape tasks continue to
|
|
1435
|
+
* load, but a deprecation warning is emitted per affected task.
|
|
1436
|
+
*
|
|
1437
|
+
* Legacy: prompt: { vars: { task: "...", docs: "file://..." } }
|
|
1438
|
+
* Canonical: prompt: { text: "..." }
|
|
1439
|
+
*
|
|
1440
|
+
* Applies to every task regardless of mode. Per-task dedup: at most one
|
|
1441
|
+
* warning per task per call, listing every reserved key that was present.
|
|
1442
|
+
*/
|
|
1443
|
+
export declare function migratePromptShape(raw: unknown, filename: string): MigrationResult;
|
|
1428
1444
|
/**
|
|
1429
1445
|
* Zod schema for .ailf/config.yaml — controls documentation source,
|
|
1430
1446
|
* report destination, and trigger behavior for evaluations from an
|
|
@@ -141,11 +141,30 @@ const AssertionSchema = z.union([
|
|
|
141
141
|
// ---------------------------------------------------------------------------
|
|
142
142
|
// Shared field schemas — building blocks reused across mode variants
|
|
143
143
|
// ---------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* Variable keys reserved by the AILF compilers — populated automatically
|
|
146
|
+
* from canonical task fields (`prompt.text`, `context.docs`, `area`).
|
|
147
|
+
* Mirrors `ReservedPromptVarKey` in `@sanity/ailf-core`; the `satisfies`
|
|
148
|
+
* clause makes drift a build error.
|
|
149
|
+
*/
|
|
150
|
+
const RESERVED_PROMPT_VAR_KEYS = [
|
|
151
|
+
"task",
|
|
152
|
+
"docs",
|
|
153
|
+
"__featureArea",
|
|
154
|
+
];
|
|
144
155
|
const TaskPromptSchema = z.object({
|
|
145
156
|
template: z.string().optional(),
|
|
146
157
|
text: z.string().optional(),
|
|
147
158
|
systemMessage: z.string().optional(),
|
|
148
|
-
vars: z
|
|
159
|
+
vars: z
|
|
160
|
+
.record(z.string(), z.unknown())
|
|
161
|
+
.refine((vars) => !RESERVED_PROMPT_VAR_KEYS.some((key) => key in vars), {
|
|
162
|
+
message: `prompt.vars contains a reserved key. Reserved keys: ` +
|
|
163
|
+
RESERVED_PROMPT_VAR_KEYS.join(", ") +
|
|
164
|
+
`. Use prompt.text for the prompt body and context.docs for ` +
|
|
165
|
+
`documentation references.`,
|
|
166
|
+
})
|
|
167
|
+
.optional(),
|
|
149
168
|
});
|
|
150
169
|
const RubricRefSchema = z.union([
|
|
151
170
|
z.object({ ref: z.string().min(1) }),
|
|
@@ -416,6 +435,64 @@ export function detectLegacyFieldNames(raw, filename) {
|
|
|
416
435
|
}
|
|
417
436
|
return warnings;
|
|
418
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Pre-process legacy `prompt.vars.{task,docs,__featureArea}` into the
|
|
440
|
+
* canonical shape. Backwards-compatible: legacy-shape tasks continue to
|
|
441
|
+
* load, but a deprecation warning is emitted per affected task.
|
|
442
|
+
*
|
|
443
|
+
* Legacy: prompt: { vars: { task: "...", docs: "file://..." } }
|
|
444
|
+
* Canonical: prompt: { text: "..." }
|
|
445
|
+
*
|
|
446
|
+
* Applies to every task regardless of mode. Per-task dedup: at most one
|
|
447
|
+
* warning per task per call, listing every reserved key that was present.
|
|
448
|
+
*/
|
|
449
|
+
export function migratePromptShape(raw, filename) {
|
|
450
|
+
if (!Array.isArray(raw))
|
|
451
|
+
return { migrated: raw, warnings: [] };
|
|
452
|
+
const warnings = [];
|
|
453
|
+
const migrated = raw.map((entry, i) => {
|
|
454
|
+
if (typeof entry !== "object" || entry === null)
|
|
455
|
+
return entry;
|
|
456
|
+
const obj = entry;
|
|
457
|
+
const prompt = obj.prompt;
|
|
458
|
+
if (typeof prompt !== "object" || prompt === null)
|
|
459
|
+
return entry;
|
|
460
|
+
const promptObj = prompt;
|
|
461
|
+
const vars = promptObj.vars;
|
|
462
|
+
if (typeof vars !== "object" || vars === null)
|
|
463
|
+
return entry;
|
|
464
|
+
const varsObj = vars;
|
|
465
|
+
// Detect which reserved keys are present
|
|
466
|
+
const presentReserved = RESERVED_PROMPT_VAR_KEYS.filter((key) => key in varsObj);
|
|
467
|
+
if (presentReserved.length === 0)
|
|
468
|
+
return entry;
|
|
469
|
+
const taskId = typeof obj.id === "string" ? obj.id : `task[${i}]`;
|
|
470
|
+
// Build migrated prompt + vars
|
|
471
|
+
const newPrompt = { ...promptObj };
|
|
472
|
+
const newVars = { ...varsObj };
|
|
473
|
+
for (const key of presentReserved) {
|
|
474
|
+
if (key === "task" && newPrompt.text === undefined) {
|
|
475
|
+
// Move the prompt body to prompt.text only if the canonical slot
|
|
476
|
+
// is unset; an explicit prompt.text always wins.
|
|
477
|
+
newPrompt.text = newVars.task;
|
|
478
|
+
}
|
|
479
|
+
delete newVars[key];
|
|
480
|
+
}
|
|
481
|
+
// Drop empty vars to keep the migrated shape minimal
|
|
482
|
+
if (Object.keys(newVars).length === 0) {
|
|
483
|
+
delete newPrompt.vars;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
newPrompt.vars = newVars;
|
|
487
|
+
}
|
|
488
|
+
warnings.push(`[${filename}] ${taskId}: deprecated prompt.vars keys ` +
|
|
489
|
+
`(${presentReserved.join(", ")}) — migrated to canonical shape ` +
|
|
490
|
+
`(prompt.text + context.docs). Update the task source to silence ` +
|
|
491
|
+
`this warning.`);
|
|
492
|
+
return { ...obj, prompt: newPrompt };
|
|
493
|
+
});
|
|
494
|
+
return { migrated, warnings };
|
|
495
|
+
}
|
|
419
496
|
// ---------------------------------------------------------------------------
|
|
420
497
|
// Config schemas — specific to the eval pipeline
|
|
421
498
|
// ---------------------------------------------------------------------------
|
|
@@ -22,7 +22,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
|
|
|
22
22
|
import { resolve } from "path";
|
|
23
23
|
import { load } from "js-yaml";
|
|
24
24
|
import { CANONICAL_EVAL_MODES } from "../../_vendor/ailf-shared/index.js";
|
|
25
|
-
import { detectLegacyFieldNames, parseCanonicalTaskFile, } from "./repo-schemas.js";
|
|
25
|
+
import { detectLegacyFieldNames, migratePromptShape, parseCanonicalTaskFile, } from "./repo-schemas.js";
|
|
26
26
|
import { discoverTsTaskFiles, loadTsTaskFile } from "./task-file-loader.js";
|
|
27
27
|
/** Set of canonical mode names for O(1) lookup */
|
|
28
28
|
const KNOWN_MODES = new Set(CANONICAL_EVAL_MODES);
|
|
@@ -69,10 +69,19 @@ export class RepoTaskSource {
|
|
|
69
69
|
legacyWarnings.join("\n") +
|
|
70
70
|
"\n\nSee contributing-tasks.md for the canonical task format.");
|
|
71
71
|
}
|
|
72
|
+
// W0193: pre-migrate legacy prompt.vars.{task,docs,__featureArea}
|
|
73
|
+
// to the canonical prompt.text + context.docs shape. Mode-agnostic —
|
|
74
|
+
// every mode's TaskPromptSchema rejects reserved keys, so the shim
|
|
75
|
+
// unblocks legacy tasks regardless of mode. Per-task deprecation
|
|
76
|
+
// warning fires on stderr.
|
|
77
|
+
const { migrated, warnings: deprecationWarnings } = migratePromptShape(parsed, file);
|
|
78
|
+
for (const warning of deprecationWarnings) {
|
|
79
|
+
console.warn(warning);
|
|
80
|
+
}
|
|
72
81
|
// Validate through canonical Zod schema
|
|
73
82
|
let validated;
|
|
74
83
|
try {
|
|
75
|
-
validated = parseCanonicalTaskFile(
|
|
84
|
+
validated = parseCanonicalTaskFile(migrated, file);
|
|
76
85
|
}
|
|
77
86
|
catch (err) {
|
|
78
87
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -17,7 +17,7 @@ import { existsSync, readdirSync, readFileSync } from "fs";
|
|
|
17
17
|
import { resolve, relative, basename } from "path";
|
|
18
18
|
import { Command } from "commander";
|
|
19
19
|
import { load } from "js-yaml";
|
|
20
|
-
import { detectLegacyFieldNames, parseCanonicalTaskFile, } from "../adapters/task-sources/repo-schemas.js";
|
|
20
|
+
import { detectLegacyFieldNames, migratePromptShape, parseCanonicalTaskFile, } from "../adapters/task-sources/repo-schemas.js";
|
|
21
21
|
import { validateCanonicalTasks, formatRepoValidationResult, } from "../adapters/task-sources/repo-validation.js";
|
|
22
22
|
import { discoverTsTaskFiles, loadTsTaskFile, } from "../adapters/task-sources/task-file-loader.js";
|
|
23
23
|
export function createValidateTasksCommand() {
|
|
@@ -133,8 +133,14 @@ function validateTaskArray(entries, file, accumulator) {
|
|
|
133
133
|
console.error();
|
|
134
134
|
return false;
|
|
135
135
|
}
|
|
136
|
+
// W0193: pre-migrate legacy prompt.vars.{task,docs,__featureArea} shape
|
|
137
|
+
// and surface deprecation warnings (non-fatal — the file still validates).
|
|
138
|
+
const { migrated, warnings: deprecationWarnings } = migratePromptShape(entries, file);
|
|
139
|
+
for (const warning of deprecationWarnings) {
|
|
140
|
+
console.warn(` ${warning}`);
|
|
141
|
+
}
|
|
136
142
|
try {
|
|
137
|
-
const tasks = parseCanonicalTaskFile(
|
|
143
|
+
const tasks = parseCanonicalTaskFile(migrated, file);
|
|
138
144
|
console.log(` ${file}: ${tasks.length} task${tasks.length === 1 ? "" : "s"} valid`);
|
|
139
145
|
accumulator.push(...tasks);
|
|
140
146
|
return true;
|
|
@@ -30,10 +30,6 @@ export const scaffoldProjectTask = {
|
|
|
30
30
|
"2. Configure sanity.config.ts with project ID 'test-project' and dataset 'production'\n" +
|
|
31
31
|
"3. Create a 'post' schema type with title, slug, body, and author fields\n" +
|
|
32
32
|
"4. Ensure the project builds without errors",
|
|
33
|
-
vars: {
|
|
34
|
-
task: "Scaffold a Sanity Studio project with a post schema type. " +
|
|
35
|
-
"The project should build cleanly.",
|
|
36
|
-
},
|
|
37
33
|
},
|
|
38
34
|
assertions: [
|
|
39
35
|
{ type: "file-exists", value: "sanity.config.ts" },
|
|
@@ -70,10 +66,6 @@ export const modifyCodeTask = {
|
|
|
70
66
|
text: "In the existing Sanity Studio project, add a custom document action " +
|
|
71
67
|
"that logs a message before publishing. Follow the Sanity docs for " +
|
|
72
68
|
"custom document actions.",
|
|
73
|
-
vars: {
|
|
74
|
-
task: "Add a custom document action that wraps the default publish action " +
|
|
75
|
-
"and logs 'Publishing document: <title>' before executing.",
|
|
76
|
-
},
|
|
77
69
|
},
|
|
78
70
|
assertions: [
|
|
79
71
|
{ type: "file-exists", value: "actions/logPublishAction.ts" },
|
|
@@ -127,10 +119,6 @@ export const multiFileRefactorTask = {
|
|
|
127
119
|
"3. Query method calls (fetch → client.fetch with new signature)\n" +
|
|
128
120
|
"4. Mutation helpers (create/patch/delete API changes)\n" +
|
|
129
121
|
"Ensure the project compiles after migration.",
|
|
130
|
-
vars: {
|
|
131
|
-
task: "Migrate the codebase from @sanity/client v5 to v6, " +
|
|
132
|
-
"updating all files. Project must compile cleanly after migration.",
|
|
133
|
-
},
|
|
134
122
|
},
|
|
135
123
|
assertions: [
|
|
136
124
|
{
|
|
@@ -38,10 +38,6 @@ export const groqProjectionTask = {
|
|
|
38
38
|
"5. Array slicing with `[0..5]` and `[0...5]`\n" +
|
|
39
39
|
"6. Conditional projections using `select()`\n\n" +
|
|
40
40
|
"Provide working code examples for each.",
|
|
41
|
-
vars: {
|
|
42
|
-
task: "Explain GROQ projection syntax with working code examples " +
|
|
43
|
-
"covering projections, spread, dereference, slicing, and select().",
|
|
44
|
-
},
|
|
45
41
|
},
|
|
46
42
|
assertions: [
|
|
47
43
|
{ type: "contains", value: "->" },
|
|
@@ -89,10 +85,6 @@ export const defineTypeApiTask = {
|
|
|
89
85
|
"3. Why were these typed helpers introduced? What did they replace?\n" +
|
|
90
86
|
"4. Show a complete example of a document schema with various field types\n" +
|
|
91
87
|
"5. How do you add validation rules using the typed API?",
|
|
92
|
-
vars: {
|
|
93
|
-
task: "Explain Sanity's defineType/defineField schema API with examples, " +
|
|
94
|
-
"motivation, and validation rules.",
|
|
95
|
-
},
|
|
96
88
|
},
|
|
97
89
|
assertions: [
|
|
98
90
|
{ type: "contains", value: "defineType" },
|
|
@@ -142,10 +134,6 @@ export const ecosystemComparisonTask = {
|
|
|
142
134
|
"4. Developer experience and customization\n" +
|
|
143
135
|
"5. Pricing models\n" +
|
|
144
136
|
"6. When would you choose one over the other?",
|
|
145
|
-
vars: {
|
|
146
|
-
task: "Compare Sanity and Contentful across architecture, content modeling, " +
|
|
147
|
-
"querying, DX, pricing, and use case fit.",
|
|
148
|
-
},
|
|
149
137
|
},
|
|
150
138
|
assertions: [
|
|
151
139
|
{ type: "contains-any", value: ["GROQ", "groq"] },
|
|
@@ -10,6 +10,18 @@ import { LiteracyVariant, } from "../../../normalize-mode.js";
|
|
|
10
10
|
import { buildBaselineAssertions, resolveAssertions } from "./assertions.js";
|
|
11
11
|
import { LITERACY_PROMPT_TEMPLATES } from "./prompts.js";
|
|
12
12
|
import { validateLiteracyTask } from "./validation.js";
|
|
13
|
+
/**
|
|
14
|
+
* Variable keys reserved by the AILF compilers. Authoring these via
|
|
15
|
+
* `prompt.vars` is rejected by `PromptVars` at compile time and by
|
|
16
|
+
* `TaskPromptSchema` at parse time; this constant exists to defend
|
|
17
|
+
* the literacy compiler at runtime against legacy-shape `*.task.ts`
|
|
18
|
+
* files that bypass both gates.
|
|
19
|
+
*/
|
|
20
|
+
const RESERVED_PROMPT_VAR_KEYS = [
|
|
21
|
+
"task",
|
|
22
|
+
"docs",
|
|
23
|
+
"__featureArea",
|
|
24
|
+
];
|
|
13
25
|
/**
|
|
14
26
|
* Compile a literacy task into Promptfoo configuration.
|
|
15
27
|
*/
|
|
@@ -58,20 +70,47 @@ function buildPrompts(evalMode) {
|
|
|
58
70
|
// ---------------------------------------------------------------------------
|
|
59
71
|
function buildTestCases(task, evalMode, options, warnings) {
|
|
60
72
|
const tests = [];
|
|
61
|
-
|
|
73
|
+
// W0193: type-erased read of prompt.vars so we can defensively detect
|
|
74
|
+
// reserved keys on legacy-shape `*.task.ts` files (the type narrow makes
|
|
75
|
+
// `task.prompt.vars.task` `never`, but TS task files bypass both the
|
|
76
|
+
// type and the parse-time schema). YAML/inline-task paths have already
|
|
77
|
+
// been migrated by `migratePromptShape` upstream.
|
|
78
|
+
const rawVars = (task.prompt?.vars ?? {});
|
|
79
|
+
const legacyTaskBody = typeof rawVars.task === "string" ? rawVars.task : undefined;
|
|
80
|
+
const promptText = task.prompt?.text ?? legacyTaskBody ?? task.prompt?.template ?? "";
|
|
62
81
|
const contextDocs = task.context?.docs ?? [];
|
|
63
82
|
const taskArea = task.area ?? "";
|
|
64
83
|
const taskTitle = task.title;
|
|
65
|
-
|
|
84
|
+
// Strip reserved keys from the vars spread so they cannot override the
|
|
85
|
+
// canonical assignments below. `safePromptVars` carries only freeform
|
|
86
|
+
// template extras.
|
|
87
|
+
const safePromptVars = {};
|
|
88
|
+
const presentReserved = [];
|
|
89
|
+
for (const [key, value] of Object.entries(rawVars)) {
|
|
90
|
+
if (RESERVED_PROMPT_VAR_KEYS.includes(key)) {
|
|
91
|
+
presentReserved.push(key);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
safePromptVars[key] = value;
|
|
95
|
+
}
|
|
96
|
+
// Single deduplicated deprecation warning per task — even when several
|
|
97
|
+
// reserved keys are present.
|
|
98
|
+
if (presentReserved.length > 0) {
|
|
99
|
+
warnings.push(`Literacy task "${task.id}": deprecated prompt.vars keys ` +
|
|
100
|
+
`(${presentReserved.join(", ")}) — use prompt.text for the prompt ` +
|
|
101
|
+
`body and context.docs for documentation references. The compiler ` +
|
|
102
|
+
`migrated them in-memory, but the task source should be updated.`);
|
|
103
|
+
}
|
|
66
104
|
const hasDocs = contextDocs.length > 0;
|
|
67
105
|
const docsVar = hasDocs ? `file://contexts/canonical/${task.id}.md` : "";
|
|
68
106
|
const assertions = resolveAssertions(task, options, warnings);
|
|
69
|
-
// Gold entry — canonical docs injected
|
|
107
|
+
// Gold entry — canonical docs injected. Spread freeform extras first so
|
|
108
|
+
// canonical keys (task / docs / __featureArea) cannot be overridden.
|
|
70
109
|
const goldVars = {
|
|
110
|
+
...safePromptVars,
|
|
71
111
|
task: promptText,
|
|
72
112
|
docs: docsVar,
|
|
73
113
|
__featureArea: taskArea,
|
|
74
|
-
...promptVars,
|
|
75
114
|
};
|
|
76
115
|
tests.push({
|
|
77
116
|
description: `${taskTitle} (gold)`,
|
|
@@ -89,10 +128,10 @@ function buildTestCases(task, evalMode, options, warnings) {
|
|
|
89
128
|
tests.push({
|
|
90
129
|
description: `${taskTitle} (baseline)`,
|
|
91
130
|
vars: {
|
|
131
|
+
...safePromptVars,
|
|
92
132
|
task: promptText,
|
|
93
133
|
docs: "",
|
|
94
134
|
__featureArea: taskArea,
|
|
95
|
-
...promptVars,
|
|
96
135
|
},
|
|
97
136
|
prompts: ["without-docs"],
|
|
98
137
|
...(baselineAssertions.length > 0
|