@sanity/ailf 3.2.0 → 3.3.1

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * validate-tasks command — standalone validation of task files.
3
3
  *
4
- * Validates .ailf/tasks/*.yaml files against the CanonicalTaskSchema without
5
- * running the full pipeline. Useful for pre-commit hooks and CI checks
6
- * in external repos.
4
+ * Validates .ailf/tasks/*.yaml and .ailf/tasks/*.task.ts files against the
5
+ * CanonicalTaskSchema without running the full pipeline. Useful for
6
+ * pre-commit hooks and CI checks in external repos.
7
7
  *
8
8
  * Usage:
9
9
  * ailf validate-tasks .ailf/tasks/
@@ -11,97 +11,141 @@
11
11
  *
12
12
  * @see packages/eval/src/adapters/task-sources/repo-schemas.ts
13
13
  * @see packages/eval/src/adapters/task-sources/repo-validation.ts
14
+ * @see packages/eval/src/adapters/task-sources/task-file-loader.ts
14
15
  */
15
16
  import { existsSync, readdirSync, readFileSync } from "fs";
16
- import { resolve, relative } from "path";
17
+ import { resolve, relative, basename } from "path";
17
18
  import { Command } from "commander";
18
19
  import { load } from "js-yaml";
19
20
  import { detectLegacyFieldNames, parseCanonicalTaskFile, } from "../adapters/task-sources/repo-schemas.js";
20
21
  import { validateCanonicalTasks, formatValidationResult, } from "../adapters/task-sources/repo-validation.js";
