@sanity/ailf 0.1.5 → 0.1.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.
- package/dist/_vendor/ailf-core/examples/index.d.ts +1 -1
- package/dist/_vendor/ailf-core/examples/index.js +1 -1
- package/dist/_vendor/ailf-core/ports/context.d.ts +6 -0
- package/dist/_vendor/ailf-core/schemas/pipeline-request.d.ts +1 -53
- package/dist/_vendor/ailf-core/schemas/pipeline-request.js +1 -2
- package/dist/_vendor/ailf-tasks/cli.d.ts +8 -0
- package/dist/_vendor/ailf-tasks/cli.js +61 -0
- package/dist/_vendor/ailf-tasks/index.d.ts +13 -0
- package/dist/_vendor/ailf-tasks/index.js +16 -0
- package/dist/_vendor/ailf-tasks/parser.d.ts +27 -0
- package/dist/_vendor/ailf-tasks/parser.js +73 -0
- package/dist/_vendor/ailf-tasks/schemas.d.ts +186 -0
- package/dist/_vendor/ailf-tasks/schemas.js +176 -0
- package/dist/_vendor/ailf-tasks/validation.d.ts +47 -0
- package/dist/_vendor/ailf-tasks/validation.js +162 -0
- package/dist/adapters/api-client/api-client.d.ts +75 -0
- package/dist/adapters/api-client/api-client.js +201 -0
- package/dist/adapters/api-client/build-request.d.ts +75 -0
- package/dist/adapters/api-client/build-request.js +176 -0
- package/dist/adapters/api-client/errors.d.ts +43 -0
- package/dist/adapters/api-client/errors.js +68 -0
- package/dist/adapters/api-client/format-error.d.ts +22 -0
- package/dist/adapters/api-client/format-error.js +48 -0
- package/dist/adapters/api-client/index.d.ts +13 -0
- package/dist/adapters/api-client/index.js +12 -0
- package/dist/adapters/api-client/progress.d.ts +26 -0
- package/dist/adapters/api-client/progress.js +69 -0
- package/dist/adapters/api-client/remediation.d.ts +19 -0
- package/dist/adapters/api-client/remediation.js +76 -0
- package/dist/adapters/api-client/types.d.ts +98 -0
- package/dist/adapters/api-client/types.js +14 -0
- package/dist/adapters/config-sources/file-config-adapter.js +2 -0
- package/dist/adapters/task-sources/repo-schemas.d.ts +16 -181
- package/dist/adapters/task-sources/repo-schemas.js +27 -184
- package/dist/adapters/task-sources/repo-validation.d.ts +5 -46
- package/dist/adapters/task-sources/repo-validation.js +5 -161
- package/dist/commands/calculate-scores.js +2 -0
- package/dist/commands/explain-handler.js +6 -0
- package/dist/commands/fetch-docs.js +2 -0
- package/dist/commands/generate-configs.js +2 -0
- package/dist/commands/init.js +9 -9
- package/dist/commands/pipeline-action.d.ts +3 -0
- package/dist/commands/pipeline-action.js +13 -0
- package/dist/commands/pipeline.d.ts +2 -0
- package/dist/commands/pipeline.js +2 -0
- package/dist/commands/pr-comment.js +2 -0
- package/dist/commands/publish.js +2 -0
- package/dist/commands/remote-pipeline.d.ts +27 -0
- package/dist/commands/remote-pipeline.js +133 -0
- package/dist/commands/remote-results.d.ts +33 -0
- package/dist/commands/remote-results.js +97 -0
- package/dist/orchestration/build-app-context.js +3 -0
- package/dist/pipeline/map-request-to-config.js +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schemas.ts — Zod schemas for repo-based task definitions.
|
|
3
|
+
*
|
|
4
|
+
* Validates .ailf/tasks/*.yaml task files from external repositories.
|
|
5
|
+
* These schemas are the contract between external repos and the AILF eval
|
|
6
|
+
* pipeline — they define exactly what fields are accepted, with friendly
|
|
7
|
+
* error messages for authors writing task YAML by hand.
|
|
8
|
+
*
|
|
9
|
+
* This module is the single source of truth for task schemas. The eval
|
|
10
|
+
* package re-exports from here to avoid duplication.
|
|
11
|
+
*
|
|
12
|
+
* @see docs/exec-plans/completed/tasks-as-content/phase-4-repo-based-tasks.md
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants — curated assertion types and rubric template names
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/**
|
|
19
|
+
* The set of assertion types allowed in repo-based task files.
|
|
20
|
+
*
|
|
21
|
+
* This is a curated subset of Promptfoo assertion types — we expose only the
|
|
22
|
+
* types that are stable, well-documented, and useful for external authors.
|
|
23
|
+
*/
|
|
24
|
+
export const CURATED_ASSERTION_TYPES = [
|
|
25
|
+
"llm-rubric",
|
|
26
|
+
"contains",
|
|
27
|
+
"contains-any",
|
|
28
|
+
"contains-all",
|
|
29
|
+
"not-contains",
|
|
30
|
+
"icontains",
|
|
31
|
+
"icontains-any",
|
|
32
|
+
"regex",
|
|
33
|
+
"javascript",
|
|
34
|
+
"similar",
|
|
35
|
+
"cost",
|
|
36
|
+
"latency",
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Valid rubric template names — must match keys in config/rubrics.yaml.
|
|
40
|
+
*/
|
|
41
|
+
export const RUBRIC_TEMPLATE_NAMES = [
|
|
42
|
+
"task-completion",
|
|
43
|
+
"code-correctness",
|
|
44
|
+
"doc-coverage",
|
|
45
|
+
];
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Assertion schemas
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Polymorphic canonical doc reference — discriminated by key presence.
|
|
51
|
+
* Exactly one resolution key (slug, path, id, or perspective) must be present.
|
|
52
|
+
*
|
|
53
|
+
* @see docs/design-docs/canonical-doc-resolution.md
|
|
54
|
+
*/
|
|
55
|
+
const SlugDocRefSchema = z.object({
|
|
56
|
+
slug: z.string().min(1),
|
|
57
|
+
reason: z.string().optional().default(""),
|
|
58
|
+
});
|
|
59
|
+
const PathDocRefSchema = z.object({
|
|
60
|
+
path: z.string().min(1),
|
|
61
|
+
reason: z.string().optional().default(""),
|
|
62
|
+
});
|
|
63
|
+
const IdDocRefSchema = z.object({
|
|
64
|
+
id: z.string().min(1),
|
|
65
|
+
reason: z.string().optional().default(""),
|
|
66
|
+
/** Human-readable slug annotation (not used for resolution) */
|
|
67
|
+
slug: z.string().optional(),
|
|
68
|
+
/** Human-readable path annotation (not used for resolution) */
|
|
69
|
+
path: z.string().optional(),
|
|
70
|
+
});
|
|
71
|
+
const PerspectiveDocRefSchema = z.object({
|
|
72
|
+
perspective: z.string().min(1),
|
|
73
|
+
reason: z.string().optional().default(""),
|
|
74
|
+
});
|
|
75
|
+
// Order matters: IdDocRefSchema first because it may also carry `slug`
|
|
76
|
+
// and `path` as optional annotations. Zod tries schemas in order, so
|
|
77
|
+
// entries like `{ id: "...", slug: "..." }` must match IdDocRefSchema
|
|
78
|
+
// (not SlugDocRefSchema).
|
|
79
|
+
const CanonicalDocRefSchema = z.union([
|
|
80
|
+
IdDocRefSchema,
|
|
81
|
+
SlugDocRefSchema,
|
|
82
|
+
PathDocRefSchema,
|
|
83
|
+
PerspectiveDocRefSchema,
|
|
84
|
+
]);
|
|
85
|
+
/**
|
|
86
|
+
* A templated LLM-rubric assertion — uses one of the predefined rubric
|
|
87
|
+
* templates with author-supplied criteria.
|
|
88
|
+
*/
|
|
89
|
+
const TemplatedAssertionSchema = z.object({
|
|
90
|
+
type: z.literal("llm-rubric"),
|
|
91
|
+
template: z.enum(RUBRIC_TEMPLATE_NAMES),
|
|
92
|
+
criteria: z.array(z.string().min(1)).min(1),
|
|
93
|
+
weight: z.number().optional(),
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* A value-based assertion (contains, regex, cost, etc.). Uses .passthrough()
|
|
97
|
+
* to allow extra fields for future extension without schema breakage.
|
|
98
|
+
*/
|
|
99
|
+
const ValueAssertionSchema = z
|
|
100
|
+
.object({
|
|
101
|
+
type: z.enum(CURATED_ASSERTION_TYPES),
|
|
102
|
+
value: z.unknown().optional(),
|
|
103
|
+
threshold: z.number().optional(),
|
|
104
|
+
weight: z.number().optional(),
|
|
105
|
+
})
|
|
106
|
+
.passthrough();
|
|
107
|
+
/** Union of all supported assertion shapes. */
|
|
108
|
+
const AssertionSchema = z.union([
|
|
109
|
+
TemplatedAssertionSchema,
|
|
110
|
+
ValueAssertionSchema,
|
|
111
|
+
]);
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Nested config schemas
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
const BaselineConfigSchema = z
|
|
116
|
+
.object({
|
|
117
|
+
enabled: z.boolean().optional(),
|
|
118
|
+
rubric: z.enum(["abbreviated", "full", "none"]).optional(),
|
|
119
|
+
})
|
|
120
|
+
.optional();
|
|
121
|
+
const ExecutionConfigSchema = z
|
|
122
|
+
.object({
|
|
123
|
+
enabled: z.boolean().optional().default(true),
|
|
124
|
+
blocking: z.boolean().optional().default(false),
|
|
125
|
+
threshold: z
|
|
126
|
+
.object({
|
|
127
|
+
score: z.number().min(0).max(100).optional(),
|
|
128
|
+
})
|
|
129
|
+
.optional(),
|
|
130
|
+
trigger: z
|
|
131
|
+
.object({
|
|
132
|
+
branches: z.array(z.string()).optional(),
|
|
133
|
+
paths: z.array(z.string()).optional(),
|
|
134
|
+
})
|
|
135
|
+
.optional(),
|
|
136
|
+
source: z.string().optional(),
|
|
137
|
+
})
|
|
138
|
+
.optional();
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// RepoTaskSchema — a single task definition from .ailf/tasks/*.yaml
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/**
|
|
143
|
+
* Zod schema for a single repo-based task definition.
|
|
144
|
+
*
|
|
145
|
+
* This is the external-author-facing contract. Field names are camelCase
|
|
146
|
+
* to match the Content Lake document schema (ailf.task).
|
|
147
|
+
*/
|
|
148
|
+
export const RepoTaskSchema = z.object({
|
|
149
|
+
id: z
|
|
150
|
+
.string()
|
|
151
|
+
.min(1)
|
|
152
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, "Task ID must be lowercase alphanumeric with hyphens"),
|
|
153
|
+
description: z.string().min(1),
|
|
154
|
+
featureArea: z
|
|
155
|
+
.string()
|
|
156
|
+
.min(1)
|
|
157
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, "Feature area must be lowercase alphanumeric with hyphens"),
|
|
158
|
+
tags: z.array(z.string()).optional(),
|
|
159
|
+
canonicalDocs: z.array(CanonicalDocRefSchema).optional().default([]),
|
|
160
|
+
vars: z
|
|
161
|
+
.object({
|
|
162
|
+
task: z.string().min(1),
|
|
163
|
+
})
|
|
164
|
+
.passthrough()
|
|
165
|
+
.optional(),
|
|
166
|
+
assert: z.array(AssertionSchema).min(1),
|
|
167
|
+
baseline: BaselineConfigSchema,
|
|
168
|
+
docCoverage: z.boolean().optional().default(false),
|
|
169
|
+
referenceSolution: z.string().optional(),
|
|
170
|
+
execution: ExecutionConfigSchema,
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* Schema for an array of repo tasks — what a single .ailf/tasks/*.yaml file
|
|
174
|
+
* contains. Each file must define at least one task.
|
|
175
|
+
*/
|
|
176
|
+
export const RepoTaskFileSchema = z.array(RepoTaskSchema).min(1);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validation.ts — Semantic validation for repo-based tasks.
|
|
3
|
+
*
|
|
4
|
+
* Checks that go beyond Zod schema parsing:
|
|
5
|
+
* - Assertion types are in the curated set
|
|
6
|
+
* - Rubric template names resolve to known templates
|
|
7
|
+
* - Feature area strings are well-formed
|
|
8
|
+
* - Canonical doc slugs look reasonable (slugs, not URLs)
|
|
9
|
+
*
|
|
10
|
+
* These produce warnings, not errors — the pipeline can still run
|
|
11
|
+
* with imperfect tasks. Only structural failures (caught by Zod) block.
|
|
12
|
+
*/
|
|
13
|
+
import { type RepoTask } from "./schemas.js";
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
valid: boolean;
|
|
16
|
+
errors: ValidationMessage[];
|
|
17
|
+
warnings: ValidationMessage[];
|
|
18
|
+
}
|
|
19
|
+
export interface ValidationMessage {
|
|
20
|
+
taskId: string;
|
|
21
|
+
field: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run semantic validation on an array of parsed repo tasks.
|
|
26
|
+
*
|
|
27
|
+
* Returns warnings for issues that don't block execution (unknown feature
|
|
28
|
+
* areas, unresolved slugs) and errors for issues that would cause pipeline
|
|
29
|
+
* failures (completely missing required fields — though Zod catches most).
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateRepoTasks(tasks: RepoTask[]): ValidationResult;
|
|
32
|
+
/**
|
|
33
|
+
* Format validation results for console output.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatValidationResult(result: ValidationResult): string;
|
|
36
|
+
/**
|
|
37
|
+
* Detect snake_case field names in raw task YAML data.
|
|
38
|
+
*
|
|
39
|
+
* This runs BEFORE Zod parsing to provide a user-friendly error message
|
|
40
|
+
* when authors use framework-internal snake_case names instead of the
|
|
41
|
+
* camelCase names expected in repo task files.
|
|
42
|
+
*
|
|
43
|
+
* @param raw - Raw parsed YAML (before Zod validation)
|
|
44
|
+
* @param filename - Source filename for error messages
|
|
45
|
+
* @returns Array of warning messages (empty if no issues)
|
|
46
|
+
*/
|
|
47
|
+
export declare function detectSnakeCaseFields(raw: unknown, filename: string): string[];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validation.ts — Semantic validation for repo-based tasks.
|
|
3
|
+
*
|
|
4
|
+
* Checks that go beyond Zod schema parsing:
|
|
5
|
+
* - Assertion types are in the curated set
|
|
6
|
+
* - Rubric template names resolve to known templates
|
|
7
|
+
* - Feature area strings are well-formed
|
|
8
|
+
* - Canonical doc slugs look reasonable (slugs, not URLs)
|
|
9
|
+
*
|
|
10
|
+
* These produce warnings, not errors — the pipeline can still run
|
|
11
|
+
* with imperfect tasks. Only structural failures (caught by Zod) block.
|
|
12
|
+
*/
|
|
13
|
+
import { CURATED_ASSERTION_TYPES, RUBRIC_TEMPLATE_NAMES, } from "./schemas.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Public API
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Run semantic validation on an array of parsed repo tasks.
|
|
19
|
+
*
|
|
20
|
+
* Returns warnings for issues that don't block execution (unknown feature
|
|
21
|
+
* areas, unresolved slugs) and errors for issues that would cause pipeline
|
|
22
|
+
* failures (completely missing required fields — though Zod catches most).
|
|
23
|
+
*/
|
|
24
|
+
export function validateRepoTasks(tasks) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
// Check for duplicate IDs
|
|
28
|
+
const seenIds = new Set();
|
|
29
|
+
for (const task of tasks) {
|
|
30
|
+
if (seenIds.has(task.id)) {
|
|
31
|
+
errors.push({
|
|
32
|
+
taskId: task.id,
|
|
33
|
+
field: "id",
|
|
34
|
+
message: `Duplicate task ID "${task.id}"`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
seenIds.add(task.id);
|
|
38
|
+
}
|
|
39
|
+
for (const task of tasks) {
|
|
40
|
+
// Check assertion types
|
|
41
|
+
for (let i = 0; i < task.assert.length; i++) {
|
|
42
|
+
const assertion = task.assert[i];
|
|
43
|
+
if (!CURATED_ASSERTION_TYPES.includes(assertion.type)) {
|
|
44
|
+
warnings.push({
|
|
45
|
+
taskId: task.id,
|
|
46
|
+
field: `assert[${i}].type`,
|
|
47
|
+
message: `Unknown assertion type "${assertion.type}". ` +
|
|
48
|
+
`Valid types: ${CURATED_ASSERTION_TYPES.join(", ")}`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// Check rubric template for llm-rubric assertions
|
|
52
|
+
if (assertion.type === "llm-rubric" && "template" in assertion) {
|
|
53
|
+
const template = assertion.template;
|
|
54
|
+
if (!RUBRIC_TEMPLATE_NAMES.includes(template)) {
|
|
55
|
+
warnings.push({
|
|
56
|
+
taskId: task.id,
|
|
57
|
+
field: `assert[${i}].template`,
|
|
58
|
+
message: `Unknown rubric template "${template}". ` +
|
|
59
|
+
`Valid templates: ${RUBRIC_TEMPLATE_NAMES.join(", ")}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Check canonical doc refs look reasonable
|
|
65
|
+
for (let i = 0; i < (task.canonicalDocs?.length ?? 0); i++) {
|
|
66
|
+
const doc = task.canonicalDocs[i];
|
|
67
|
+
// Slug refs: warn if they look like URLs or paths
|
|
68
|
+
if ("slug" in doc && !("id" in doc) && typeof doc.slug === "string") {
|
|
69
|
+
if (doc.slug.includes("/") || doc.slug.includes("http")) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
taskId: task.id,
|
|
72
|
+
field: `canonicalDocs[${i}].slug`,
|
|
73
|
+
message: `Slug "${doc.slug}" looks like a URL or path — use 'path' type for paths or 'slug' for document slugs (e.g., "groq-introduction")`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Check task has at least one llm-rubric assertion (recommended but not required)
|
|
79
|
+
const hasLlmRubric = task.assert.some((a) => a.type === "llm-rubric");
|
|
80
|
+
if (!hasLlmRubric) {
|
|
81
|
+
warnings.push({
|
|
82
|
+
taskId: task.id,
|
|
83
|
+
field: "assert",
|
|
84
|
+
message: "No llm-rubric assertion found. Tasks should have at least one scored rubric for meaningful evaluation.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Check taskPrompt exists in vars (vars.task)
|
|
88
|
+
if (!task.vars?.task) {
|
|
89
|
+
warnings.push({
|
|
90
|
+
taskId: task.id,
|
|
91
|
+
field: "vars.task",
|
|
92
|
+
message: "No task prompt found in vars.task. The LLM will receive an empty implementation request.",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
valid: errors.length === 0,
|
|
98
|
+
errors,
|
|
99
|
+
warnings,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Format validation results for console output.
|
|
104
|
+
*/
|
|
105
|
+
export function formatValidationResult(result) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
if (result.errors.length > 0) {
|
|
108
|
+
lines.push("❌ Errors:");
|
|
109
|
+
for (const e of result.errors) {
|
|
110
|
+
lines.push(` [${e.taskId}] ${e.field}: ${e.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (result.warnings.length > 0) {
|
|
114
|
+
lines.push("⚠️ Warnings:");
|
|
115
|
+
for (const w of result.warnings) {
|
|
116
|
+
lines.push(` [${w.taskId}] ${w.field}: ${w.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (result.valid && result.warnings.length === 0) {
|
|
120
|
+
lines.push("✅ All repo tasks pass validation");
|
|
121
|
+
}
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Snake_case detection (pre-parse helper)
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/** Known snake_case → camelCase field mappings for common errors */
|
|
128
|
+
const SNAKE_TO_CAMEL = {
|
|
129
|
+
feature_area: "featureArea",
|
|
130
|
+
canonical_docs: "canonicalDocs",
|
|
131
|
+
doc_coverage: "docCoverage",
|
|
132
|
+
reference_solution: "referenceSolution",
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Detect snake_case field names in raw task YAML data.
|
|
136
|
+
*
|
|
137
|
+
* This runs BEFORE Zod parsing to provide a user-friendly error message
|
|
138
|
+
* when authors use framework-internal snake_case names instead of the
|
|
139
|
+
* camelCase names expected in repo task files.
|
|
140
|
+
*
|
|
141
|
+
* @param raw - Raw parsed YAML (before Zod validation)
|
|
142
|
+
* @param filename - Source filename for error messages
|
|
143
|
+
* @returns Array of warning messages (empty if no issues)
|
|
144
|
+
*/
|
|
145
|
+
export function detectSnakeCaseFields(raw, filename) {
|
|
146
|
+
const warnings = [];
|
|
147
|
+
if (!Array.isArray(raw))
|
|
148
|
+
return warnings;
|
|
149
|
+
for (let i = 0; i < raw.length; i++) {
|
|
150
|
+
const entry = raw[i];
|
|
151
|
+
if (typeof entry !== "object" || entry === null)
|
|
152
|
+
continue;
|
|
153
|
+
const obj = entry;
|
|
154
|
+
const taskId = typeof obj.id === "string" ? obj.id : `task[${i}]`;
|
|
155
|
+
for (const [snake, camel] of Object.entries(SNAKE_TO_CAMEL)) {
|
|
156
|
+
if (snake in obj) {
|
|
157
|
+
warnings.push(`[${filename}] ${taskId}: Found "${snake}" — repo tasks use camelCase. Did you mean "${camel}"?`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return warnings;
|
|
162
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/api-client.ts — Typed HTTP client for the AILF API.
|
|
3
|
+
*
|
|
4
|
+
* Handles all communication with the AILF API: submission, polling,
|
|
5
|
+
* report fetching, and task validation. Uses Node's built-in `fetch`
|
|
6
|
+
* (Node 22+) — no external HTTP dependencies.
|
|
7
|
+
*
|
|
8
|
+
* The client is intentionally thin: it builds HTTP requests, parses
|
|
9
|
+
* responses into typed objects, and translates HTTP errors into typed
|
|
10
|
+
* error classes. It does NOT contain business logic.
|
|
11
|
+
*
|
|
12
|
+
* @see packages/api/src/routes/ — server-side route handlers
|
|
13
|
+
* @see docs/design-docs/cli-as-api-client.md — design doc
|
|
14
|
+
*/
|
|
15
|
+
import type { PipelineRequest } from "../../_vendor/ailf-core/index.d.ts";
|
|
16
|
+
import type { ApiClientOptions, JobResponse, SubmitResponse, ValidationResponse, WaitOptions } from "./types.js";
|
|
17
|
+
export declare class ApiClient {
|
|
18
|
+
private readonly apiKey;
|
|
19
|
+
private readonly baseUrl;
|
|
20
|
+
constructor(options: ApiClientOptions);
|
|
21
|
+
/**
|
|
22
|
+
* POST /v1/pipeline — submit an evaluation and return the job info.
|
|
23
|
+
*
|
|
24
|
+
* Returns HTTP 202 on success with a `SubmitResponse` containing the
|
|
25
|
+
* `jobId` and `statusUrl` for polling.
|
|
26
|
+
*/
|
|
27
|
+
submitPipeline(request: PipelineRequest): Promise<SubmitResponse>;
|
|
28
|
+
/**
|
|
29
|
+
* GET /v1/jobs/:id — fetch job status.
|
|
30
|
+
*
|
|
31
|
+
* Supports long-polling via the `Prefer: wait=N` header. When
|
|
32
|
+
* `longPollSeconds` is set, the server holds the connection open
|
|
33
|
+
* for up to N seconds (max 25) waiting for a status change.
|
|
34
|
+
*/
|
|
35
|
+
getJob(jobId: string, options?: {
|
|
36
|
+
longPollSeconds?: number;
|
|
37
|
+
}): Promise<JobResponse>;
|
|
38
|
+
/**
|
|
39
|
+
* Poll until the job reaches a terminal state (completed, failed,
|
|
40
|
+
* timed-out) or the timeout is exceeded.
|
|
41
|
+
*
|
|
42
|
+
* Uses long-polling for efficiency — the server holds the connection
|
|
43
|
+
* instead of the client sleeping between requests.
|
|
44
|
+
*/
|
|
45
|
+
waitForCompletion(jobId: string, options?: WaitOptions): Promise<JobResponse>;
|
|
46
|
+
/**
|
|
47
|
+
* GET /v1/reports/:id/markdown — fetch the rendered markdown report.
|
|
48
|
+
*
|
|
49
|
+
* Returns the raw markdown string. Throws on 404 or error.
|
|
50
|
+
*/
|
|
51
|
+
getReportMarkdown(reportId: string): Promise<string>;
|
|
52
|
+
/**
|
|
53
|
+
* POST /v1/validate/task — validate task definitions against the API.
|
|
54
|
+
*
|
|
55
|
+
* Returns a structured validation result. Does not throw on invalid
|
|
56
|
+
* tasks — the caller checks `response.valid`.
|
|
57
|
+
*/
|
|
58
|
+
validateTasks(tasks: unknown[]): Promise<ValidationResponse>;
|
|
59
|
+
private get;
|
|
60
|
+
private post;
|
|
61
|
+
/**
|
|
62
|
+
* Build standard headers. Auth is always included.
|
|
63
|
+
*/
|
|
64
|
+
private headers;
|
|
65
|
+
/**
|
|
66
|
+
* Fetch with retry on transient network errors.
|
|
67
|
+
* Retries up to MAX_RETRY_ATTEMPTS with exponential backoff.
|
|
68
|
+
* Only retries on network errors (TypeError from fetch), not HTTP errors.
|
|
69
|
+
*/
|
|
70
|
+
private fetchWithRetry;
|
|
71
|
+
/**
|
|
72
|
+
* Parse a non-OK HTTP response and throw the appropriate typed error.
|
|
73
|
+
*/
|
|
74
|
+
private handleErrorResponse;
|
|
75
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api-client/api-client.ts — Typed HTTP client for the AILF API.
|
|
3
|
+
*
|
|
4
|
+
* Handles all communication with the AILF API: submission, polling,
|
|
5
|
+
* report fetching, and task validation. Uses Node's built-in `fetch`
|
|
6
|
+
* (Node 22+) — no external HTTP dependencies.
|
|
7
|
+
*
|
|
8
|
+
* The client is intentionally thin: it builds HTTP requests, parses
|
|
9
|
+
* responses into typed objects, and translates HTTP errors into typed
|
|
10
|
+
* error classes. It does NOT contain business logic.
|
|
11
|
+
*
|
|
12
|
+
* @see packages/api/src/routes/ — server-side route handlers
|
|
13
|
+
* @see docs/design-docs/cli-as-api-client.md — design doc
|
|
14
|
+
*/
|
|
15
|
+
import { ApiAuthError, ApiConnectionError, ApiError, ApiTimeoutError, } from "./errors.js";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Constants
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const DEFAULT_BASE_URL = "https://ailf-api.sanity.build";
|
|
20
|
+
const MAX_LONG_POLL_SECONDS = 25;
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes
|
|
22
|
+
const MAX_RETRY_ATTEMPTS = 3;
|
|
23
|
+
const RETRY_BASE_DELAY_MS = 2000;
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// ApiClient
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export class ApiClient {
|
|
28
|
+
apiKey;
|
|
29
|
+
baseUrl;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.apiKey = options.apiKey;
|
|
32
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
33
|
+
}
|
|
34
|
+
// ─── Submission ─────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* POST /v1/pipeline — submit an evaluation and return the job info.
|
|
37
|
+
*
|
|
38
|
+
* Returns HTTP 202 on success with a `SubmitResponse` containing the
|
|
39
|
+
* `jobId` and `statusUrl` for polling.
|
|
40
|
+
*/
|
|
41
|
+
async submitPipeline(request) {
|
|
42
|
+
return this.post("/v1/pipeline", request);
|
|
43
|
+
}
|
|
44
|
+
// ─── Job status ─────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* GET /v1/jobs/:id — fetch job status.
|
|
47
|
+
*
|
|
48
|
+
* Supports long-polling via the `Prefer: wait=N` header. When
|
|
49
|
+
* `longPollSeconds` is set, the server holds the connection open
|
|
50
|
+
* for up to N seconds (max 25) waiting for a status change.
|
|
51
|
+
*/
|
|
52
|
+
async getJob(jobId, options) {
|
|
53
|
+
const headers = {};
|
|
54
|
+
if (options?.longPollSeconds) {
|
|
55
|
+
const wait = Math.min(options.longPollSeconds, MAX_LONG_POLL_SECONDS);
|
|
56
|
+
headers["Prefer"] = `wait=${wait}`;
|
|
57
|
+
}
|
|
58
|
+
return this.get(`/v1/jobs/${jobId}`, headers);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Poll until the job reaches a terminal state (completed, failed,
|
|
62
|
+
* timed-out) or the timeout is exceeded.
|
|
63
|
+
*
|
|
64
|
+
* Uses long-polling for efficiency — the server holds the connection
|
|
65
|
+
* instead of the client sleeping between requests.
|
|
66
|
+
*/
|
|
67
|
+
async waitForCompletion(jobId, options) {
|
|
68
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress, longPollSeconds = MAX_LONG_POLL_SECONDS, } = options ?? {};
|
|
69
|
+
const deadline = Date.now() + timeoutMs;
|
|
70
|
+
const terminalStatuses = new Set(["completed", "failed", "timed-out"]);
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
let job;
|
|
73
|
+
try {
|
|
74
|
+
job = await this.getJob(jobId, { longPollSeconds });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// Retry on transient errors during polling
|
|
78
|
+
if (err instanceof ApiConnectionError && Date.now() < deadline) {
|
|
79
|
+
await sleep(RETRY_BASE_DELAY_MS);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
onProgress?.(job);
|
|
85
|
+
if (terminalStatuses.has(job.status)) {
|
|
86
|
+
return job;
|
|
87
|
+
}
|
|
88
|
+
// Long-polling already handles the delay; only add a small
|
|
89
|
+
// backoff if the server responded instantly (non-terminal).
|
|
90
|
+
}
|
|
91
|
+
throw new ApiTimeoutError(jobId, timeoutMs);
|
|
92
|
+
}
|
|
93
|
+
// ─── Reports ────────────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* GET /v1/reports/:id/markdown — fetch the rendered markdown report.
|
|
96
|
+
*
|
|
97
|
+
* Returns the raw markdown string. Throws on 404 or error.
|
|
98
|
+
*/
|
|
99
|
+
async getReportMarkdown(reportId) {
|
|
100
|
+
const url = `${this.baseUrl}/v1/reports/${reportId}/markdown`;
|
|
101
|
+
const response = await this.fetchWithRetry(url, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: this.headers({ Accept: "text/markdown" }),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
await this.handleErrorResponse(response, url);
|
|
107
|
+
}
|
|
108
|
+
return response.text();
|
|
109
|
+
}
|
|
110
|
+
// ─── Validation ─────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* POST /v1/validate/task — validate task definitions against the API.
|
|
113
|
+
*
|
|
114
|
+
* Returns a structured validation result. Does not throw on invalid
|
|
115
|
+
* tasks — the caller checks `response.valid`.
|
|
116
|
+
*/
|
|
117
|
+
async validateTasks(tasks) {
|
|
118
|
+
return this.post("/v1/validate/task", { tasks });
|
|
119
|
+
}
|
|
120
|
+
// ─── Internal HTTP methods ──────────────────────────────────────
|
|
121
|
+
async get(path, extraHeaders) {
|
|
122
|
+
const url = `${this.baseUrl}${path}`;
|
|
123
|
+
const response = await this.fetchWithRetry(url, {
|
|
124
|
+
method: "GET",
|
|
125
|
+
headers: this.headers(extraHeaders),
|
|
126
|
+
});
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
await this.handleErrorResponse(response, url);
|
|
129
|
+
}
|
|
130
|
+
return response.json();
|
|
131
|
+
}
|
|
132
|
+
async post(path, body) {
|
|
133
|
+
const url = `${this.baseUrl}${path}`;
|
|
134
|
+
const response = await this.fetchWithRetry(url, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
137
|
+
body: JSON.stringify(body),
|
|
138
|
+
});
|
|
139
|
+
// 202 is success for async operations
|
|
140
|
+
if (!response.ok && response.status !== 202) {
|
|
141
|
+
await this.handleErrorResponse(response, url);
|
|
142
|
+
}
|
|
143
|
+
return response.json();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build standard headers. Auth is always included.
|
|
147
|
+
*/
|
|
148
|
+
headers(extra) {
|
|
149
|
+
return {
|
|
150
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
151
|
+
...extra,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fetch with retry on transient network errors.
|
|
156
|
+
* Retries up to MAX_RETRY_ATTEMPTS with exponential backoff.
|
|
157
|
+
* Only retries on network errors (TypeError from fetch), not HTTP errors.
|
|
158
|
+
*/
|
|
159
|
+
async fetchWithRetry(url, init) {
|
|
160
|
+
let lastError;
|
|
161
|
+
for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
|
|
162
|
+
try {
|
|
163
|
+
return await fetch(url, init);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
lastError = err;
|
|
167
|
+
// Only retry on network-level errors (not HTTP status errors)
|
|
168
|
+
if (attempt < MAX_RETRY_ATTEMPTS - 1) {
|
|
169
|
+
const delay = RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
170
|
+
await sleep(delay);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw new ApiConnectionError(`Could not reach the AILF API at ${url}`, url, lastError);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Parse a non-OK HTTP response and throw the appropriate typed error.
|
|
178
|
+
*/
|
|
179
|
+
async handleErrorResponse(response, url) {
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = (await response.json());
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Non-JSON error response (e.g., HTML from CDN)
|
|
186
|
+
}
|
|
187
|
+
const code = body?.error?.code ?? `http_${response.status}`;
|
|
188
|
+
const message = body?.error?.message ??
|
|
189
|
+
`HTTP ${response.status} ${response.statusText} (${url})`;
|
|
190
|
+
if (response.status === 401 || response.status === 403) {
|
|
191
|
+
throw new ApiAuthError(message, code, response.status);
|
|
192
|
+
}
|
|
193
|
+
throw new ApiError(message, code, response.status, body?.error?.param);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Helpers
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
function sleep(ms) {
|
|
200
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
201
|
+
}
|