@sanity/ailf 4.0.5 → 4.0.7

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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @sanity/ailf-core — Cross-package sentinel constants.
3
+ *
4
+ * Sentinel strings shared between the init template (which writes them
5
+ * into `.github/workflows/ailf-eval.yml`) and the consumers that must
6
+ * recognize them (run-context builder, caller envelope assembly, CLI
7
+ * pre-flight checks). Centralizing here keeps the placeholder definition
8
+ * in exactly one place — duplicating the literal across producers and
9
+ * consumers historically caused the literal to leak into Sanity reports
10
+ * (W0143).
11
+ */
12
+ /**
13
+ * The literal string `ailf init` writes into the scaffolded GitHub Actions
14
+ * workflow as the value of `AILF_OWNER_TEAM`. Consumers treat this string
15
+ * as semantically unset — it must never end up persisted as a real team
16
+ * slug on a run report.
17
+ *
18
+ * @see packages/core/examples/ailf-eval-workflow.yml — the canonical source
19
+ * @see packages/eval/src/pipeline/run-context.ts — sanitizes on read
20
+ * @see packages/eval/src/adapters/api-client/build-request.ts — drops on the wire
21
+ */
22
+ export declare const PLACEHOLDER_OWNER_TEAM = "<REPLACE-WITH-YOUR-TEAM-SLUG>";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @sanity/ailf-core — Cross-package sentinel constants.
3
+ *
4
+ * Sentinel strings shared between the init template (which writes them
5
+ * into `.github/workflows/ailf-eval.yml`) and the consumers that must
6
+ * recognize them (run-context builder, caller envelope assembly, CLI
7
+ * pre-flight checks). Centralizing here keeps the placeholder definition
8
+ * in exactly one place — duplicating the literal across producers and
9
+ * consumers historically caused the literal to leak into Sanity reports
10
+ * (W0143).
11
+ */
12
+ /**
13
+ * The literal string `ailf init` writes into the scaffolded GitHub Actions
14
+ * workflow as the value of `AILF_OWNER_TEAM`. Consumers treat this string
15
+ * as semantically unset — it must never end up persisted as a real team
16
+ * slug on a run report.
17
+ *
18
+ * @see packages/core/examples/ailf-eval-workflow.yml — the canonical source
19
+ * @see packages/eval/src/pipeline/run-context.ts — sanitizes on read
20
+ * @see packages/eval/src/adapters/api-client/build-request.ts — drops on the wire
21
+ */
22
+ export const PLACEHOLDER_OWNER_TEAM = "<REPLACE-WITH-YOUR-TEAM-SLUG>";
@@ -17,6 +17,7 @@ export * from "./services/index.js";
17
17
  export * from "./examples/index.js";
18
18
  export * from "./artifact-registry.js";
19
19
  export * from "./batch-signing.js";
20
+ export * from "./constants.js";
20
21
  export { defineCanaryTasks, defineConfig, defineFeatures, defineModeBase, defineModels, definePricingTable, definePreset, definePrompts, defineRubrics, defineSchedules, defineSinks, defineSources, defineTask, defineTestBudgets, defineThresholds, } from "./config-helpers.js";
21
22
  export type { PricingEntry, PromptEntry, SourceEntry, } from "./config-helpers.js";
22
23
  export { env } from "./env-helper.js";
@@ -17,6 +17,7 @@ export * from "./services/index.js";
17
17
  export * from "./examples/index.js";
18
18
  export * from "./artifact-registry.js";
19
19
  export * from "./batch-signing.js";
20
+ export * from "./constants.js";
20
21
  // ---------------------------------------------------------------------------
21
22
  // Architecture overhaul — Phase 0 helpers
22
23
  // ---------------------------------------------------------------------------
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { existsSync } from "fs";
16
16
  import { resolve } from "path";
17
- import { PipelineRequestSchema, } from "../../_vendor/ailf-core/index.js";
17
+ import { PLACEHOLDER_OWNER_TEAM, PipelineRequestSchema, } from "../../_vendor/ailf-core/index.js";
18
18
  import { LEGACY_EVAL_MODE_ALIASES, isRunClassification, } from "../../_vendor/ailf-shared/index.js";
19
19
  import { RepoTaskSource } from "../task-sources/repo-task-source.js";
20
20
  const LEGACY_LITERACY_VARIANT_SET = new Set(LEGACY_EVAL_MODE_ALIASES);
@@ -316,7 +316,12 @@ export function buildCallerEnvelope(config) {
316
316
  }
317
317
  }
318
318
  // Owner: flag > env. Team required, individual optional.