22
+ import { discoverTsTaskFiles, loadTsTaskFile, } from "../adapters/task-sources/task-file-loader.js";
21
23
  export function createValidateTasksCommand() {
22
24
  return new Command("validate-tasks")
23
- .description("Validate task YAML files (.ailf/tasks/) against the canonical schema")
25
+ .description("Validate task files (YAML and TypeScript) in .ailf/tasks/ against the canonical schema")
24
26
  .argument("[path]", "Path to tasks directory (default: .ailf/tasks/)", ".ailf/tasks")
25
27
  .option("--strict", "Treat warnings as errors", false)
26
28
  .action(async (tasksPath, opts) => {
27
- // Resolve relative to the caller's working directory, not the
28
- // eval package root (which differs when run via bin/ailf.js)
29
- const callerCwd = process.env.AILF_CALLER_CWD ?? process.cwd();
30
- const resolvedPath = resolve(callerCwd, tasksPath);
31
- if (!existsSync(resolvedPath)) {
32
- console.error(`Directory not found: ${resolvedPath}`);
33
- process.exit(1);
29
+ const exitCode = await runValidateTasks(tasksPath, opts);
30
+ process.exit(exitCode);
31
+ });
32
+ }
33
+ /**
34
+ * Execute the validate-tasks command logic. Returns the exit code (0 success,
35
+ * 1 failure) so callers can decide how to surface it — the CLI wrapper calls
36
+ * `process.exit`, tests can assert directly.
37
+ */
38
+ export async function runValidateTasks(tasksPath, opts) {
39
+ // Resolve relative to the caller's working directory, not the
40
+ // eval package root (which differs when run via bin/ailf.js)
41
+ const callerCwd = opts.callerCwd ?? process.env.AILF_CALLER_CWD ?? process.cwd();
42
+ const resolvedPath = resolve(callerCwd, tasksPath);
43
+ if (!existsSync(resolvedPath)) {
44
+ console.error(`Directory not found: ${resolvedPath}`);
45
+ return 1;
46
+ }
47
+ const yamlFiles = readdirSync(resolvedPath).filter((f) => (f.endsWith(".yaml") || f.endsWith(".yml")) && !f.startsWith("."));
48
+ const tsFiles = discoverTsTaskFiles(resolvedPath);
49
+ const fileCount = yamlFiles.length + tsFiles.length;
50
+ if (fileCount === 0) {
51
+ console.error(`No task files found in ${resolvedPath}\n` +
52
+ " Expected .yaml, .yml, .task.ts, or .task.js files");
53
+ return 1;
54
+ }
55
+ console.log(`\nValidating ${fileCount} task file(s) in ${relative(process.cwd(), resolvedPath)}/\n`);
56
+ let totalTasks = 0;
57
+ let hasErrors = false;
58
+ const allTasks = [];
59
+ for (const file of yamlFiles) {
60
+ const filePath = resolve(resolvedPath, file);
61
+ const raw = readFileSync(filePath, "utf-8");
62
+ let parsed;
63
+ try {
64
+ parsed = load(raw);
34
65
  }
35
- const yamlFiles = readdirSync(resolvedPath).filter((f) => (f.endsWith(".yaml") || f.endsWith(".yml")) && !f.startsWith("."));
36
- if (yamlFiles.length === 0) {
37
- console.error(`No YAML files found in ${resolvedPath}`);
38
- process.exit(1);
66
+ catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ console.error(` ${file}: YAML parse error`);
69
+ console.error(` ${msg}\n`);
70
+ hasErrors = true;
71
+ continue;
39
72
  }
40
- console.log(`\nValidating ${yamlFiles.length} task file(s) in ${relative(process.cwd(), resolvedPath)}/\n`);
41
- let totalTasks = 0;
42
- let hasErrors = false;
43
- const allTasks = [];
44
- for (const file of yamlFiles) {
45
- const filePath = resolve(resolvedPath, file);
46
- const raw = readFileSync(filePath, "utf-8");
47
- let parsed;
48
- try {
49
- parsed = load(raw);
50
- }
51
- catch (err) {
52
- const msg = err instanceof Error ? err.message : String(err);
53
- console.error(` ${file}: YAML parse error`);
54
- console.error(` ${msg}\n`);
55
- hasErrors = true;
56
- continue;
57
- }
58
- if (!Array.isArray(parsed)) {
59
- console.error(` ${file}: Expected a YAML array of task definitions`);
60
- hasErrors = true;
61
- continue;
62
- }
63
- // Detect legacy field names before Zod validation
64
- const legacyWarnings = detectLegacyFieldNames(parsed, file);
65
- if (legacyWarnings.length > 0) {
66
- console.error(` ${file}: Uses legacy field names`);
67
- for (const w of legacyWarnings) {
68
- console.error(` ${w}`);
69
- }
70
- console.error();
71
- hasErrors = true;
72
- continue;
73
- }
74
- try {
75
- const tasks = parseCanonicalTaskFile(parsed, file);
76
- console.log(` ${file}: ${tasks.length} task${tasks.length === 1 ? "" : "s"} valid`);
77
- totalTasks += tasks.length;
78
- allTasks.push(...tasks);
79
- }
80
- catch (err) {
81
- const msg = err instanceof Error ? err.message : String(err);
82
- console.error(` ${file}: Schema validation failed`);
83
- console.error(`${msg
84
- .split("\n")
85
- .map((l) => ` ${l}`)
86
- .join("\n")}\n`);
87
- hasErrors = true;
88
- }
73
+ if (!Array.isArray(parsed)) {
74
+ console.error(` ${file}: Expected a YAML array of task definitions`);
75
+ hasErrors = true;
76
+ continue;
89
77
  }
90
- // Run semantic validation on all parsed tasks
91
- if (allTasks.length > 0) {
92
- console.log(); // blank line
93
- const semanticResult = validateCanonicalTasks(allTasks);
94
- const formatted = formatValidationResult(semanticResult);
95
- console.log(formatted);
96
- if (!semanticResult.valid) {
97
- hasErrors = true;
98
- }
99
- if (opts.strict && semanticResult.warnings.length > 0) {
100
- hasErrors = true;
101
- console.log("\n --strict mode: warnings treated as errors");
102
- }
78
+ if (!validateTaskArray(parsed, file, allTasks)) {
79
+ hasErrors = true;
80
+ continue;
103
81
  }
104
- console.log(`\n${hasErrors ? "FAIL" : "OK"} ${totalTasks} task${totalTasks === 1 ? "" : "s"} across ${yamlFiles.length} file${yamlFiles.length === 1 ? "" : "s"}\n`);
105
- process.exit(hasErrors ? 1 : 0);
106
- });
82
+ totalTasks += parsed.length;
83
+ }
84
+ for (const tsFilePath of tsFiles) {
85
+ const file = basename(tsFilePath);
86
+ let loaded;
87
+ try {
88
+ loaded = await loadTsTaskFile(tsFilePath);
89
+ }
90
+ catch (err) {
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ console.error(` ${file}: Failed to load TypeScript task file`);
93
+ console.error(` ${msg}\n`);
94
+ hasErrors = true;
95
+ continue;
96
+ }
97
+ if (!validateTaskArray(loaded.tasks, file, allTasks)) {
98
+ hasErrors = true;
99
+ continue;
100
+ }
101
+ totalTasks += loaded.tasks.length;
102
+ }
103
+ if (allTasks.length > 0) {
104
+ console.log();
105
+ const semanticResult = validateCanonicalTasks(allTasks);
106
+ const formatted = formatValidationResult(semanticResult);
107
+ console.log(formatted);
108
+ if (!semanticResult.valid) {
109
+ hasErrors = true;
110
+ }
111
+ if (opts.strict && semanticResult.warnings.length > 0) {
112
+ hasErrors = true;
113
+ console.log("\n --strict mode: warnings treated as errors");
114
+ }
115
+ }
116
+ console.log(`\n${hasErrors ? "FAIL" : "OK"} ${totalTasks} task${totalTasks === 1 ? "" : "s"} across ${fileCount} file${fileCount === 1 ? "" : "s"}\n`);
117
+ return hasErrors ? 1 : 0;
118
+ }
119
+ /**
120
+ * Validate an array of raw task entries — runs the legacy-field detector and
121
+ * the canonical Zod schema, appending valid tasks to `accumulator`.
122
+ *
123
+ * Returns `true` when the file is fully valid, `false` when any error was
124
+ * reported (the caller is responsible for flipping its own error flag).
125
+ */
126
+ function validateTaskArray(entries, file, accumulator) {
127
+ const legacyWarnings = detectLegacyFieldNames(entries, file);
128
+ if (legacyWarnings.length > 0) {
129
+ console.error(` ${file}: Uses legacy field names`);
130
+ for (const w of legacyWarnings) {
131
+ console.error(` ${w}`);
132
+ }
133
+ console.error();
134
+ return false;
135
+ }
136
+ try {
137
+ const tasks = parseCanonicalTaskFile(entries, file);
138
+ console.log(` ${file}: ${tasks.length} task${tasks.length === 1 ? "" : "s"} valid`);
139
+ accumulator.push(...tasks);
140
+ return true;
141
+ }
142
+ catch (err) {
143
+ const msg = err instanceof Error ? err.message : String(err);
144
+ console.error(` ${file}: Schema validation failed`);
145
+ console.error(`${msg
146
+ .split("\n")
147
+ .map((l) => ` ${l}`)
148
+ .join("\n")}\n`);
149
+ return false;
150
+ }
107
151
  }
package/dist/index.d.ts CHANGED
@@ -39,3 +39,5 @@ export { env } from "./_vendor/ailf-core/index.d.ts";
39
39
  export type { AgentHarnessTaskDefinition, CustomTaskDefinition, GeneralizedAssertionDefinition, GeneralizedDocRef, GeneralizedTaskDefinition, GeneralizedTemplatedAssertion, GeneralizedValueAssertion, IdDocRef, KnowledgeProbeTaskDefinition, LiteracyTaskDefinition, MCPServerTaskDefinition, PathDocRef, PerspectiveDocRef, RubricRef, SlugDocRef, TaskCommonFields, TaskDifficulty, TaskOptions, TaskProviderConfig, TaskStatus, } from "./_vendor/ailf-core/index.d.ts";
40
40
  export { CanonicalTaskFileSchema, CanonicalTaskSchema, CURATED_ASSERTION_TYPES, detectLegacyFieldNames, parseCanonicalTaskFile, RUBRIC_TEMPLATE_NAMES, type CanonicalTask, type CuratedAssertionType, type RubricTemplateName, } from "./adapters/task-sources/repo-schemas.js";
41
41
  export { formatValidationResult, validateCanonicalTasks, type ValidationMessage, type ValidationResult, } from "./adapters/task-sources/repo-validation.js";
42
+ export { InMemoryPluginRegistry } from "./_vendor/ailf-core/index.d.ts";
43
+ export type { CompilationContext, ModeBase, ModeCompileResult, ModeHandler, PresetDefinition, } from "./_vendor/ailf-core/index.d.ts";
package/dist/index.js CHANGED
@@ -46,3 +46,7 @@ export { env } from "./_vendor/ailf-core/index.js";
46
46
  // ---------------------------------------------------------------------------
47
47
  export { CanonicalTaskFileSchema, CanonicalTaskSchema, CURATED_ASSERTION_TYPES, detectLegacyFieldNames, parseCanonicalTaskFile, RUBRIC_TEMPLATE_NAMES, } from "./adapters/task-sources/repo-schemas.js";
48
48
  export { formatValidationResult, validateCanonicalTasks, } from "./adapters/task-sources/repo-validation.js";
49
+ // ---------------------------------------------------------------------------
50
+ // Plugin extension points — for authoring custom presets, modes, and registries
51
+ // ---------------------------------------------------------------------------
52
+ export { InMemoryPluginRegistry } from "./_vendor/ailf-core/index.js";
@@ -25,6 +25,7 @@ import { createRequire } from "module";
25
25
  import { existsSync, readFileSync } from "fs";
26
26
  import { load } from "js-yaml";
27
27
  import { resolve } from "path";
28
+ import { resolveAilfAlias } from "../../adapters/config-sources/ailf-resolver.js";
28
29
  /**
29
30
  * Load a config file by name, searching for TS/JS/YAML/JSON variants.
30
31
  *
@@ -134,7 +135,11 @@ function loadTsFile(filePath, format) {
134
135
  // jiti supports sync loading. Use createRequire for ESM compatibility.
135
136
  const esmRequire = createRequire(import.meta.url);
136
137
  const { createJiti } = esmRequire("jiti");
137
- const jiti = createJiti(filePath, { interopDefault: true });
138
+ const alias = resolveAilfAlias(filePath);
139
+ const jiti = createJiti(filePath, {
140
+ interopDefault: true,
141
+ ...(alias ? { alias } : {}),
142
+ });
138
143
  const mod = jiti(filePath);
139
144
  const data = (mod?.default ?? mod);
140
145
  return { data, filePath, format };
@@ -14,6 +14,7 @@ import { existsSync } from "fs";
14
14
  import { resolve } from "path";
15
15
  import { pathToFileURL } from "url";
16
16
  import { createJiti } from "jiti";
17
+ import { resolveAilfAlias } from "../../adapters/config-sources/ailf-resolver.js";
17
18
  /** Thrown for preset-specific load errors (distinguishes from third-party errors) */
18
19
  class PresetLoadError extends Error {
19
20
  constructor(message) {
@@ -53,9 +54,11 @@ function loadSinglePreset(ref, rootDir) {
53
54
  }
54
55
  }
55
56
  try {
57
+ const alias = resolveAilfAlias(filePath);
56
58
  const jiti = createJiti(pathToFileURL(rootDir).href, {
57
59
  interopDefault: true,
58
60
  requireCache: true,
61
+ ...(alias ? { alias } : {}),
59
62
  });
60
63
  // jiti() is the synchronous loader
61
64
  const mod = jiti(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"