319
- const team = config.ownerTeamOption ?? process.env.AILF_OWNER_TEAM?.trim() ?? undefined;
319
+ // W0143: drop the init-template placeholder when a consumer hasn't
320
+ // filled in their team slug yet we treat it as unset rather than
321
+ // shipping the literal `<REPLACE-WITH-YOUR-TEAM-SLUG>` across the wire
322
+ // and into the report's Provenance card.
323
+ const rawTeam = config.ownerTeamOption ?? process.env.AILF_OWNER_TEAM?.trim() ?? undefined;
324
+ const team = rawTeam === PLACEHOLDER_OWNER_TEAM ? undefined : rawTeam;
320
325
  const individual = config.ownerIndividualOption ??
321
326
  process.env.AILF_OWNER_INDIVIDUAL?.trim() ??
322
327
  process.env.GITHUB_ACTOR?.trim() ??
@@ -376,12 +376,16 @@ export function parseCanonicalTaskFile(raw, filename) {
376
376
  // (featureArea, canonicalDocs, assert, vars), surface a helpful error
377
377
  // message telling them what the canonical names are.
378
378
  // ---------------------------------------------------------------------------
379
+ // Phrasing avoids literal `{` and `}` characters on purpose. GitHub Actions
380
+ // registers each line of a multi-line secret as a mask, so a pretty-printed
381
+ // JSON secret introduces standalone `{` / `}` masks that then redact every
382
+ // `{` / `}` in subsequent log output. See W0144.
379
383
  /** Old field names from @sanity/ailf-tasks → canonical equivalents */
380
384
  const LEGACY_FIELD_MAP = {
381
385
  featureArea: "area",
382
- canonicalDocs: "context.docs (nested under context: { docs: [...] })",
386
+ canonicalDocs: "context.docs (move the docs array under a context parent)",
383
387
  assert: "assertions",
384
- vars: "prompt (nested under prompt: { text: ... })",
388
+ vars: "prompt (move the text string under a prompt parent)",
385
389
  };
386
390
  /**
387
391
  * Detect legacy field names in raw task data and return helpful messages.
@@ -19,8 +19,18 @@
19
19
  * @see packages/core/src/ports/task-source.ts — TaskSource port
20
20
  */
21
21
  import type { FilterOptions, GeneralizedTaskDefinition, TaskSource } from "../../_vendor/ailf-core/index.d.ts";
22
+ export interface RepoTaskSourceOptions {
23
+ /**
24
+ * When true, treat a missing directory or empty task set as a valid
25
+ * empty result instead of throwing. Used by the composition root for
26
+ * the AILF-bundled `tasks/${mode}/` source, which is missing in some
27
+ * test rootDirs and modes that ship no defaults.
28
+ */
29
+ allowMissing?: boolean;
30
+ }
22
31
  export declare class RepoTaskSource implements TaskSource {
23
32
  private readonly tasksDir;
24
- constructor(tasksDir: string);
33
+ private readonly options;
34
+ constructor(tasksDir: string, options?: RepoTaskSourceOptions);
25
35
  loadTasks(filter?: FilterOptions): Promise<GeneralizedTaskDefinition[]>;
26
36
  }
@@ -26,16 +26,17 @@ import { detectLegacyFieldNames, parseCanonicalTaskFile, } from "./repo-schemas.
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);
29
- // ---------------------------------------------------------------------------
30
- // RepoTaskSource adapter
31
- // ---------------------------------------------------------------------------
32
29
  export class RepoTaskSource {
33
30
  tasksDir;
34
- constructor(tasksDir) {
31
+ options;
32
+ constructor(tasksDir, options = {}) {
35
33
  this.tasksDir = tasksDir;
34
+ this.options = options;
36
35
  }
37
36
  async loadTasks(filter) {
38
37
  if (!existsSync(this.tasksDir)) {
38
+ if (this.options.allowMissing)
39
+ return [];
39
40
  throw new Error(`Repo tasks directory not found: ${this.tasksDir}\n` +
40
41
  " Provide a valid path via --repo-tasks-path");
41
42
  }
@@ -44,6 +45,8 @@ export class RepoTaskSource {
44
45
  .sort();
45
46
  const tsFiles = discoverTsTaskFiles(this.tasksDir);
46
47
  if (yamlFiles.length === 0 && tsFiles.length === 0) {
48
+ if (this.options.allowMissing)
49
+ return [];
47
50
  throw new Error(`No task files found in ${this.tasksDir}\n` +
48
51
  " Expected .ailf/tasks/*.yaml or .ailf/tasks/*.task.ts files");
49
52
  }
@@ -20,6 +20,7 @@ import { buildAppContext, parseArtifactUploadEnv, } from "../orchestration/build
20
20
  import { buildStepSequence } from "../orchestration/build-step-sequence.js";
21
21
  import { orchestratePipeline } from "../orchestration/pipeline-orchestrator.js";
22
22
  import { load } from "js-yaml";
23
+ import { PLACEHOLDER_OWNER_TEAM } from "../_vendor/ailf-core/index.js";
23
24
  import { parseRepoConfig, } from "../adapters/task-sources/repo-schemas.js";
24
25
  import { getCallerCwd, resolveOutputDir } from "./shared/resolve-output-dir.js";
25
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -406,6 +407,7 @@ function resolveRepoTasksPath(callerCwd, explicitPath, taskSourceType) {
406
407
  * 4. Delegate to the PipelineOrchestrator
407
408
  */
408
409
  export async function executePipeline(cliOpts) {
410
+ warnIfPlaceholderOwnerTeam();
409
411
  // When --config is provided, resolve config from file instead of CLI flags
410
412
  if (cliOpts.config) {
411
413
  const { existsSync } = await import("fs");
@@ -493,6 +495,20 @@ export async function executePipeline(cliOpts) {
493
495
  // ---------------------------------------------------------------------------
494
496
  // Internal helpers
495
497
  // ---------------------------------------------------------------------------
498
+ /**
499
+ * W0143: warn once on stderr when the consumer is still running with the
500
+ * unedited init-template placeholder for AILF_OWNER_TEAM. The run
501
+ * continues — provenance just gets attributed as unset rather than the
502
+ * literal placeholder.
503
+ */
504
+ function warnIfPlaceholderOwnerTeam() {
505
+ if (process.env.AILF_OWNER_TEAM?.trim() !== PLACEHOLDER_OWNER_TEAM)
506
+ return;
507
+ console.warn(` ⚠️ AILF_OWNER_TEAM is still set to the init-template placeholder ` +
508
+ `"${PLACEHOLDER_OWNER_TEAM}". Provenance will be logged as unset. ` +
509
+ `Set a real team slug in .github/workflows/ailf-eval.yml (or unset ` +
510
+ `AILF_OWNER_TEAM) to attribute this run.`);
511
+ }
496
512
  /**
497
513
  * Resolve CLI options into typed ResolvedOptions.
498
514
  */
@@ -16,6 +16,7 @@
16
16
  * @see docs/archive/exec-plans/ports-and-adapters/phase-7-composition-root.md
17
17
  */
18
18
  import { type AppContext, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssertionRegistration, type Logger, type ResolvedConfig } from "./_vendor/ailf-core/index.d.ts";
19
+ import { CompositeTaskSource, ContentLakeTaskSource, RepoTaskSource } from "./adapters/task-sources/index.js";
19
20
  /**
20
21
  * Create a fully wired AppContext from resolved configuration.
21
22
  *
@@ -42,6 +43,12 @@ export declare function createAppContext(config: ResolvedConfig): AppContext;
42
43
  * Exported for unit-test access; not part of the public package API.
43
44
  */
44
45
  export declare function createArtifactWriter(config: ResolvedConfig, logger: Logger, progress?: ArtifactWriterProgressOptions): ArtifactWriter;
46
+ /**
47
+ * Build the `TaskSource` adapter wired by the composition root for a
48
+ * given `ResolvedConfig`. Exported for test access — composition-root
49
+ * wiring is a contract worth asserting directly.
50
+ */
51
+ export declare function createTaskSource(config: ResolvedConfig): CompositeTaskSource | ContentLakeTaskSource | RepoTaskSource;
45
52
  /**
46
53
  * Generic Promptfoo assertion types available to all evaluation modes.
47
54
  *
@@ -32,6 +32,7 @@ import { PromptfooEvalAdapter } from "./adapters/eval-runners/promptfoo-eval-ada
32
32
  import { ConsoleLogger, JsonLogger, QuietLogger, } from "./adapters/loggers/index.js";
33
33
  import { ConsoleProgressReporter } from "./adapters/progress/console-progress-reporter.js";
34
34
  import { CompositeTaskSource, ContentLakeTaskSource, RepoTaskSource, } from "./adapters/task-sources/index.js";
35
+ import { resolveVendoredSubdir } from "./pipeline/compiler/config-loader.js";
35
36
  import { createAgentHarnessBase, createKnowledgeProbeBase, createLiteracyModeBase, createMcpServerModeBase, } from "./pipeline/compiler/mode-bases/index.js";
36
37
  import { createSanityLiteracyPreset } from "./pipeline/compiler/presets/index.js";
37
38
  import { getSanityClient } from "./sanity/client.js";
@@ -297,7 +298,12 @@ function createCache(config) {
297
298
  return local;
298
299
  return new ContentLakeCacheAdapter(local, createReportStore(config));
299
300
  }
300
- function createTaskSource(config) {
301
+ /**
302
+ * Build the `TaskSource` adapter wired by the composition root for a
303
+ * given `ResolvedConfig`. Exported for test access — composition-root
304
+ * wiring is a contract worth asserting directly.
305
+ */
306
+ export function createTaskSource(config) {
301
307
  // "repo" mode — use ONLY repo tasks, no Content Lake or YAML merge.
302
308
  // This is the correct mode for API-triggered inline-task evaluations
303
309
  // where the caller sent their own task definitions. Without this,
@@ -309,21 +315,28 @@ function createTaskSource(config) {
309
315
  }
310
316
  return new RepoTaskSource(config.repoTasksPath);
311
317
  }
312
- // Primary source Content Lake (the only non-repo source remaining)
313
- const primary = new ContentLakeTaskSource(getSanityClient({
314
- token: process.env.AILF_REPORT_SANITY_API_TOKEN ??
315
- process.env.SANITY_API_TOKEN ??
316
- undefined,
317
- }));
318
- // If repo tasks path is set, combine primary + repo sources.
319
- // This is the "augment" mode — repo tasks extend the primary source.
318
+ // "content-lake"Studio-authored ailf.task documents only.
319
+ if (config.taskSourceType === "content-lake") {
320
+ return new ContentLakeTaskSource(getSanityClient({
321
+ token: process.env.AILF_REPORT_SANITY_API_TOKEN ??
322
+ process.env.SANITY_API_TOKEN ??
323
+ undefined,
324
+ }));
325
+ }
326
+ // Unset — AILF-bundled defaults from `tasks/${mode}/`, optionally
327
+ // augmented with the caller's `--repo-tasks-path` (W0146). The
328
+ // bundled directory is allowed to be missing so test rootDirs and
329
+ // modes that ship no defaults degrade gracefully to the augment
330
+ // source (or empty).
331
+ const bundledDir = resolveVendoredSubdir(config.rootDir, `tasks/${config.mode}`);
332
+ const bundled = new RepoTaskSource(bundledDir, { allowMissing: true });
320
333
  if (config.repoTasksPath) {
321
334
  return new CompositeTaskSource([
322
- primary,
335
+ bundled,
323
336
  new RepoTaskSource(config.repoTasksPath),
324
337
  ]);
325
338
  }
326
- return primary;
339
+ return bundled;
327
340
  }
328
341
  // ---------------------------------------------------------------------------
329
342
  // Layer 0: Framework built-in assertions
@@ -17,7 +17,6 @@ import { emitFileContents } from "../../artifact-capture/emit-file.js";
17
17
  import { getStepInputPaths } from "../../pipeline/cache.js";
18
18
  import { buildCacheContext } from "../cache-context.js";
19
19
  import { checkCanonicalContextsExist } from "../../pipeline/checks.js";
20
- import { loadPipelineTasks } from "../load-pipeline-tasks.js";
21
20
  import { loadSource } from "../../sources.js";
22
21
  import { configToSourceOverrides } from "../config-to-source-overrides.js";
23
22
  export class FetchDocsStep {
@@ -30,35 +29,16 @@ export class FetchDocsStep {
30
29
  return { status: "skipped", reason: "--no-fetch" };
31
30
  }
32
31
  const start = Date.now();
33
- // Load tasks — use the same source as GenerateConfigsStep to avoid
34
- // a mismatch where configs reference context files that were never
35
- // fetched.
36
- //
37
- // Adapter path: ctx.taskSource handles both content-lake and repo modes.
38
- // The composition root wires the right adapter (ContentLakeTaskSource
39
- // or RepoTaskSource) per taskSourceType. RepoTaskSource loads BOTH
40
- // .yaml and .task.ts files necessary for external-consumer evals
41
- // that materialize inline tasks as YAML (W0148).
42
- // Filesystem path: load from .task.ts files (legacy unset path
43
- // AILF defaults from tasks/${mode}/ + optional repoTasksPath augment).
44
- let allTasks;
45
- if (ctx.config.taskSourceType === "content-lake" ||
46
- ctx.config.taskSourceType === "repo") {
47
- const filter = {
48
- ...(ctx.config.areas?.length ? { areas: ctx.config.areas } : {}),
49
- ...(ctx.config.tasks?.length ? { taskIds: ctx.config.tasks } : {}),
50
- ...(ctx.config.tags?.length ? { tags: ctx.config.tags } : {}),
51
- };
52
- allTasks = await ctx.taskSource.loadTasks(Object.keys(filter).length > 0 ? filter : undefined);
53
- }
54
- else {
55
- allTasks = await loadPipelineTasks({
56
- rootDir: ctx.config.rootDir,
57
- mode: ctx.config.mode,
58
- repoTasksPath: ctx.config.repoTasksPath,
59
- taskSourceType: ctx.config.taskSourceType,
60
- });
61
- }
32
+ // Load tasks via ctx.taskSource — the composition root wires the
33
+ // right adapter for every taskSourceType (W0146). FetchDocsStep and
34
+ // GenerateConfigsStep MUST go through the same adapter so configs
35
+ // reference context files that were actually fetched.
36
+ const filter = {
37
+ ...(ctx.config.areas?.length ? { areas: ctx.config.areas } : {}),
38
+ ...(ctx.config.tasks?.length ? { taskIds: ctx.config.tasks } : {}),
39
+ ...(ctx.config.tags?.length ? { tags: ctx.config.tags } : {}),
40
+ };
41
+ const allTasks = await ctx.taskSource.loadTasks(Object.keys(filter).length > 0 ? filter : undefined);
62
42
  // Bridge: narrow to literacy tasks for canonical doc access
63
43
  const literacyTasks = allTasks.filter((t) => t.mode === "literacy");
64
44
  const tasksWithDocs = literacyTasks.filter((t) => (t.context?.docs?.length ?? 0) > 0);
@@ -17,24 +17,17 @@ export declare class GenerateConfigsStep implements PipelineStep {
17
17
  execute(ctx: AppContext, state: PipelineState): Promise<StepResult>;
18
18
  private compileLiteracyVariants;
19
19
  private compileSingleMode;
20
- private loadTasks;
21
20
  /**
22
- * Load tasks via ctx.taskSource (the composition-root-wired adapter).
21
+ * Load tasks via ctx.taskSource the single adapter wired by the
22
+ * composition root for every taskSourceType (W0146). FetchDocsStep
23
+ * and GenerateConfigsStep MUST go through the same adapter so configs
24
+ * reference context files that were actually fetched.
23
25
  *
24
- * Used for both `taskSourceType === "content-lake"` (ContentLakeTaskSource)
25
- * and `taskSourceType === "repo"` (RepoTaskSource). Filtering by
26
- * area/task/tag is delegated to the adapter — ContentLakeTaskSource
27
- * pushes it into the GROQ query, RepoTaskSource applies it in-memory.
26
+ * Filtering by area/task/tag is delegated to the adapter:
27
+ * ContentLakeTaskSource pushes it into the GROQ query;
28
+ * RepoTaskSource applies it in-memory.
28
29
  */
29
- private loadTasksFromAdapter;
30
- /**
31
- * Load tasks from filesystem .task.ts files.
32
- *
33
- * This is the original path used for repo-based and inline tasks.
34
- * It scans tasks/{mode}/ and optionally --repo-tasks-path.
35
- */
36
- private loadTasksFromFilesystem;
37
- private applyFilters;
30
+ private loadTasks;
38
31
  /**
39
32
  * Build a descriptive error message when no tasks match the current filters.
40
33
  * Distinguishes between "no tasks exist" and "tasks exist but filters exclude them".
@@ -208,99 +208,36 @@ export class GenerateConfigsStep {
208
208
  // ---------------------------------------------------------------------------
209
209
  // Task loading — unified for all modes
210
210
  // ---------------------------------------------------------------------------
211
- async loadTasks(ctx, mode, state) {
212
- // Adapter path — use ctx.taskSource. The composition root wires the
213
- // right adapter for each taskSourceType:
214
- // - "content-lake" → ContentLakeTaskSource (Studio-owned ailf.task docs)
215
- // - "repo" → RepoTaskSource (loads .yaml AND .task.ts from repoTasksPath)
216
- // Routing both through ctx.taskSource keeps the orchestration step
217
- // file-format-agnostic (W0148: external-consumer evals materialize
218
- // inline tasks as .yaml, which loadPipelineTasks can't read).
219
- if (ctx.config.taskSourceType === "content-lake" ||
220
- ctx.config.taskSourceType === "repo") {
221
- return this.loadTasksFromAdapter(ctx, state);
222
- }
223
- // Filesystem path — load from .task.ts files (legacy unset path:
224
- // AILF defaults from tasks/${mode}/ + optional repoTasksPath augment).
225
- return this.loadTasksFromFilesystem(ctx, mode, state);
226
- }
227
211
  /**
228
- * Load tasks via ctx.taskSource (the composition-root-wired adapter).
212
+ * Load tasks via ctx.taskSource the single adapter wired by the
213
+ * composition root for every taskSourceType (W0146). FetchDocsStep
214
+ * and GenerateConfigsStep MUST go through the same adapter so configs
215
+ * reference context files that were actually fetched.
229
216
  *
230
- * Used for both `taskSourceType === "content-lake"` (ContentLakeTaskSource)
231
- * and `taskSourceType === "repo"` (RepoTaskSource). Filtering by
232
- * area/task/tag is delegated to the adapter — ContentLakeTaskSource
233
- * pushes it into the GROQ query, RepoTaskSource applies it in-memory.
217
+ * Filtering by area/task/tag is delegated to the adapter:
218
+ * ContentLakeTaskSource pushes it into the GROQ query;
219
+ * RepoTaskSource applies it in-memory.
234
220
  */
235
- async loadTasksFromAdapter(ctx, state) {
221
+ async loadTasks(ctx, mode, state) {
236
222
  const filter = {
237
223
  ...(ctx.config.areas?.length ? { areas: ctx.config.areas } : {}),
238
224
  ...(ctx.config.tasks?.length ? { taskIds: ctx.config.tasks } : {}),
239
225
  ...(ctx.config.tags?.length ? { tags: ctx.config.tags } : {}),
240
226
  };
241
- const tasks = await ctx.taskSource.loadTasks(Object.keys(filter).length > 0 ? filter : undefined);
242
- // Capture loaded IDs for error messages (same as filesystem path)
243
- this.lastLoadedTaskIds = tasks
244
- .map((t) => t.id)
245
- .filter((id) => !!id);
246
- // Release auto-scope
247
- if (state.releaseAutoScope && !ctx.config.noAutoScope) {
248
- const scopedIds = new Set(state.releaseAutoScope.affectedTaskIds);
249
- const beforeCount = tasks.length;
250
- const scoped = tasks.filter((t) => "id" in t && scopedIds.has(t.id));
251
- ctx.logger.info(` 🎯 Auto-scoped to ${scoped.length} of ${beforeCount} task(s) affected by release`);
252
- return scoped;
253
- }
254
- return tasks;
255
- }
256
- /**
257
- * Load tasks from filesystem .task.ts files.
258
- *
259
- * This is the original path used for repo-based and inline tasks.
260
- * It scans tasks/{mode}/ and optionally --repo-tasks-path.
261
- */
262
- async loadTasksFromFilesystem(ctx, mode, state) {
263
- const { resolve } = await import("path");
264
- const { discoverTsTaskFiles, loadTsTaskFile } = await import("../../adapters/task-sources/task-file-loader.js");
265
- const { resolveVendoredSubdir } = await import("../../pipeline/compiler/config-loader.js");
266
- // Discover task files from the mode-specific directory and --repo-tasks-path.
267
- // Use vendored copies in dist/ when @sanity/ailf-core isn't resolvable
268
- // (i.e., running outside the monorepo via npx).
269
- //
270
- // When taskSourceType === "repo", skip the AILF-bundled tasks/${mode}/
271
- // directory and load ONLY from repoTasksPath. Mirrors the composition-root
272
- // contract for repo-only mode (see composition-root.ts:392-405).
273
- const dirs = [];
274
- if (ctx.config.taskSourceType !== "repo") {
275
- dirs.push(resolveVendoredSubdir(ctx.config.rootDir, `tasks/${mode}`));
276
- }
277
- else if (!ctx.config.repoTasksPath) {
278
- throw new Error('taskSourceType "repo" requires repoTasksPath to be set (no AILF defaults loaded in repo-only mode)');
279
- }
280
- // Also search --repo-tasks-path (e.g., .ailf/tasks/) for repo-based tasks
281
- if (ctx.config.repoTasksPath) {
282
- const repoDir = resolve(ctx.config.repoTasksPath);
283
- if (!dirs.includes(repoDir)) {
284
- dirs.push(repoDir);
285
- }
286
- }
227
+ const allTasks = await ctx.taskSource.loadTasks(Object.keys(filter).length > 0 ? filter : undefined);
228
+ // Mode filter the adapter may return a mixed-mode set (e.g. a user's
229
+ // `--repo-tasks-path` containing tasks of multiple modes). Skip
230
+ // non-matching modes with a warning so unintentional misclassification
231
+ // is visible without breaking the run.
287
232
  const tasks = [];
288
233
  const skippedByMode = new Map();
289
- for (const dir of dirs) {
290
- const files = discoverTsTaskFiles(dir);
291
- for (const file of files) {
292
- const raw = await loadTsTaskFile(file);
293
- for (const t of raw.tasks) {
294
- const task = t;
295
- // Filter to matching mode (skip tasks from other modes in same dir)
296
- if (!("mode" in task) || task.mode === mode) {
297
- tasks.push(task);
298
- }
299
- else {
300
- const taskMode = task.mode ?? "unknown";
301
- skippedByMode.set(taskMode, (skippedByMode.get(taskMode) ?? 0) + 1);
302
- }
303
- }
234
+ for (const task of allTasks) {
235
+ if (!("mode" in task) || task.mode === mode) {
236
+ tasks.push(task);
237
+ }
238
+ else {
239
+ const taskMode = task.mode ?? "unknown";
240
+ skippedByMode.set(taskMode, (skippedByMode.get(taskMode) ?? 0) + 1);
304
241
  }
305
242
  }
306
243
  if (skippedByMode.size > 0) {
@@ -310,46 +247,17 @@ export class GenerateConfigsStep {
310
247
  .join(", ");
311
248
  ctx.logger.warn(` ⚠ Skipped ${total} task(s) with non-matching mode (${summary}). Current pipeline mode: ${mode}. Run with --mode <mode> to include them.`);
312
249
  }
313
- // Apply area/task/tag filters
314
- const filtered = this.applyFilters(ctx, tasks);
315
- // Release auto-scope
250
+ this.lastLoadedTaskIds = tasks
251
+ .map((t) => t.id)
252
+ .filter((id) => !!id);
316
253
  if (state.releaseAutoScope && !ctx.config.noAutoScope) {
317
254
  const scopedIds = new Set(state.releaseAutoScope.affectedTaskIds);
318
- const beforeCount = filtered.length;
319
- const scoped = filtered.filter((t) => "id" in t && scopedIds.has(t.id));
255
+ const beforeCount = tasks.length;
256
+ const scoped = tasks.filter((t) => "id" in t && scopedIds.has(t.id));
320
257
  ctx.logger.info(` 🎯 Auto-scoped to ${scoped.length} of ${beforeCount} task(s) affected by release`);
321
258
  return scoped;
322
259
  }
323
- return filtered;
324
- }
325
- applyFilters(ctx, tasks) {
326
- // Capture pre-filter IDs for diagnostic messages
327
- this.lastLoadedTaskIds = tasks
328
- .map((t) => t.id)
329
- .filter((id) => !!id);
330
- let result = tasks;
331
- if (ctx.config.areas?.length) {
332
- const allowed = new Set(ctx.config.areas.map((a) => a.toLowerCase()));
333
- result = result.filter((t) => {
334
- const area = t.area?.toLowerCase();
335
- return area && allowed.has(area);
336
- });
337
- }
338
- if (ctx.config.tasks?.length) {
339
- const allowed = new Set(ctx.config.tasks);
340
- result = result.filter((t) => {
341
- const id = t.id;
342
- return id && allowed.has(id);
343
- });
344
- }
345
- if (ctx.config.tags?.length) {
346
- const allowed = new Set(ctx.config.tags);
347
- result = result.filter((t) => {
348
- const tags = t.tags;
349
- return tags?.some((tag) => allowed.has(tag));
350
- });
351
- }
352
- return result;
260
+ return tasks;
353
261
  }
354
262
  /**
355
263
  * Build a descriptive error message when no tasks match the current filters.
@@ -28,6 +28,18 @@ export class MirrorRepoTasksStep {
28
28
  if (!ctx.config.repoTasksPath) {
29
29
  return { status: "skipped", reason: "No --repo-tasks-path provided" };
30
30
  }
31
+ // W0145 — never mirror under repo-only mode. The API gateway maps
32
+ // PipelineRequest.taskMode="inline" → taskSourceType="repo", so an
33
+ // external consumer's ephemeral inline tasks would otherwise be
34
+ // upserted into AILF's canonical Content Lake. Mirroring is only
35
+ // correct for the in-tree dogfood path (taskSourceType unset +
36
+ // repoTasksPath set, e.g. external-eval.yml).
37
+ if (ctx.config.taskSourceType === "repo") {
38
+ return {
39
+ status: "skipped",
40
+ reason: 'taskSourceType="repo" — inline tasks are not mirrored',
41
+ };
42
+ }
31
43
  // Need a write token for mirroring
32
44
  const token = process.env.AILF_REPORT_SANITY_API_TOKEN ?? process.env.SANITY_API_TOKEN;
33
45
  if (!token) {
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * @see docs/decisions/D0032-run-anchored-artifact-store.md (§ Move 5 — Drift Prevention)
14
14
  */
15
- import type { Logger, RunContext } from "../_vendor/ailf-core/index.d.ts";
15
+ import { type Logger, type RunContext } from "../_vendor/ailf-core/index.d.ts";
16
16
  import { type RunClassification, type RunExecutor, type RunExecutorSurface, type RunHost, type RunLineage, type RunOwner, type RunTool } from "../_vendor/ailf-shared/index.d.ts";
17
17
  import type { ResolvedSourceConfig } from "../sources.js";
18
18
  import type { EvalMode } from "./types.js";
@@ -92,6 +92,11 @@ export declare function detectClassification(log: Logger): RunClassification;
92
92
  /**
93
93
  * Resolve `owner` from `AILF_OWNER_TEAM` (+ optional
94
94
  * `AILF_OWNER_INDIVIDUAL`). `team` is free-form; default is `"unknown"`.
95
+ *
96
+ * The init-template placeholder (`PLACEHOLDER_OWNER_TEAM`) is treated as
97
+ * if the env var were unset — consumers that haven't filled it in yet
98
+ * end up with `team: "unknown"` instead of the literal placeholder being
99
+ * persisted onto the report (W0143).
95
100
  */
96
101
  export declare function detectOwner(): RunOwner;
97
102
  /**
@@ -15,6 +15,7 @@
15
15
  import { execSync } from "node:child_process";
16
16
  import { createRequire } from "node:module";
17
17
  import * as os from "node:os";
18
+ import { PLACEHOLDER_OWNER_TEAM, } from "../_vendor/ailf-core/index.js";
18
19
  import { isRunClassification, } from "../_vendor/ailf-shared/index.js";
19
20
  import { ConsoleLogger } from "../adapters/loggers/index.js";
20
21
  import { tryLoadConfigFile } from "./compiler/config-loader.js";
@@ -45,7 +46,15 @@ export function buildRunContext(input) {
45
46
  // preservation across the --remote boundary.
46
47
  const envelope = input.callerEnvelope;
47
48
  const classification = envelope?.classification ?? detectClassification(log);
48
- const owner = envelope?.owner ?? detectOwner();
49
+ // W0143: a caller-supplied owner whose team is the init-template
50
+ // placeholder is treated as "user hasn't filled this in yet" — drop the
51
+ // envelope owner and fall through to env detection (which performs the
52
+ // same sanitization on AILF_OWNER_TEAM). Avoids persisting the literal
53
+ // placeholder verbatim on the report.
54
+ const sanitizedEnvelopeOwner = envelope?.owner && envelope.owner.team !== PLACEHOLDER_OWNER_TEAM
55
+ ? envelope.owner
56
+ : undefined;
57
+ const owner = sanitizedEnvelopeOwner ?? detectOwner();
49
58
  const executor = envelope?.executor ?? detectExecutor();
50
59
  // `tool` and `host` are server-environment facts — they always reflect
51
60
  // where this pipeline is actually running, never what a caller claimed.
@@ -184,9 +193,15 @@ export function detectClassification(log) {
184
193
  /**
185
194
  * Resolve `owner` from `AILF_OWNER_TEAM` (+ optional
186
195
  * `AILF_OWNER_INDIVIDUAL`). `team` is free-form; default is `"unknown"`.
196
+ *
197
+ * The init-template placeholder (`PLACEHOLDER_OWNER_TEAM`) is treated as
198
+ * if the env var were unset — consumers that haven't filled it in yet
199
+ * end up with `team: "unknown"` instead of the literal placeholder being
200
+ * persisted onto the report (W0143).
187
201
  */
188
202
  export function detectOwner() {
189
- const team = process.env.AILF_OWNER_TEAM?.trim() || "unknown";
203
+ const rawTeam = process.env.AILF_OWNER_TEAM?.trim();
204
+ const team = rawTeam && rawTeam !== PLACEHOLDER_OWNER_TEAM ? rawTeam : "unknown";
190
205
  const individual = process.env.AILF_OWNER_INDIVIDUAL?.trim() || undefined;
191
206
  return individual ? { individual, team } : { team };
192
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf",
3
- "version": "4.0.5",
3
+ "version": "4.0.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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
- }